require_relative '../spec_helper' require_relative 'fixtures/send' # Why so many fixed arg tests? JRuby and I assume other Ruby impls have # separate call paths for simple fixed arity methods. Testing up to five # will verify special and generic arity code paths for all impls. # # Method naming conventions: # M - Mandatory Args # O - Optional Arg # R - Rest Arg # Q - Post Mandatory Args specs = LangSendSpecs describe "Invoking a method" do describe "with zero arguments" do it "requires no arguments passed" do specs.fooM0.should == 100 end it "raises ArgumentError if the method has a positive arity" do -> { specs.fooM1 }.should raise_error(ArgumentError) end end describe "with only mandatory arguments" do it "requires exactly the same number of passed values" do specs.fooM1(1).should == [1] specs.fooM2(1,2).should == [1,2] specs.fooM3(1,2,3).should == [1,2,3] specs.fooM4(1,2,3,4).should == [1,2,3,4] specs.fooM5(1,2,3,4,5).should == [1,2,3,4,5] end it "raises ArgumentError if the methods arity doesn't match" do -> { specs.fooM1(1,2) }.should raise_error(ArgumentError) end end describe "with optional arguments" do it "uses the optional argument if none is is passed" do specs.fooM0O1.should == [1] end it "uses the passed argument if available" do specs.fooM0O1(2).should == [2] end it "raises ArgumentError if extra arguments are passed" do -> { specs.fooM0O1(2,3) }.should raise_error(ArgumentError) end end describe "with mandatory and optional arguments" do it "uses the passed values in left to right order" do specs.fooM1O1(2).should == [2,1] end it "raises an ArgumentError if there are no values for the mandatory args" do -> { specs.fooM1O1 }.should raise_error(ArgumentError) end it "raises an ArgumentError if too many values are passed" do -> { specs.fooM1O1(1,2,3) }.should raise_error(ArgumentError) end end describe "with a rest argument" do it "is an empty array if there are no additional arguments" do specs.fooM0R().should == [] specs.fooM1R(1).should == [1, []] end it "gathers unused arguments" do specs.fooM0R(1).should == [1] specs.fooM1R(1,2).should == [1, [2]] end end it "with a block makes it available to yield" do specs.oneb(10) { 200 }.should == [10,200] end it "with a block converts the block to a Proc" do prc = specs.makeproc { "hello" } prc.should be_kind_of(Proc) prc.call.should == "hello" end it "with an object as a block uses 'to_proc' for coercion" do o = LangSendSpecs::ToProc.new(:from_to_proc) specs.makeproc(&o).call.should == :from_to_proc specs.yield_now(&o).should == :from_to_proc end it "raises a SyntaxError with both a literal block and an object as block" do -> { eval "specs.oneb(10, &l){ 42 }" }.should raise_error(SyntaxError) end it "with same names as existing variables is ok" do foobar = 100 def foobar; 200; end foobar.should == 100 foobar().should == 200 end it "with splat operator makes the object the direct arguments" do a = [1,2,3] specs.fooM3(*a).should == [1,2,3] end it "without parentheses works" do (specs.fooM3 1,2,3).should == [1,2,3] end it "with a space separating method name and parenthesis treats expression in parenthesis as first argument" do specs.weird_parens().should == "55" end describe "allows []=" do before :each do @obj = LangSendSpecs::AttrSet.new end it "with *args in the [] expanded to individual arguments" do ary = [2,3] (@obj[1, *ary] = 4).should == 4 @obj.result.should == [1,2,3,4] end it "with multiple *args" do ary = [2,3] post = [4,5] (@obj[1, *ary] = *post).should == [4,5] @obj.result.should == [1,2,3,[4,5]] end it "with multiple *args and does not unwrap the last splat" do ary = [2,3] post = [4] (@obj[1, *ary] = *post).should == [4] @obj.result.should == [1,2,3,[4]] end it "with a *args and multiple rhs args" do ary = [2,3] (@obj[1, *ary] = 4, 5).should == [4,5] @obj.result.should == [1,2,3,[4,5]] end end it "passes literal hashes without curly braces as the last parameter" do specs.fooM3('abc', 456, 'rbx' => 'cool', 'specs' => 'fail sometimes', 'oh' => 'weh').should == \ ['abc', 456, {'rbx' => 'cool', 'specs' => 'fail sometimes', 'oh' => 'weh'}] end it "passes a literal hash without curly braces or parens" do (specs.fooM3 'abc', 456, 'rbx' => 'cool', 'specs' => 'fail sometimes', 'oh' => 'weh').should == \ ['abc', 456, { 'rbx' => 'cool', 'specs' => 'fail sometimes', 'oh' => 'weh'}] end it "allows to literal hashes without curly braces as the only parameter" do specs.fooM1(rbx: :cool, specs: :fail_sometimes).should == [{ rbx: :cool, specs: :fail_sometimes }] (specs.fooM1 rbx: :cool, specs: :fail_sometimes).should == [{ rbx: :cool, specs: :fail_sometimes }] end describe "when the method is not available" do it "invokes method_missing if it is defined" do o = LangSendSpecs::MethodMissing.new o.not_there(1,2) o.message.should == :not_there o.args.should == [1,2] end it "raises NameError if invoked as a vcall" do -> { no_such_method }.should raise_error NameError end it "should omit the method_missing call from the backtrace for NameError" do -> { no_such_method }.should raise_error { |e| e.backtrace.first.should_not include("method_missing") } end it "raises NoMethodError if invoked as an unambiguous method call" do -> { no_such_method() }.should raise_error NoMethodError -> { no_such_method(1,2,3) }.should raise_error NoMethodError end it "should omit the method_missing call from the backtrace for NoMethodError" do -> { no_such_method() }.should raise_error { |e| e.backtrace.first.should_not include("method_missing") } end end end describe "Invoking a public setter method" do it 'returns the set value' do klass = Class.new do def foobar=(*) 1 end end (klass.new.foobar = 'bar').should == 'bar' (klass.new.foobar = 'bar', 'baz').should == ["bar", "baz"] end end describe "Invoking []= methods" do it 'returns the set value' do klass = Class.new do def []=(*) 1 end end (klass.new[33] = 'bar').should == 'bar' (klass.new[33] = 'bar', 'baz').should == ['bar', 'baz'] (klass.new[33, 34] = 'bar', 'baz').should == ['bar', 'baz'] end end describe "Invoking a private setter method" do describe "permits self as a receiver" do it "for normal assignment" do receiver = LangSendSpecs::PrivateSetter.new receiver.call_self_foo_equals(42) receiver.foo.should == 42 end it "for multiple assignment" do receiver = LangSendSpecs::PrivateSetter.new receiver.call_self_foo_equals_masgn(42) receiver.foo.should == 42 end end end describe "Invoking a private getter method" do ruby_version_is ""..."2.7" do it "does not permit self as a receiver" do receiver = LangSendSpecs::PrivateGetter.new -> { receiver.call_self_foo }.should raise_error(NoMethodError) -> { receiver.call_self_foo_or_equals(6) }.should raise_error(NoMethodError) end end ruby_version_is "2.7" do it "permits self as a receiver" do receiver = LangSendSpecs::PrivateGetter.new receiver.call_self_foo_or_equals(6) receiver.call_self_foo.should == 6 end end end describe "Invoking a method" do describe "with required args after the rest arguments" do it "binds the required arguments first" do specs.fooM0RQ1(1).should == [[], 1] specs.fooM0RQ1(1,2).should == [[1], 2] specs.fooM0RQ1(1,2,3).should == [[1,2], 3] specs.fooM1RQ1(1,2).should == [1, [], 2] specs.fooM1RQ1(1,2,3).should == [1, [2], 3] specs.fooM1RQ1(1,2,3,4).should == [1, [2, 3], 4] specs.fooM1O1RQ1(1,2).should == [1, 9, [], 2] specs.fooM1O1RQ1(1,2,3).should == [1, 2, [], 3] specs.fooM1O1RQ1(1,2,3,4).should == [1, 2, [3], 4] specs.fooM1O1RQ2(1,2,3).should == [1, 9, [], 2, 3] specs.fooM1O1RQ2(1,2,3,4).should == [1, 2, [], 3, 4] specs.fooM1O1RQ2(1,2,3,4,5).should == [1, 2, [3], 4, 5] end end describe "with mandatory arguments after optional arguments" do it "binds the required arguments first" do specs.fooO1Q1(0,1).should == [0,1] specs.fooO1Q1(2).should == [1,2] specs.fooM1O1Q1(2,3,4).should == [2,3,4] specs.fooM1O1Q1(1,3).should == [1,2,3] specs.fooM2O1Q1(1,2,4).should == [1,2,3,4] specs.fooM2O2Q1(1,2,3,4,5).should == [1,2,3,4,5] specs.fooM2O2Q1(1,2,3,5).should == [1,2,3,4,5] specs.fooM2O2Q1(1,2,5).should == [1,2,3,4,5] specs.fooO4Q1(1,2,3,4,5).should == [1,2,3,4,5] specs.fooO4Q1(1,2,3,5).should == [1,2,3,4,5] specs.fooO4Q1(1,2,5).should == [1,2,3,4,5] specs.fooO4Q1(1,5).should == [1,2,3,4,5] specs.fooO4Q1(5).should == [1,2,3,4,5] specs.fooO4Q2(1,2,3,4,5,6).should == [1,2,3,4,5,6] specs.fooO4Q2(1,2,3,5,6).should == [1,2,3,4,5,6] specs.fooO4Q2(1,2,5,6).should == [1,2,3,4,5,6] specs.fooO4Q2(1,5,6).should == [1,2,3,4,5,6] specs.fooO4Q2(5,6).should == [1,2,3,4,5,6] end end it "with .() invokes #call" do q = proc { |z| z } q.(1).should == 1 obj = mock("paren call") obj.should_receive(:call).and_return(:called) obj.().should == :called end it "allows a vestigial trailing ',' in the arguments" do specs.fooM1(1,).should == [1] end it "with splat operator attempts to coerce it to an Array if the object respond_to?(:to_a)" do ary = [2,3,4] obj = mock("to_a") obj.should_receive(:to_a).and_return(ary).twice specs.fooM0R(*obj).should == ary specs.fooM1R(1,*obj).should == [1, ary] end it "with splat operator * and non-Array value uses value unchanged if it does not respond_to?(:to_ary)" do obj = Object.new obj.should_not respond_to(:to_a) specs.fooM0R(*obj).should == [obj] specs.fooM1R(1,*obj).should == [1, [obj]] end it "accepts additional arguments after splat expansion" do a = [1,2] specs.fooM4(*a,3,4).should == [1,2,3,4] specs.fooM4(0,*a,3).should == [0,1,2,3] end it "does not expand final array arguments after a splat expansion" do a = [1, 2] specs.fooM3(*a, [3, 4]).should == [1, 2, [3, 4]] end it "accepts final explicit literal Hash arguments after the splat" do a = [1, 2] specs.fooM0RQ1(*a, { a: 1 }).should == [[1, 2], { a: 1 }] end it "accepts final implicit literal Hash arguments after the splat" do a = [1, 2] specs.fooM0RQ1(*a, a: 1).should == [[1, 2], { a: 1 }] end it "accepts final Hash arguments after the splat" do a = [1, 2] b = { a: 1 } specs.fooM0RQ1(*a, b).should == [[1, 2], { a: 1 }] end it "accepts mandatory and explicit literal Hash arguments after the splat" do a = [1, 2] specs.fooM0RQ2(*a, 3, { a: 1 }).should == [[1, 2], 3, { a: 1 }] end it "accepts mandatory and implicit literal Hash arguments after the splat" do a = [1, 2] specs.fooM0RQ2(*a, 3, a: 1).should == [[1, 2], 3, { a: 1 }] end it "accepts mandatory and Hash arguments after the splat" do a = [1, 2] b = { a: 1 } specs.fooM0RQ2(*a, 3, b).should == [[1, 2], 3, { a: 1 }] end it "converts a final splatted explicit Hash to an Array" do a = [1, 2] specs.fooR(*a, 3, *{ a: 1 }).should == [1, 2, 3, [:a, 1]] end it "calls #to_a to convert a final splatted Hash object to an Array" do a = [1, 2] b = { a: 1 } b.should_receive(:to_a).and_return([:a, 1]) specs.fooR(*a, 3, *b).should == [1, 2, 3, :a, 1] end it "accepts multiple splat expansions in the same argument list" do a = [1,2,3] b = 7 c = mock("pseudo-array") c.should_receive(:to_a).and_return([0,0]) d = [4,5] specs.rest_len(*a,*d,6,*b).should == 7 specs.rest_len(*a,*a,*a).should == 9 specs.rest_len(0,*a,4,*5,6,7,*c,-1).should == 11 end ruby_version_is ""..."3.0" do it "expands the Array elements from the splat after executing the arguments and block if no other arguments follow the splat" do def self.m(*args, &block) [args, block] end args = [1, nil] m(*args, &args.pop).should == [[1], nil] args = [1, nil] order = [] m(*(order << :args; args), &(order << :block; args.pop)).should == [[1], nil] order.should == [:args, :block] end end ruby_version_is "3.0" do it "expands the Array elements from the splat before applying block argument operations" do def self.m(*args, &block) [args, block] end args = [1, nil] m(*args, &args.pop).should == [[1, nil], nil] args = [1, nil] order = [] m(*(order << :args; args), &(order << :block; args.pop)).should == [[1, nil], nil] order.should == [:args, :block] end end it "evaluates the splatted arguments before the block if there are other arguments after the splat" do def self.m(*args, &block) [args, block] end args = [1, nil] m(*args, 2, &args.pop).should == [[1, nil, 2], nil] end it "expands an array to arguments grouped in parentheses" do specs.destructure2([40,2]).should == 42 end it "expands an array to arguments grouped in parentheses and ignores any rest arguments in the array" do specs.destructure2([40,2,84]).should == 42 end it "expands an array to arguments grouped in parentheses and sets not specified arguments to nil" do specs.destructure2b([42]).should == [42, nil] end it "expands an array to arguments grouped in parentheses which in turn takes rest arguments" do specs.destructure4r([1, 2, 3]).should == [1, 2, [], 3, nil] specs.destructure4r([1, 2, 3, 4]).should == [1, 2, [], 3, 4] specs.destructure4r([1, 2, 3, 4, 5]).should == [1, 2, [3], 4, 5] end it "with optional argument(s), expands an array to arguments grouped in parentheses" do specs.destructure4o(1, [2, 3]).should == [1, 1, nil, [2, 3]] specs.destructure4o(1, [], 2).should == [1, nil, nil, 2] specs.destructure4os(1, [2, 3]).should == [1, 2, [3]] specs.destructure5o(1, [2, 3]).should == [1, 2, 1, nil, [2, 3]] specs.destructure7o(1, [2, 3]).should == [1, 2, 1, nil, 2, 3] specs.destructure7b(1, [2, 3]) do |(a,*b,c)| [a, c] end.should == [1, 3] end describe "new-style hash arguments" do describe "as the only parameter" do it "passes without curly braces" do specs.fooM1(rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end it "passes without curly braces or parens" do (specs.fooM1 rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end it "handles a hanging comma without curly braces" do specs.fooM1(abc: 123,).should == [{abc: 123}] specs.fooM1(rbx: 'cool', specs: :fail_sometimes, non_sym: 1234,).should == [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end end describe "as the last parameter" do it "passes without curly braces" do specs.fooM3('abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end it "passes without curly braces or parens" do (specs.fooM3 'abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end it "handles a hanging comma without curly braces" do specs.fooM3('abc', 123, abc: 123,).should == ['abc', 123, {abc: 123}] specs.fooM3('abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234,).should == ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end end end describe "mixed new- and old-style hash arguments" do describe "as the only parameter" do it "passes without curly braces" do specs.fooM1(rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end it "passes without curly braces or parens" do (specs.fooM1 rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end it "handles a hanging comma without curly braces" do specs.fooM1(rbx: 'cool', specs: :fail_sometimes, non_sym: 1234,).should == [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end end describe "as the last parameter" do it "passes without curly braces" do specs.fooM3('abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end it "passes without curly braces or parens" do (specs.fooM3 'abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end it "handles a hanging comma without curly braces" do specs.fooM3('abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234,).should == ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] end end end end describe "allows []= with arguments after splat" do before :each do @obj = LangSendSpecs::Attr19Set.new @ary = ["a"] end it "with *args in the [] and post args" do @obj[1,*@ary,123] = 2 @obj.result.should == [1, "a", 123, 2] end end 'rem' style='width: 0.0%;'/>
diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index a04f57faa8..0000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,104 +0,0 @@ ---- -version: '{build}' -init: - - git config --global user.name git - - git config --global user.email svn-admin@ruby-lang.org - - git config --global core.autocrlf false - - git config --global core.eol lf - - git config --global advice.detachedHead 0 -shallow_clone: true -clone_depth: 10 -platform: - - x64 -skip_commits: - message: /^\[DOC\]/ - files: - - doc/* - - '**/*.md' - - '**/*.rdoc' -environment: - ruby_version: "24-%Platform%" - zlib_version: "1.2.12" - matrix: - - build: vs - vs: 120 - ssl: OpenSSL - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2013 - GEMS_FOR_TEST: "" - - build: vs - vs: 140 - ssl: OpenSSL-v111 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 - GEMS_FOR_TEST: "" - RELINE_TEST_ENCODING: "UTF-8" -for: -- - matrix: - only: - - build: vs - install: - - ver - - chcp - - SET BITS=%Platform:x86=32% - - SET BITS=%BITS:x=% - - SET OPENSSL_DIR=C:\%ssl%-Win%BITS% - - CALL SET vcvars=%%^VS%VS%COMNTOOLS^%%..\..\VC\vcvarsall.bat - - SET vcvars - - '"%vcvars%" %Platform:x64=amd64%' - - SET ruby_path=C:\Ruby%ruby_version:-x86=% - - SET PATH=\usr\local\bin;%ruby_path%\bin;%PATH%;C:\msys64\mingw64\bin;C:\msys64\usr\bin - - ruby --version - - 'cl' - - echo> Makefile srcdir=. - - echo>> Makefile MSC_VER=0 - - echo>> Makefile RT=none - - echo>> Makefile RT_VER=0 - - echo>> Makefile BUILTIN_ENCOBJS=nul - - type win32\Makefile.sub >> Makefile - - nmake %mflags% up VCSUP="echo Update OK" - - nmake %mflags% extract-extlibs - - del Makefile - - mkdir \usr\local\bin - - mkdir \usr\local\include - - mkdir \usr\local\lib - - SET ZLIB_ZIP=.downloaded-cache\zlib%zlib_version:.=%.zip - - if not exist %ZLIB_ZIP% curl -fsSL -o %ZLIB_ZIP% --retry 10 https://zlib.net/zlib%zlib_version:.=%.zip - - 7z x -aos -o%APPVEYOR_BUILD_FOLDER%\ext\zlib %ZLIB_ZIP% - - for %%I in (%OPENSSL_DIR%\*.dll) do mklink /h \usr\local\bin\%%~nxI %%I - - attrib +r /s /d - - mkdir %Platform%-mswin_%vs% - build_script: - - cd %APPVEYOR_BUILD_FOLDER% - - cd %Platform%-mswin_%vs% - - ..\win32\configure.bat --without-ext=+,dbm,gdbm,readline --with-opt-dir=/usr/local --with-openssl-dir=%OPENSSL_DIR:\=/% - - nmake -l - - nmake install-nodoc - - \usr\bin\ruby -v -e "p :locale => Encoding.find('locale'), :filesystem => Encoding.find('filesystem')" - - if not "%GEMS_FOR_TEST%" == "" \usr\bin\gem install --no-document %GEMS_FOR_TEST% - - \usr\bin\ruby -ropenssl -e "puts 'Build ' + OpenSSL::OPENSSL_VERSION, 'Runtime ' + OpenSSL::OPENSSL_LIBRARY_VERSION" - test_script: - - set /a JOBS=%NUMBER_OF_PROCESSORS% - - nmake -l "TESTOPTS=-v -q" btest - - nmake -l "TESTOPTS=-v -q" test-basic - - nmake -l "TESTOPTS=-v --timeout-scale=3.0 --excludes=../test/excludes/_appveyor -j%JOBS% --exclude readline --exclude win32ole --exclude test_bignum --exclude test_syntax --exclude test_open-uri --exclude test_bundled_ca" test-all - # separately execute tests without -j which may crash worker with -j. - - nmake -l "TESTOPTS=-v --timeout-scale=3.0 --excludes=../test/excludes/_appveyor" test-all TESTS="../test/win32ole ../test/ruby/test_bignum.rb ../test/ruby/test_syntax.rb ../test/open-uri/test_open-uri.rb ../test/rubygems/test_bundled_ca.rb" - - nmake -l test-spec MSPECOPT=-fs # not using `-j` because sometimes `mspec -j` silently dies on Windows -notifications: - - provider: Webhook - method: POST - url: - secure: CcFlJNDJ/a6to7u3Z4Fnz6dScEPNx7hTha2GkSRlV+1U6dqmxY/7uBcLXYb9gR3jfQk6w+2o/HrjNAyXMNGU/JOka3s2WRI4VKitzM+lQ08owvJIh0R7LxrGH0J2e81U # ruby-lang slack: ruby/simpler-alerts-bot - body: >- - {{^isPullRequest}} - { - "ci": "AppVeyor CI", - "env": "Visual Studio 2013 / 2015", - "url": "{{buildUrl}}", - "commit": "{{commitId}}", - "branch": "{{branch}}" - } - {{/isPullRequest}} - on_build_success: false - on_build_failure: true - on_build_status_changed: false diff --git a/.cirrus.yml b/.cirrus.yml deleted file mode 100644 index c8fb326c89..0000000000 --- a/.cirrus.yml +++ /dev/null @@ -1,64 +0,0 @@ -# This CI is used to test Arm cases. We can set the maximum 16 tasks. -# The entire testing design is inspired from .github/workflows/compilers.yml. - -# By default, Cirrus mounts an empty volume to `/tmp` -# which triggers all sorts of warnings like "system temporary path is world-writable: /tmp". -# Lets workaround it by specifying a custom volume mount point. -env: - CIRRUS_VOLUME: /cirrus-ci-volume - LANG: C.UTF-8 - -task: - name: Arm64 Graviton2 / $CC - skip: "changesIncludeOnly('doc/**', '**.{md,rdoc}')" - arm_container: - # We use the arm64 images at http://ghcr.io/ruby/ruby-ci-image . - image: ghcr.io/ruby/ruby-ci-image:$CC - # Define the used cpu core in each matrix task. We can use total 16 cpu - # cores in entire matrix. [cpu] = [total cpu: 16] / [number of tasks] - cpu: 8 - # We can request maximum 4 GB per cpu. - # [memory per task] = [memory per cpu: 4 GB] * [cpu] - memory: 32G - env: - CIRRUS_CLONE_DEPTH: 50 - optflags: '-O1' - debugflags: '-ggdb3' - RUBY_PREFIX: /tmp/ruby-prefix - RUBY_DEBUG: ci rgengc - RUBY_TESTOPTS: >- - -q - --color=always - --tty=no - matrix: - CC: clang-12 - CC: gcc-11 - id_script: id - set_env_script: - # Set `GNUMAKEFLAGS`, because the flags are GNU make specific. Note using - # the `make` environment variable used in compilers.yml causes some rubygems - # tests to fail. - # https://github.com/rubygems/rubygems/issues/4921 - - echo "GNUMAKEFLAGS=-s -j$((1 + $CIRRUS_CPU))" >> $CIRRUS_ENV - print_env_script: - - echo "GNUMAKEFLAGS=$GNUMAKEFLAGS" - # Arm containers are executed in AWS's EKS, and it's not yet supporting IPv6 - # See https://github.com/aws/containers-roadmap/issues/835 - disable_ipv6_script: sudo ./tool/disable_ipv6.sh - autogen_script: ./autogen.sh - configure_script: >- - ./configure -C - --enable-debug-env - --disable-install-doc - --with-ext=-test-/cxxanyargs,+ - --prefix="$RUBY_PREFIX" - make_extract-extlibs_script: make extract-extlibs - make_incs_script: make incs - make_script: make - make_leaked-globals_script: make leaked-globals - make_test_script: make test - make_install_script: make install - install_gems_for_test_script: $RUBY_PREFIX/bin/gem install --no-doc timezone tzinfo - make_test-tool_script: make test-tool - make_test-all_script: make test-all - make_test-spec_script: make test-spec @@ -15,16 +15,26 @@ array.rb ast.rb dir.rb gc.rb +hash.rb io.rb kernel.rb marshal.rb numeric.rb nilclass.rb pack.rb +pathname_builtin.rb ractor.rb +string.rb +symbol.rb timev.rb +thread_sync.rb trace_point.rb warning.rb +yjit.rb +zjit.rb + +# Errno::* +known_errors.inc # the lib/ directory (which has its own .document file) lib @@ -40,11 +50,7 @@ README.ja.md COPYING COPYING.ja -CONTRIBUTING.md LEGAL -# win32/README.win32 linked from README.md -win32 - doc @@ -1,23 +1,7 @@ -set startup-with-shell off - -define hook-run - set $color_type = 0 - set $color_highlite = 0 - set $color_end = 0 -end - define ruby_gdb_init - if !$color_type - set $color_type = "\033[31m" - end - if !$color_highlite - set $color_highlite = "\033[36m" - end - if !$color_end - set $color_end = "\033[m" - end - if ruby_dummy_gdb_enums.special_consts - end + init-if-undefined $color_type = "\033[31m" + init-if-undefined $color_highlite = "\033[36m" + init-if-undefined $color_end = "\033[m" end # set prompt \033[36m(gdb)\033[m\040 @@ -67,7 +51,7 @@ define rp printf "%sT_OBJECT%s: ", $color_type, $color_end print ((struct RObject *)($arg0))->basic if ($flags & ROBJECT_EMBED) - print/x *((VALUE*)((struct RObject*)($arg0))->as.ary) @ (ROBJECT_EMBED_LEN_MAX+0) + print/x *((VALUE*)((struct RObject*)($arg0))->as.ary) @ (RSHAPE_CAPACITY(rb_obj_shape_id($arg0))) else print (((struct RObject *)($arg0))->as.heap) if (((struct RObject*)($arg0))->as.heap.numiv) > 0 @@ -99,13 +83,11 @@ define rp set $regsrc = ((struct RRegexp*)($arg0))->src set $rsflags = ((struct RBasic*)$regsrc)->flags printf "%sT_REGEXP%s: ", $color_type, $color_end - set $len = ($rsflags & RUBY_FL_USER1) ? \ - ((struct RString*)$regsrc)->as.heap.len : \ - (($rsflags & (RUBY_FL_USER2|RUBY_FL_USER3|RUBY_FL_USER4|RUBY_FL_USER5|RUBY_FL_USER6)) >> RUBY_FL_USHIFT+2) + set $len = ((struct RString*)($arg0))->len set print address off output *(char *)(($rsflags & RUBY_FL_USER1) ? \ - ((struct RString*)$regsrc)->as.heap.ptr : \ - ((struct RString*)$regsrc)->as.ary) @ $len + ((struct RString*)$regsrc)->as.heap.ptr : \ + ((struct RString*)$regsrc)->as.embed.ary) @ $len set print address on printf " len:%ld ", $len if $flags & RUBY_FL_USER6 @@ -126,26 +108,26 @@ define rp printf "%sT_ARRAY%s: len=%ld ", $color_type, $color_end, $len printf "(embed) " if ($len == 0) - printf "{(empty)} " + printf "{(empty)} " else - print/x *((VALUE*)((struct RArray*)($arg0))->as.ary) @ $len - printf " " + print/x *((VALUE*)((struct RArray*)($arg0))->as.ary) @ $len + printf " " end else set $len = ((struct RArray*)($arg0))->as.heap.len printf "%sT_ARRAY%s: len=%ld ", $color_type, $color_end, $len if ($flags & RUBY_FL_USER2) - printf "(shared) shared=" - output/x ((struct RArray*)($arg0))->as.heap.aux.shared_root - printf " " + printf "(shared) shared=" + output/x ((struct RArray*)($arg0))->as.heap.aux.shared_root + printf " " else - printf "(ownership) capa=%ld ", ((struct RArray*)($arg0))->as.heap.aux.capa + printf "(ownership) capa=%ld ", ((struct RArray*)($arg0))->as.heap.aux.capa end if ($len == 0) - printf "{(empty)} " + printf "{(empty)} " else - print/x *((VALUE*)((struct RArray*)($arg0))->as.heap.ptr) @ $len - printf " " + print/x *((VALUE*)((struct RArray*)($arg0))->as.heap.ptr) @ $len + printf " " end end print (struct RArray *)($arg0) @@ -157,13 +139,15 @@ define rp if ($flags & RUBY_T_MASK) == RUBY_T_HASH printf "%sT_HASH%s: ", $color_type, $color_end, if (((struct RHash *)($arg0))->basic.flags & RHASH_ST_TABLE_FLAG) - printf "st len=%ld ", ((struct RHash *)($arg0))->as.st->num_entries + set $st = (struct st_table *)((uintptr_t)($arg0) + sizeof(struct RHash)) + printf "st len=%ld ", $st->num_entries + print $st else printf "li len=%ld bound=%ld ", \ ((((struct RHash *)($arg0))->basic.flags & RHASH_AR_TABLE_SIZE_MASK) >> RHASH_AR_TABLE_SIZE_SHIFT), \ ((((struct RHash *)($arg0))->basic.flags & RHASH_AR_TABLE_BOUND_MASK) >> RHASH_AR_TABLE_BOUND_SHIFT) + print (struct ar_table_struct *)((uintptr_t)($arg0) + sizeof(struct RHash)) end - print (struct RHash *)($arg0) else if ($flags & RUBY_T_MASK) == RUBY_T_STRUCT set $len = (($flags & (RUBY_FL_USER1|RUBY_FL_USER2)) ? \ @@ -201,12 +185,19 @@ define rp print (struct RBasic *)($arg0) else if ($flags & RUBY_T_MASK) == RUBY_T_DATA - if ((struct RTypedData *)($arg0))->typed_flag == 1 - printf "%sT_DATA%s(%s): ", $color_type, $color_end, ((struct RTypedData *)($arg0))->type->wrap_struct_name - print (struct RTypedData *)($arg0) + if ($flags & RUBY_TYPED_FL_IS_TYPED_DATA) + set $data = (struct RTypedData *)($arg0) + set $type = (const rb_data_type_t *)($data->type & ~1) + printf "%sT_DATA%s(%s): ", $color_type, $color_end, $type->wrap_struct_name + print *$type + if ($data->type & 1) + print (void *)&$data->data + else + print $data + end else printf "%sT_DATA%s: ", $color_type, $color_end - print (struct RData *)($arg0) + print *(struct RData *)($arg0) end else if ($flags & RUBY_T_MASK) == RUBY_T_MATCH @@ -440,13 +431,11 @@ end define output_string set $flags = ((struct RBasic*)($arg0))->flags - set $len = ($flags & RUBY_FL_USER1) ? \ - ((struct RString*)($arg0))->as.heap.len : \ - (($flags & (RUBY_FL_USER2|RUBY_FL_USER3|RUBY_FL_USER4|RUBY_FL_USER5|RUBY_FL_USER6)) >> RUBY_FL_USHIFT+2) + set $len = ((struct RString*)($arg0))->len if $len > 0 output *(char *)(($flags & RUBY_FL_USER1) ? \ - ((struct RString*)($arg0))->as.heap.ptr : \ - ((struct RString*)($arg0))->as.ary) @ $len + ((struct RString*)($arg0))->as.heap.ptr : \ + ((struct RString*)($arg0))->as.embed.ary) @ $len else output "" end @@ -454,13 +443,11 @@ end define print_string set $flags = ((struct RBasic*)($arg0))->flags - set $len = ($flags & RUBY_FL_USER1) ? \ - ((struct RString*)($arg0))->as.heap.len : \ - (($flags & (RUBY_FL_USER2|RUBY_FL_USER3|RUBY_FL_USER4|RUBY_FL_USER5|RUBY_FL_USER6)) >> RUBY_FL_USHIFT+2) + set $len = ((struct RString*)($arg0))->len if $len > 0 printf "%s", *(char *)(($flags & RUBY_FL_USER1) ? \ - ((struct RString*)($arg0))->as.heap.ptr : \ - ((struct RString*)($arg0))->as.ary) @ $len + ((struct RString*)($arg0))->as.heap.ptr : \ + ((struct RString*)($arg0))->as.embed.ary) @ $len end end @@ -543,14 +530,14 @@ document rp_bignum end define rp_class + set $class_and_classext = (struct RClass_and_rb_classext_t *)($arg0) printf "(struct RClass *) %p", (void*)$arg0 - if ((struct RClass *)($arg0))->ptr.origin_ != $arg0 - printf " -> %p", ((struct RClass *)($arg0))->ptr.origin_ + if $class_and_classext->classext->origin_ != (VALUE)$arg0 + printf " -> %p", $class_and_classext->classext->origin_ end printf "\n" rb_classname $arg0 - print/x *(struct RClass *)($arg0) - print *((struct RClass *)($arg0))->ptr + print/x *$class_and_classext end document rp_class Print the content of a Class/Module. @@ -689,11 +676,6 @@ define nd_stts end -define nd_entry - printf "%su3.entry%s: ", $color_highlite, $color_end - p ($arg0).u3.entry -end - define nd_vid printf "%su1.id%s: ", $color_highlite, $color_end p ($arg0).u1.id @@ -868,22 +850,22 @@ define rb_numtable_entry set $rb_numtable_p = $rb_numtable_tbl->as.packed.bins while $rb_numtable_p && $rb_numtable_p < $rb_numtable_tbl->as.packed.bins+$rb_numtable_tbl->num_entries if $rb_numtable_p.k == $rb_numtable_id - set $rb_numtable_key = $rb_numtable_p.k - set $rb_numtable_rec = $rb_numtable_p.v - set $rb_numtable_p = 0 + set $rb_numtable_key = $rb_numtable_p.k + set $rb_numtable_rec = $rb_numtable_p.v + set $rb_numtable_p = 0 else - set $rb_numtable_p = $rb_numtable_p + 1 + set $rb_numtable_p = $rb_numtable_p + 1 end end else set $rb_numtable_p = $rb_numtable_tbl->as.big.bins[st_numhash($rb_numtable_id) % $rb_numtable_tbl->num_bins] while $rb_numtable_p if $rb_numtable_p->key == $rb_numtable_id - set $rb_numtable_key = $rb_numtable_p->key - set $rb_numtable_rec = $rb_numtable_p->record - set $rb_numtable_p = 0 + set $rb_numtable_key = $rb_numtable_p->key + set $rb_numtable_rec = $rb_numtable_p->record + set $rb_numtable_p = 0 else - set $rb_numtable_p = $rb_numtable_p->next + set $rb_numtable_p = $rb_numtable_p->next end end end @@ -921,10 +903,10 @@ document rb_method_entry end define rb_classname - # up to 128bit int - set $rb_classname = rb_mod_name($arg0) - if $rb_classname != RUBY_Qnil - rp $rb_classname + set $rb_classname = ((struct RClass_and_rb_classext_t*)$arg0)->classext->classpath + if $rb_classname != RUBY_Qfalse + print_string $rb_classname + printf "\n" else echo anonymous class/module\n end @@ -961,7 +943,7 @@ define iseq set $operand_size = ((INSN*)($arg0))->operand_size set $operands = ((INSN*)($arg0))->operands while $i < $operand_size - rp $operands[$i++] + rp $operands[$i++] end end end @@ -979,8 +961,8 @@ end define rb_ps_vm print $ps_vm = (rb_vm_t*)$arg0 - set $ps_thread_ln = $ps_vm->living_threads.n.next - set $ps_thread_ln_last = $ps_vm->living_threads.n.prev + set $ps_thread_ln = $ps_vm->ractor.main_ractor.threads.set.n.next + set $ps_thread_ln_last = $ps_vm->ractor.main_ractor.threads.set.n.prev while 1 set $ps_thread_th = (rb_thread_t *)$ps_thread_ln set $ps_thread = (VALUE)($ps_thread_th->self) @@ -1131,7 +1113,7 @@ define rb_ps_thread set $ps_thread = (struct RTypedData*)$arg0 set $ps_thread_th = (rb_thread_t*)$ps_thread->data printf "* #<Thread:%p rb_thread_t:%p native_thread:%p>\n", \ - $ps_thread, $ps_thread_th, $ps_thread_th->thread_id + $ps_thread, $ps_thread_th, $ps_thread_th->nt set $cfp = $ps_thread_th->ec->cfp set $cfpend = (rb_control_frame_t *)($ps_thread_th->ec->vm_stack + $ps_thread_th->ec->vm_stack_size)-1 while $cfp < $cfpend @@ -1279,9 +1261,9 @@ document rb_count_objects Counts all objects grouped by type. end -# Details: https://bugs.ruby-lang.org/projects/ruby-master/wiki/MachineInstructionsTraceWithGDB +# Details: https://github.com/ruby/ruby/wiki/Machine-Instructions-Trace-with-GDB define trace_machine_instructions - set logging on + set logging enabled set height 0 set width 0 display/i $pc @@ -1316,13 +1298,12 @@ define dump_node set $flags = ((struct RBasic*)($str))->flags printf "%s", (char *)(($flags & RUBY_FL_USER1) ? \ ((struct RString*)$str)->as.heap.ptr : \ - ((struct RString*)$str)->as.ary) + ((struct RString*)$str)->as.embed.ary) end define print_flags printf "RUBY_FL_WB_PROTECTED: %s\n", ((struct RBasic*)($arg0))->flags & RUBY_FL_WB_PROTECTED ? "1" : "0" - printf "RUBY_FL_PROMOTED0 : %s\n", ((struct RBasic*)($arg0))->flags & RUBY_FL_PROMOTED0 ? "1" : "0" - printf "RUBY_FL_PROMOTED1 : %s\n", ((struct RBasic*)($arg0))->flags & RUBY_FL_PROMOTED1 ? "1" : "0" + printf "RUBY_FL_PROMOTED : %s\n", ((struct RBasic*)($arg0))->flags & RUBY_FL_PROMOTED ? "1" : "0" printf "RUBY_FL_FINALIZE : %s\n", ((struct RBasic*)($arg0))->flags & RUBY_FL_FINALIZE ? "1" : "0" printf "RUBY_FL_SHAREABLE : %s\n", ((struct RBasic*)($arg0))->flags & RUBY_FL_SHAREABLE ? "1" : "0" printf "RUBY_FL_EXIVAR : %s\n", ((struct RBasic*)($arg0))->flags & RUBY_FL_EXIVAR ? "1" : "0" @@ -1348,3 +1329,8 @@ define print_flags printf "RUBY_FL_USER17 : %s\n", ((struct RBasic*)($arg0))->flags & RUBY_FL_USER17 ? "1" : "0" printf "RUBY_FL_USER18 : %s\n", ((struct RBasic*)($arg0))->flags & RUBY_FL_USER18 ? "1" : "0" end + +source -s misc/gdb.py + +# Moved from beginning, since it fails on older gdbs +set startup-with-shell off diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..d98646febf --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,41 @@ +# This is a file used by GitHub to ignore the following commits on `git blame`. +# +# You can also do the same thing in your local repository with: +# $ git config --local blame.ignoreRevsFile .git-blame-ignore-revs + +# Expand tabs +5b21e94bebed90180d8ff63dad03b8b948361089 +c5e9af9c9d890578182a21e7b71b50334cd5579e +e63a2115f64433b21cb5dd67c5bf8b30f87ef293 +712ac99e4d0384a941c80a9f48f62943ba7d97c0 +d1474affa8e105bece209cc9d594bb0a989859e1 +2da92388b948821269b18d6b178a680f17e41750 +5062c0c621d887367af8a054e5e5d83d7ec57dd3 + +# Enable Style/StringLiterals cop for RubyGems/Bundler +d7ffd3fea402239b16833cc434404a7af82d44f3 + +# [ruby/digest] Revert tab-expansion in external files +48b09aae7ec5632209229dcc294dd0d75a93a17f +8a65cf3b61c60e4cb886f59a73ff6db44364bfa9 +39dc9f9093901d40d2998653948d5da38b18ee2c + +# [ruby/io-nonblock] Revert tab expansion +f28287d34c03f472ffe90ea262bdde9affd4b965 +0d842fecb4f75ab3b1d4097ebdb8e88f51558041 +4ba2c66761d6a293abdfba409241d31063cefd62 + +# Make benchmark indentation consistent +fc4acf8cae82e5196186d3278d831f2438479d91 + +# Make prism_compile.c indentation consistent +40b2c8e5e7e6e5f83cee9276dc9c1922a69292d6 +d2c5867357ed88eccc28c2b3bd4a46e206e7ff85 + +# Miss-and-revived commits +a0f7de814ae5c299d6ce99bed5fb308a05d50ba0 +d4e24021d39e1f80f0055b55d91f8d5f22e15084 +7a56c316418980b8a41fcbdc94067b2bda2ad112 +e90282be7ba1bc8e3119f6e1a2c80356ceb3f80a +26a9e0b4e31f7b5a9cbd755e0a15823a8fa51bae +2f53985da9ee593fe524d408256835667938c7d7 diff --git a/.gitattributes b/.gitattributes index d0c2d266b4..6ac6e6fcc3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ *.gemspec diff=ruby *.rb diff=ruby +*.inc.rs linguist-generated=true bin svn-properties=svn:ignore=ruby bin/* diff=ruby tool/update-deps diff=ruby diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 15abc79af6..0000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,10 +0,0 @@ -# Lines starting with '#' are comments. -# Each line is a file pattern followed by one or more owners. -# Code owners will be automatically tagged as reviewers when a pull request is opened - -# YJIT sources and tests -yjit* @maximecb @xrxr @tenderlove -doc/yjit/* @maximecb @xrxr @tenderlove -bootstraptest/test_yjit* @maximecb @xrxr @tenderlove -test/ruby/test_yjit* @maximecb @xrxr @tenderlove -.github/workflows/yjit* @maximecb @xrxr @tenderlove diff --git a/.github/actions/capiext/action.yml b/.github/actions/capiext/action.yml new file mode 100644 index 0000000000..49562725f4 --- /dev/null +++ b/.github/actions/capiext/action.yml @@ -0,0 +1,86 @@ +name: rubyspec C-API extensions + +inputs: + builddir: + required: false + default: '.' + make: + required: false + default: 'make -s' + +outputs: + key: + value: >- + ${{ + !steps.restore.outputs.cache-hit && + github.ref == 'refs/heads/master' && + steps.config.outputs.key + }} + +runs: + using: composite + + steps: + - id: config + shell: bash + run: | + eval $(grep -e '^arch *=' -e '^ruby_version *=' -e '^DLEXT *=' Makefile | + sed 's/ *= */=/') + case "${ruby_version}" in + *+*) key=capiexts-${arch}-${ruby_version}-${{ hashFiles('src/spec/ruby/optional/capi/ext/*.[ch]') }};; + *) key=;; + esac + echo version=$ruby_version >> $GITHUB_OUTPUT + echo key="$key" >> $GITHUB_OUTPUT + echo DLEXT=$DLEXT >> $GITHUB_OUTPUT + working-directory: ${{ inputs.builddir }} + + - name: Restore previous CAPI extensions + uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + id: cache + with: + path: ${{ inputs.builddir }}/spec/ruby/optional/capi/ext/ + key: ${{ steps.config.outputs.key }} + if: ${{ steps.config.outputs.key }} + + - name: Run test-spec with previous CAPI extension binaries + id: check + shell: bash + run: | + touch spec/ruby/optional/capi/ext/*.$DLEXT + [ ! -f spec/ruby/optional/capi/ext/\*.$DLEXT ] + ${{ inputs.make }} SPECOPTS=optional/capi test-spec + env: + DLEXT: ${{ steps.config.outputs.DLEXT }} + working-directory: ${{ inputs.builddir }} + if: ${{ steps.cache.outputs.cache-hit }} + + - name: Strip CAPI extensions + id: strip + shell: bash + run: | + rm -f spec/ruby/optional/capi/ext/*.c + [ "$DLEXT" = bundle ] || # separated to .dSYM directories + strip spec/ruby/optional/capi/ext/*.$DLEXT + env: + DLEXT: ${{ steps.config.outputs.DLEXT }} + working-directory: ${{ inputs.builddir }} + if: >- + ${{true + && ! steps.cache.outputs.cache-hit + && github.ref_name == 'master' + }} + + - name: Save CAPI extensions + uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: ${{ inputs.builddir }}/spec/ruby/optional/capi/ext/ + key: ${{ steps.config.outputs.key }} + if: ${{ steps.strip.outcome == 'success' }} + + - shell: bash + run: | + echo "::error::Change from ${prev} detected; bump up ABI version" + env: + prev: ${{ steps.config.outputs.version }} + if: ${{ always() && steps.check.outcome == 'failure' }} diff --git a/.github/actions/compilers/action.yml b/.github/actions/compilers/action.yml new file mode 100644 index 0000000000..ab5b56a889 --- /dev/null +++ b/.github/actions/compilers/action.yml @@ -0,0 +1,133 @@ +name: Compiles ruby in a container +description: >- + Makes ruby using a dedicated container + +inputs: + tag: + required: false + default: clang-20 + description: >- + container image tag to use in this run. + + with_gcc: + required: false + description: >- + override compiler path & flags. + + CFLAGS: + required: false + description: >- + C compiler flags to override. + + CXXFLAGS: + required: false + description: >- + C++ compiler flags to override. + + optflags: + required: false + # -O1 is faster than -O3 in our tests... Majority of time are consumed trying + # to optimize binaries. Also GitHub Actions run on relatively modern CPUs + # compared to, say, GCC 4 or Clang 3. We don't specify `-march=native` + # because compilers tend not understand what the CPU is. + default: '-O1' + description: >- + Compiler flags for optimisations. + + cppflags: + required: false + description: >- + Additional preprocessor flags. + + append_configure: + required: false + default: >- + --without-valgrind + --without-jemalloc + --without-gmp + description: >- + flags to append to configure. + + enable_shared: + required: false + default: true + description: >- + Whether to build libruby.so. + + check: + required: false + default: '' + description: >- + Whether to run `make check` + + test_all: + required: false + default: '' + description: >- + Whether to run `make test-all` with options for test-all. + + test_spec: + required: false + default: '' + description: >- + Whether to run `make test-spec` with options for mspec. + + static_exts: + required: false + description: >- + whitespace separated list of extensions that need be linked statically. + +runs: + using: composite + steps: + - shell: bash + run: docker pull --quiet 'ghcr.io/ruby/ruby-ci-image:${{ inputs.tag }}' + + - name: Enable Launchable conditionally + id: enable-launchable + run: echo "enable-launchable=true" >> $GITHUB_OUTPUT + shell: bash + if: >- + ${{ + github.repository == 'ruby/ruby' || + (github.repository != 'ruby/ruby' && env.LAUNCHABLE_TOKEN) + }} + + - name: compile + shell: bash + run: >- + docker run + --rm + --user=root + --volume '${{ github.workspace }}:/github/workspace:ro' + --workdir=/github/workspace + --entrypoint=/github/workspace/.github/actions/compilers/entrypoint.sh + --env CI + --env GITHUB_ACTION + --env INPUT_WITH_GCC='${{ inputs.with_gcc || inputs.tag }}' + --env INPUT_CFLAGS='${{ inputs.CFLAGS }}' + --env INPUT_CXXFLAGS='${{ inputs.CXXFLAGS }}' + --env INPUT_OPTFLAGS='${{ inputs.OPTFLAGS }}' + --env INPUT_CPPFLAGS='${{ inputs.cppflags }}' + --env INPUT_APPEND_CONFIGURE='${{ inputs.append_configure }}' + --env INPUT_CHECK='${{ inputs.check }}' + --env INPUT_TEST_ALL='${{ inputs.test_all }}' + --env INPUT_TEST_SPEC='${{ inputs.test_spec }}' + --env INPUT_ENABLE_SHARED='${{ inputs.enable_shared }}' + --env INPUT_STATIC_EXTS='${{ inputs.static_exts }}' + --env LAUNCHABLE_ORGANIZATION='${{ github.repository_owner }}' + --env LAUNCHABLE_WORKSPACE='${{ github.event.repository.name }}' + --env LAUNCHABLE_ENABLED='${{ steps.enable-launchable.outputs.enable-launchable || false }}' + --env GITHUB_PR_HEAD_SHA='${{ github.event.pull_request.head.sha || github.sha }}' + --env GITHUB_PULL_REQUEST_URL='${{ github.event.pull_request.html_url }}' + --env GITHUB_REF='${{ github.ref }}' + --env GITHUB_ACTIONS + --env GITHUB_RUN_ID + --env GITHUB_REPOSITORY + --env GITHUB_WORKFLOW + --env GITHUB_RUN_NUMBER + --env GITHUB_EVENT_NAME + --env GITHUB_SHA + --env GITHUB_HEAD_REF + --env GITHUB_SERVER_URL + 'ghcr.io/ruby/ruby-ci-image:${{ inputs.tag }}' diff --git a/.github/actions/compilers/entrypoint.sh b/.github/actions/compilers/entrypoint.sh new file mode 100755 index 0000000000..b554151091 --- /dev/null +++ b/.github/actions/compilers/entrypoint.sh @@ -0,0 +1,90 @@ +#! /bin/bash + +# Copyright (c) 2024 Ruby developers. All rights reserved. +# +# This file is a part of the programming language Ruby. Permission is hereby +# granted, to either redistribute and/or modify this file, provided that the +# conditions mentioned in the file COPYING are met. Consult the file for +# details. + +grouped() +{ + echo "::group::${@}" + "${@}" + echo "::endgroup::" +} + +set -e +set -u +set -o pipefail + +srcdir="/github/workspace/src" +builddir="$(mktemp -dt)" + +export GITHUB_WORKFLOW='Compilations' +export CONFIGURE_TTY='never' +export RUBY_DEBUG='ci rgengc' +export RUBY_TESTOPTS='-q --color=always --tty=no' +export RUBY_DEBUG_COUNTER_DISABLE='1' +export GNUMAKEFLAGS="-j$((1 + $(nproc)))" + +case "x${INPUT_ENABLE_SHARED}" in +x | xno | xfalse ) + enable_shared='--disable-shared' + ;; +*) + enable_shared='--enable-shared' + ;; +esac + +pushd ${builddir} + +grouped git config --global --add safe.directory ${srcdir} + +grouped ${srcdir}/configure \ + -C \ + --with-gcc="${INPUT_WITH_GCC}" \ + --enable-debug-env \ + --disable-install-doc \ + --with-ext=-test-/cxxanyargs,+ \ + --without-git \ + ${enable_shared} \ + ${INPUT_APPEND_CONFIGURE} \ + CFLAGS="${INPUT_CFLAGS}" \ + CXXFLAGS="${INPUT_CXXFLAGS}" \ + optflags="${INPUT_OPTFLAGS}" \ + cppflags="${INPUT_CPPFLAGS}" \ + debugflags='-ggdb3' # -g0 disables backtraces when SEGV. Do not set that. + +popd + +if [[ -n "${INPUT_STATIC_EXTS}" ]]; then + echo "::group::ext/Setup" + set -x + mkdir ${builddir}/ext + ( + for ext in ${INPUT_STATIC_EXTS}; do + echo "${ext}" + done + ) >> ${builddir}/ext/Setup + set +x + echo "::endgroup::" +fi + +if [ -n "$INPUT_TEST_ALL" ]; then + tests=" -- $INPUT_TEST_ALL" +else + tests=" -- ruby -ext-" +fi + +pushd ${builddir} + +grouped make showflags +grouped make all +# grouped make install + +# Run only `make test` by default. Run other tests if specified. +grouped make test +if [[ -n "$INPUT_CHECK" ]]; then grouped make test-tool; fi +if [[ -n "$INPUT_CHECK" || -n "$INPUT_TEST_ALL" ]]; then grouped make test-all TESTS="$tests"; fi +if [[ -n "$INPUT_CHECK" || -n "$INPUT_TEST_SPEC" ]]; then grouped env CHECK_LEAKS=true make test-spec MSPECOPT="$INPUT_TEST_SPEC"; fi diff --git a/.github/actions/launchable/setup/action.yml b/.github/actions/launchable/setup/action.yml new file mode 100644 index 0000000000..16af8fc3fd --- /dev/null +++ b/.github/actions/launchable/setup/action.yml @@ -0,0 +1,313 @@ +name: Set up Launchable +description: >- + Install the required dependencies and execute the necessary Launchable commands for test recording + +inputs: + os: + required: true + description: The operating system that CI runs on. This value is used in Launchable flavor. + + test-opts: + default: none + required: false + description: >- + Test options that determine how tests are run. + This value is used in the Launchable flavor. + + launchable-token: + required: false + description: >- + Launchable token is needed if you want to run Launchable on your forked repository. + See https://github.com/ruby/ruby/wiki/CI-Servers#launchable-ci for details. + + builddir: + required: false + default: ${{ github.workspace }} + description: >- + Directory to create Launchable report file. + + srcdir: + required: false + default: ${{ github.workspace }} + description: >- + Directory to (re-)checkout source codes. Launchable retrieves the commit information + from the directory. + + test-task: + required: false + default: ${{ matrix.test_task }} + description: >- + Specifies a single test task to be executed. + This value is used in the Launchable flavor. + Either 'test-task' or 'multi-test-tasks' must be configured. + + test-tasks: + required: false + default: '[]' + description: >- + Specifies an array of multiple test tasks to be executed. + For example: '["test", "test-all"]'. + If you want to run a single test task, use the 'test-task' input instead. + + is-yjit: + required: false + default: 'false' + description: >- + Whether this workflow is executed on YJIT. + +outputs: + stdout_report_path: + value: ${{ steps.global.outputs.stdout_report_path }} + description: >- + Report file path for standard output. + + stderr_report_path: + value: ${{ steps.global.outputs.stderr_report_path }} + description: >- + Report file path for standard error. + +runs: + using: composite + + steps: + - name: Enable Launchable conditionally + id: enable-launchable + run: echo "enable-launchable=true" >> $GITHUB_OUTPUT + shell: bash + if: >- + ${{ + (github.repository == 'ruby/ruby' + || (github.repository != 'ruby/ruby' + && env.LAUNCHABLE_TOKEN)) + && (inputs.test-task == 'check' + || inputs.test-task == 'test-all' + || inputs.test-task == 'test' + || contains(fromJSON(inputs.test-tasks), 'test-all') + || contains(fromJSON(inputs.test-tasks), 'test')) + }} + + # Launchable CLI requires Python and Java. + # https://www.launchableinc.com/docs/resources/cli-reference/ + - name: Set up Python + uses: actions/setup-python@871daa956ca9ea99f3c3e30acb424b7960676734 # v5.0.0 + with: + python-version: "3.x" + if: >- + ${{ steps.enable-launchable.outputs.enable-launchable + && !endsWith(inputs.os, 'ppc64le') && !endsWith(inputs.os, 's390x') }} + + - name: Set up Java + uses: actions/setup-java@7a445ee88d4e23b52c33fdc7601e40278616c7f8 # v4.0.0 + with: + distribution: 'temurin' + java-version: '17' + if: >- + ${{ steps.enable-launchable.outputs.enable-launchable + && !endsWith(inputs.os, 'ppc64le') && !endsWith(inputs.os, 's390x') }} + + - name: Set up Java ppc64le + uses: actions/setup-java@7a445ee88d4e23b52c33fdc7601e40278616c7f8 # v4.0.0 + with: + distribution: 'semeru' + architecture: 'ppc64le' + java-version: '17' + if: >- + ${{ steps.enable-launchable.outputs.enable-launchable + && endsWith(inputs.os, 'ppc64le') }} + + - name: Set up Java s390x + uses: actions/setup-java@7a445ee88d4e23b52c33fdc7601e40278616c7f8 # v4.0.0 + with: + distribution: 'semeru' + architecture: 's390x' + java-version: '17' + if: >- + ${{ steps.enable-launchable.outputs.enable-launchable + && endsWith(inputs.os, 's390x') }} + + - name: Set global vars + id: global + shell: bash + run: | + test_all_enabled="${{ inputs.test-task == 'check' || inputs.test-task == 'test-all' || contains(fromJSON(inputs.test-tasks), 'test-all') }}" + btest_enabled="${{ inputs.test-task == 'check' || inputs.test-task == 'test' || contains(fromJSON(inputs.test-tasks), 'test') }}" + test_spec_enabled="${{ inputs.test-task == 'check' || inputs.test-task == 'test-spec' || contains(fromJSON(inputs.test-tasks), 'test-spec') }}" + echo test_all_enabled="${test_all_enabled}" >> $GITHUB_OUTPUT + echo btest_enabled="${btest_enabled}" >> $GITHUB_OUTPUT + echo test_spec_enabled="${test_spec_enabled}" >> $GITHUB_OUTPUT + echo test_all_report_file='launchable_test_all_report.json' >> $GITHUB_OUTPUT + echo btest_report_file='launchable_btest_report.json' >> $GITHUB_OUTPUT + echo test_spec_report_dir='launchable_test_spec_report' >> $GITHUB_OUTPUT + echo stdout_report_path="launchable_stdout.log" >> $GITHUB_OUTPUT + echo stderr_report_path="launchable_stderr.log" >> $GITHUB_OUTPUT + if: steps.enable-launchable.outputs.enable-launchable + + - name: Set environment variables for Launchable + shell: bash + run: | + : # GITHUB_PULL_REQUEST_URL are used for commenting test reports in Launchable Github App. + : # https://github.com/launchableinc/cli/blob/v1.80.1/launchable/utils/link.py#L42 + echo "GITHUB_PULL_REQUEST_URL=${{ github.event.pull_request.html_url }}" >> $GITHUB_ENV + : # The following envs are necessary in Launchable tokenless authentication. + : # https://github.com/launchableinc/cli/blob/v1.80.1/launchable/utils/authentication.py#L20 + echo "LAUNCHABLE_ORGANIZATION=${{ github.repository_owner }}" >> $GITHUB_ENV + echo "LAUNCHABLE_WORKSPACE=${{ github.event.repository.name }}" >> $GITHUB_ENV + : # https://github.com/launchableinc/cli/blob/v1.80.1/launchable/utils/authentication.py#L71 + echo "GITHUB_PR_HEAD_SHA=${{ github.event.pull_request.head.sha || github.sha }}" >> $GITHUB_ENV + echo "LAUNCHABLE_TOKEN=${{ inputs.launchable-token }}" >> $GITHUB_ENV + : # To prevent a slowdown in CI, disable request retries when the Launchable server is unstable. + echo "LAUNCHABLE_SKIP_TIMEOUT_RETRY=1" >> $GITHUB_ENV + echo "LAUNCHABLE_COMMIT_TIMEOUT=1" >> $GITHUB_ENV + if: steps.enable-launchable.outputs.enable-launchable + + - name: Set up path + shell: bash + working-directory: ${{ inputs.srcdir }} + # Since updated PATH variable will be available in only subsequent actions, we need to add the path beforehand. + # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-system-path + run: echo "$(python -msite --user-base)/bin" >> $GITHUB_PATH + if: >- + ${{ + steps.enable-launchable.outputs.enable-launchable + && (startsWith(inputs.os, 'macos') + || endsWith(inputs.os, 'ppc64le') + || endsWith(inputs.os, 's390x')) + }} + + - name: Set up Launchable + id: setup-launchable + shell: bash + working-directory: ${{ inputs.srcdir }} + run: | + set -x + pip install --user launchable + : # The build name cannot include a slash, so we replace the string here. + github_ref="${{ github.ref }}" + github_ref="${github_ref//\//_}" + : # With the --name option, we need to configure a unique identifier for this build. + : # To avoid setting the same build name as the CI which runs on other branches, we use the branch name here. + build_name="${github_ref}_${GITHUB_PR_HEAD_SHA}" + test_opts="${{ inputs.test-opts }}" + test_opts="${test_opts// /}" + test_opts="${test_opts//=/:}" + test_all_test_suite='test-all' + btest_test_suite='btest' + test_spec_test_suite='test-spec' + if [ "${{ inputs.is-yjit }}" = "true" ]; then + test_all_test_suite="yjit-${test_all_test_suite}" + btest_test_suite="yjit-${btest_test_suite}" + test_spec_test_suite="yjit-${test_spec_test_suite}" + fi + # launchable_setup target var -- refers ${target} prefixed variables + launchable_setup() { + local target=$1 session + eval [ "\${${target}_enabled}" = "true" ] || return + eval local suite=\${${target}_test_suite} + session=$(launchable record session \ + --build "${build_name}" \ + --observation \ + --flavor os="${{ inputs.os }}" \ + --flavor test_task="${{ inputs.test-task }}" \ + --flavor test_opts="${test_opts}" \ + --flavor workflow="${{ github.workflow }}" \ + --test-suite ${suite} \ + ) + echo "${target}_session=${session}" >> $GITHUB_OUTPUT + } + + launchable record build --name "${build_name}" + if launchable_setup test_all; then + echo "TESTS=${TESTS:+$TESTS }--launchable-test-reports=${test_all_report_file}" >> $GITHUB_ENV + fi + if launchable_setup btest; then + echo "BTESTS=${BTESTS:+$BTESTS }--launchable-test-reports=${btest_report_file}" >> $GITHUB_ENV + fi + if launchable_setup test_spec; then + echo "SPECOPTS=${SPECOPTS:$SPECOPTS }--launchable-test-reports=${test_spec_report_dir}" >> $GITHUB_ENV + echo test_spec_enabled=true >> $GITHUB_OUTPUT + fi + + echo launchable_setup_dir=$(pwd) >> $GITHUB_OUTPUT + if: steps.enable-launchable.outputs.enable-launchable + env: + test_all_enabled: ${{ steps.global.outputs.test_all_enabled }} + btest_enabled: ${{ steps.global.outputs.btest_enabled }} + test_spec_enabled: ${{ steps.global.outputs.test_spec_enabled }} + test_all_report_file: ${{ steps.global.outputs.test_all_report_file }} + btest_report_file: ${{ steps.global.outputs.btest_report_file }} + test_spec_report_dir: ${{ steps.global.outputs.test_spec_report_dir }} + + - name: make test-spec report directory in build directory + shell: bash + working-directory: ${{ inputs.builddir }} + run: mkdir "${test_spec_report_dir}" + if: ${{ steps.setup-launchable.outputs.test_spec_enabled == 'true' }} + env: + test_spec_report_dir: ${{ steps.global.outputs.test_spec_report_dir }} + + - name: Clean up test results in Launchable + uses: gacts/run-and-post-run@674528335da98a7afc80915ff2b4b860a0b3553a # v1.4.0 + with: + shell: bash + working-directory: ${{ inputs.builddir }} + post: | + rm -f "${test_all_report_file}" + rm -f "${btest_report_file}" + rm -fr "${test_spec_report_dir}" + rm -f launchable_stdout.log + rm -f launchable_stderr.log + if: always() && steps.setup-launchable.outcome == 'success' + env: + test_all_report_file: ${{ steps.global.outputs.test_all_report_file }} + btest_report_file: ${{ steps.global.outputs.btest_report_file }} + test_spec_report_dir: ${{ steps.global.outputs.test_spec_report_dir }} + + - name: Record test results in Launchable + uses: gacts/run-and-post-run@674528335da98a7afc80915ff2b4b860a0b3553a # v1.4.0 + with: + shell: bash + working-directory: ${{ inputs.builddir }} + post: | + if [[ "${test_all_enabled}" = "true" ]]; then \ + launchable record attachment \ + --session "${test_all_session}" \ + "${stdout_report_path}" \ + "${stderr_report_path}"; \ + launchable record tests \ + --session "${test_all_session}" \ + raw "${test_all_report_file}" || true; \ + fi + + if [[ "${btest_enabled}" = "true" ]]; then \ + launchable record attachment \ + --session "${btest_session}" \ + "${stdout_report_path}" \ + "${stderr_report_path}"; \ + launchable record tests \ + --session "${btest_session}" \ + raw "${btest_report_file}" || true; \ + fi + + if [[ "${test_spec_enabled}" = "true" ]]; then \ + launchable record attachment \ + --session "${test_spec_session}" \ + "${stdout_report_path}" \ + "${stderr_report_path}"; \ + launchable record tests \ + --session "${test_spec_session}" \ + raw ${test_spec_report_dir}/* || true; \ + fi + if: ${{ always() && steps.setup-launchable.outcome == 'success' }} + env: + test_all_report_file: ${{ steps.global.outputs.test_all_report_file }} + btest_report_file: ${{ steps.global.outputs.btest_report_file }} + test_spec_report_dir: ${{ steps.global.outputs.test_spec_report_dir }} + test_all_enabled: ${{ steps.global.outputs.test_all_enabled }} + btest_enabled: ${{ steps.global.outputs.btest_enabled }} + test_spec_enabled: ${{ steps.global.outputs.test_spec_enabled }} + test_all_session: ${{ steps.setup-launchable.outputs.test_all_session }} + btest_session: ${{ steps.setup-launchable.outputs.btest_session }} + test_spec_session: ${{ steps.setup-launchable.outputs.test_spec_session }} + stdout_report_path: ${{ steps.global.outputs.stdout_report_path }} + stderr_report_path: ${{ steps.global.outputs.stderr_report_path }} + LAUNCHABLE_SETUP_DIR: ${{ steps.setup-launchable.outputs.launchable_setup_dir }} diff --git a/.github/actions/setup/directories/action.yml b/.github/actions/setup/directories/action.yml new file mode 100644 index 0000000000..0e8ffd59ef --- /dev/null +++ b/.github/actions/setup/directories/action.yml @@ -0,0 +1,194 @@ +name: Setup directories etc. +description: >- + Set up the source code and build directories (plus some + environmental tweaks) + +inputs: + srcdir: + required: false + default: ${{ github.workspace }} + description: >- + Directory to (re-)checkout source codes. This will be created + if absent. If there is no `configure` file that is also + generated inside. + + builddir: + required: false + default: ${{ github.workspace }} + description: >- + Where binaries and other generated contents go. This will be + created if absent. + + make-command: + required: false + type: string + default: 'make' + description: >- + The command of `make`. + + makeup: + required: false + type: boolean + # Note that `default: false` evaluates to a string constant + # `'false'`, which is a truthy value :sigh: + # https://github.com/actions/runner/issues/2238 + default: '' + description: >- + If set to true, additionally runs `make up`. + + checkout: + required: false + type: boolean + default: true + description: >- + If set to '' (false), skip running actions/checkout. This is useful when + you don't want to overwrite a GitHub token that is already set up. + + dummy-files: + required: false + type: boolean + default: '' + description: >- + If set to true, creates dummy files in build dir. + + fetch-depth: + required: false + default: '1' + description: The depth of commit history fetched from the remote repository + + clean: + required: false + type: boolean + default: '' + description: >- + If set to true, clean build directory. + +outputs: {} # nothing? + +runs: + using: composite + + steps: + # Note that `shell: bash` works on both Windows and Linux, but not + # `shell: sh`. This is because GitHub hosted Windows runners have + # their bash manually installed. + - shell: bash + run: | + mkdir -p ${{ inputs.srcdir }} + mkdir -p ${{ inputs.builddir }} + + # Did you know that actions/checkout works without git(1)? We are + # checking that here. + - id: which + shell: bash + run: | + echo "git=`command -v git`" >> "$GITHUB_OUTPUT" + echo "sudo=`sudo true && command -v sudo`" >> "$GITHUB_OUTPUT" + echo "autoreconf=`command -v autoreconf`" >> "$GITHUB_OUTPUT" + + - if: steps.which.outputs.git + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.eol lf + git config --global advice.detachedHead 0 + git config --global init.defaultBranch garbage + + - if: inputs.checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + path: ${{ inputs.srcdir }} + fetch-depth: ${{ inputs.fetch-depth }} + + - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + with: + path: ${{ inputs.srcdir }}/.downloaded-cache + key: ${{ runner.os }}-${{ runner.arch }}-downloaded-cache + + - if: steps.which.outputs.autoreconf + shell: bash + working-directory: ${{ inputs.srcdir }} + run: ./autogen.sh --install + + # This is for MinGW. + - if: runner.os == 'Windows' + shell: bash + run: echo "GNUMAKEFLAGS=-j$((2 * NUMBER_OF_PROCESSORS))" >> $GITHUB_ENV + + - if: runner.os == 'Linux' + shell: bash + run: echo "GNUMAKEFLAGS=-sj$((1 + $(nproc)))" >> "$GITHUB_ENV" + + # macOS' GNU make is so old that they doesn't understand `GNUMAKEFLAGS`. + - if: runner.os == 'macOS' + shell: bash + run: echo "MAKEFLAGS=-j$((1 + $(sysctl -n hw.activecpu)))" >> "$GITHUB_ENV" + + - if: inputs.makeup + shell: bash + working-directory: ${{ inputs.srcdir }} + run: | + touch config.status .rbconfig.time + for mk in Makefile GNUmakefile; do + sed -f tool/prereq.status template/$mk.in > $mk + done + make up + + # Cleanup, runs even on failure + - if: always() && inputs.makeup + shell: bash + working-directory: ${{ inputs.srcdir }} + run: | + rm -f config.status .rbconfig.time \ + Makefile GNUmakefile uncommon.mk enc.mk noarch-fake.rb + + - if: steps.which.outputs.sudo + shell: bash + run: | + sudo chmod -R go-w /usr/share + chmod -v go-w $HOME $HOME/.config || : + declare -a dirs # -A is not supported by old bash, e.g. macos + SAVE_IFS="$IFS" IFS=:; set $PATH + for d do + while [ -d "$d" ]; do + case "$IFS${dirs[*]}$IFS" in *"$IFS$d$IFS"*) ;; *) dirs+=("$d");; esac + d="${d%/*}" + done + done + IFS="$SAVE_IFS" + sudo chmod -v go-w "${dirs[@]}" || : + + - if: inputs.dummy-files == 'true' + shell: bash + id: dummy-files + working-directory: ${{ inputs.builddir }} + run: | + : Create dummy files in build dir + set {{a..z},{A..Z},{0..9},foo,bar,test,zzz}.rb + for file; do \ + echo > $file "raise 'do not load $file'"; \ + done + # drop {a..z}.rb if case-insensitive filesystem + grep -F A.rb a.rb > /dev/null && set "${@:27}" + echo clean="cd ${{ inputs.builddir }} && rm $*" >> $GITHUB_OUTPUT + + - if: inputs.clean == 'true' + shell: bash + id: clean + run: | + echo distclean='cd ${{ inputs.builddir }} && ${{ inputs.make-command }} distclean' >> $GITHUB_OUTPUT + echo remained-files='find ${{ inputs.builddir }} -ls' >> $GITHUB_OUTPUT + [ "${{ inputs.builddir }}" = "${{ inputs.srcdir }}" ] || + echo final='rmdir ${{ inputs.builddir }}' >> $GITHUB_OUTPUT + + - name: clean + uses: gacts/run-and-post-run@81b6ce503cde93862cec047c54652e45c5dca991 # v1.4.3 + with: + working-directory: + post: | + ${{ steps.dummy-files.outputs.clean }} + ${{ steps.clean.outputs.distclean }} + ${{ steps.clean.outputs.remained-files }} + ${{ steps.clean.outputs.final }} + # rmdir randomly fails due to launchable files + continue-on-error: true diff --git a/.github/actions/setup/macos/action.yml b/.github/actions/setup/macos/action.yml new file mode 100644 index 0000000000..d0072ff828 --- /dev/null +++ b/.github/actions/setup/macos/action.yml @@ -0,0 +1,29 @@ +name: Setup macOS environment +description: >- + Installs necessary packages via Homebrew. + +inputs: {} # nothing? + +outputs: {} # nothing? + +runs: + using: composite + + steps: + - name: brew + shell: bash + run: | + brew install --quiet jemalloc gmp libffi openssl@3 zlib autoconf automake libtool + + - name: Set ENV + shell: bash + run: | + dir_config() { + local args=() lib var="$1"; shift + for lib in "$@"; do + args+=("--with-${lib%@*}-dir=$(brew --prefix $lib)") + done + echo "$var=${args[*]}" >> $GITHUB_ENV + } + dir_config ruby_configure_args gmp + dir_config CONFIGURE_ARGS openssl@3 diff --git a/.github/actions/setup/ubuntu/action.yml b/.github/actions/setup/ubuntu/action.yml new file mode 100644 index 0000000000..a9e5b41951 --- /dev/null +++ b/.github/actions/setup/ubuntu/action.yml @@ -0,0 +1,53 @@ +name: Setup ubuntu environment +description: >- + At the beginning there was no way but to copy & paste `apt-get` + everywhere. But now that we have composite actions, it seems better + merge them into one. + +inputs: + arch: + required: false + default: '' + description: >- + Architecture. Because we run this on a GitHub-hosted runner + acceptable value for this input is very limited. + +outputs: + arch: + value: ${{ steps.uname.outputs.uname }} + description: >- + Actual architecture. This could be different from the one + passed to the `inputs.arch`. For instance giving `i386` to this + action yields `i686`. + +runs: + using: composite + + steps: + - name: set SETARCH + shell: bash + run: echo "SETARCH=${setarch}" >> "$GITHUB_ENV" + env: + setarch: ${{ inputs.arch && format('setarch {0} --', inputs.arch) }} + + - id: uname + name: uname + shell: bash + run: | + echo uname=`${SETARCH} uname -m` >> "$GITHUB_OUTPUT" + echo dpkg=`${SETARCH} uname -m | sed s/686/386/` >> "$GITHUB_OUTPUT" + + - name: apt-get + shell: bash + env: + arch: ${{ inputs.arch && format(':{0}', steps.uname.outputs.dpkg) || '' }} + run: | + set -x + ${arch:+sudo dpkg --add-architecture ${arch#:}} + sudo apt-get update -qq || : + sudo apt-get install --no-install-recommends -qq -y -o=Dpkg::Use-Pty=0 \ + ${arch:+cross}build-essential${arch/:/-} \ + libssl-dev${arch} libyaml-dev${arch} libreadline6-dev${arch} \ + zlib1g-dev${arch} libncurses5-dev${arch} libffi-dev${arch} \ + autoconf ruby + sudo apt-get install -qq -y pkg-config${arch} || : diff --git a/.github/actions/slack/action.yml b/.github/actions/slack/action.yml new file mode 100644 index 0000000000..4a398da1d1 --- /dev/null +++ b/.github/actions/slack/action.yml @@ -0,0 +1,51 @@ +name: Post a message to slack +description: >- + We have our ruby/action-slack webhook. However its arguments are + bit verbose to be listed in every workflow files. Better merge them + into one. + +inputs: + SLACK_WEBHOOK_URL: + required: true + description: >- + The URL to post the payload. This is an input because it tends + to be stored in a secrets vault and a composite action cannot + look into one. + + label: + required: false + description: >- + Human-readable description of the run, something like "DEBUG=1". + This need not be unique among runs. + + event_name: + required: false + default: 'push' + description: >- + Target event to trigger notification. Notify only push by default. + + extra_channel_id: + required: false + description: >- + Slack channel ID to notify besides #alerts and #alerts-emoji. + +outputs: {} # Nothing? + +runs: + using: composite + + steps: + - uses: ruby/action-slack@54175162371f1f7c8eb94d7c8644ee2479fcd375 # v3.2.2 + with: + payload: | + { + "ci": "GitHub Actions", + "env": "${{ github.workflow }}${{ inputs.label && format(' / {0}', inputs.label) }}", + "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "commit": "${{ github.sha }}", + "branch": "${{ github.ref_name }}" + ${{ inputs.extra_channel_id && format(', "extra_channel_id": "{0}"', inputs.extra_channel_id) }} + } + env: + SLACK_WEBHOOK_URL: ${{ inputs.SLACK_WEBHOOK_URL }} + if: ${{ github.event_name == inputs.event_name && startsWith(github.repository, 'ruby/') }} diff --git a/.github/auto_request_review.yml b/.github/auto_request_review.yml new file mode 100644 index 0000000000..38496d5ceb --- /dev/null +++ b/.github/auto_request_review.yml @@ -0,0 +1,20 @@ +files: + 'yjit*': [team:jit] + 'yjit/**/*': [team:jit] + 'yjit/src/cruby_bindings.inc.rs': [] + 'bootstraptest/test_yjit*': [team:jit] + 'test/ruby/test_yjit*': [team:jit] + 'zjit*': [team:jit] + 'zjit/**/*': [team:jit] + 'zjit/src/cruby_bindings.inc.rs': [] + 'test/ruby/test_zjit*': [team:jit] + 'defs/jit.mk': [team:jit] + 'tool/zjit_bisect.rb': [team:jit] + 'doc/jit/*': [team:jit] + # Skip github workflow files because the team don't necessarily need to review dependabot updates for GitHub Actions. It's noisy in notifications, and they're auto-merged anyway. +options: + ignore_draft: true + # This currently doesn't work as intended. We want to skip reviews when only + # cruby_bingings.inc.rs is modified, but this skips reviews even when other + # files are modified as well. To be enabled after fixing the behavior. + #last_files_match_only: true diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml deleted file mode 100644 index 91f82b842b..0000000000 --- a/.github/codeql/codeql-config.yml +++ /dev/null @@ -1,3 +0,0 @@ -name: "CodeQL config for the Ruby language" - -languages: cpp diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..2c2982d1d4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' + - package-ecosystem: 'github-actions' + directory: '/.github/actions/slack' + schedule: + interval: 'daily' + - package-ecosystem: 'github-actions' + directory: '/.github/actions/setup/directories' + schedule: + interval: 'daily' + - package-ecosystem: 'cargo' + directory: '/yjit' + schedule: + interval: 'daily' + - package-ecosystem: 'vcpkg' + directory: '/' + schedule: + interval: 'daily' diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..e81aed8e98 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,6 @@ +Documentation: +- changed-files: + - all-globs-to-all-files: doc/** + +Backport: +- base-branch: 'ruby_3_\d' diff --git a/.github/workflows/annocheck.yml b/.github/workflows/annocheck.yml new file mode 100644 index 0000000000..899d601aef --- /dev/null +++ b/.github/workflows/annocheck.yml @@ -0,0 +1,110 @@ +name: Annocheck + +on: + push: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + pull_request: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + compile: + name: test-annocheck + + runs-on: ubuntu-latest + + container: + image: ghcr.io/ruby/ruby-ci-image:gcc-11 + options: --user root + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + env: + CONFIGURE_TTY: never + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + RUBY_DEBUG: ci rgengc + RUBY_TESTOPTS: >- + -q + --color=always + --tty=no + # FIXME: Drop skipping options + # https://bugs.ruby-lang.org/issues/18061 + # https://sourceware.org/annobin/annobin.html/Test-pie.html + TEST_ANNOCHECK_OPTS: '--skip-pie --skip-gaps' + + steps: + - run: id + working-directory: + + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 + with: + ruby-version: '3.1' + bundler: none + + # Minimal flags to pass the check. + # -g0 disables backtraces when SEGV. Do not set that. + - name: Run configure + run: > + ../src/configure -C + --enable-debug-env + --disable-install-doc + --with-ext=-test-/cxxanyargs,+ + --without-valgrind + --without-jemalloc + --without-gmp + --with-gcc="gcc-11 -fcf-protection -Wa,--generate-missing-build-notes=yes" + --enable-shared + debugflags=-ggdb3 + optflags=-O2 + LDFLAGS=-Wl,-z,now + + - run: make showflags + + - run: make + + - run: make test-annocheck + + - uses: ./.github/actions/slack + with: + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + +defaults: + run: + working-directory: build diff --git a/.github/workflows/auto_request_review.yml b/.github/workflows/auto_request_review.yml new file mode 100644 index 0000000000..207315a084 --- /dev/null +++ b/.github/workflows/auto_request_review.yml @@ -0,0 +1,20 @@ +name: Auto Request Review +on: + pull_request_target: + types: [opened, ready_for_review, reopened] + branches: [master] + +permissions: + contents: read + +jobs: + auto-request-review: + name: Auto Request Review + runs-on: ubuntu-latest + if: ${{ github.repository == 'ruby/ruby' && github.base_ref == 'master' }} + steps: + - name: Request review based on files changes and/or groups the author belongs to + uses: necojackarc/auto-request-review@e89da1a8cd7c8c16d9de9c6e763290b6b0e3d424 # v0.13.0 + with: + # scope: public_repo + token: ${{ secrets.MATZBOT_AUTO_REQUEST_REVIEW_TOKEN }} diff --git a/.github/workflows/auto_review_pr.yml b/.github/workflows/auto_review_pr.yml new file mode 100644 index 0000000000..ad0e63ba12 --- /dev/null +++ b/.github/workflows/auto_review_pr.yml @@ -0,0 +1,33 @@ +name: Auto Review PR +on: + pull_request_target: + types: [opened, ready_for_review, reopened] + branches: [master] + +permissions: + contents: read + +jobs: + auto-review-pr: + name: Auto Review PR + runs-on: ubuntu-latest + if: ${{ github.repository == 'ruby/ruby' && github.base_ref == 'master' }} + + permissions: + pull-requests: write + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.1 + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 + with: + ruby-version: '3.4' + bundler: none + + - name: Auto Review PR + run: ruby tool/auto_review_pr.rb "$GITHUB_PR_NUMBER" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/baseruby.yml b/.github/workflows/baseruby.yml index cf86c2d7d7..d3e734f885 100644 --- a/.github/workflows/baseruby.yml +++ b/.github/workflows/baseruby.yml @@ -4,62 +4,71 @@ on: push: paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' pull_request: paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' + merge_group: concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} +permissions: + contents: read + jobs: baseruby: name: BASERUBY - runs-on: ubuntu-20.04 - if: ${{ !startsWith(github.event.head_commit.message, '[DOC]') && !contains(github.event.pull_request.labels.*.name, 'Documentation') }} + + runs-on: ubuntu-22.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + strategy: matrix: ruby: - - ruby-2.2 -# - ruby-2.3 -# - ruby-2.4 -# - ruby-2.5 -# - ruby-2.6 - - ruby-2.7 - - ruby-3.0 + - ruby-3.1 + - ruby-3.2 + - ruby-3.3 steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: .downloaded-cache - key: downloaded-cache - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 with: ruby-version: ${{ matrix.ruby }} bundler: none - - run: echo "GNUMAKEFLAGS=-j$((1 + $(nproc --all)))" >> $GITHUB_ENV - - run: sudo apt-get install build-essential autoconf bison - - run: ./autogen.sh + + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - uses: ./.github/actions/setup/ubuntu + + - uses: ./.github/actions/setup/directories + with: + makeup: true + - run: ./configure --disable-install-doc - - run: make common-srcs - - run: make incs + - run: make all + - run: make test - - uses: k0kubun/action-slack@v2.0.0 + + - uses: ./.github/actions/slack with: - payload: | - { - "ci": "GitHub Actions", - "env": "${{ github.workflow }} / BASERUBY @ ${{ matrix.ruby }}", - "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] - } - env: + label: ${{ matrix.ruby }} SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot - if: ${{ failure() && github.event_name == 'push' }} + if: ${{ failure() }} diff --git a/.github/workflows/bundled_gems.yml b/.github/workflows/bundled_gems.yml index 12fb2b06e5..59f64e8312 100644 --- a/.github/workflows/bundled_gems.yml +++ b/.github/workflows/bundled_gems.yml @@ -1,131 +1,190 @@ name: bundled_gems +env: + UPDATE_ENABLED: true + on: push: + branches: ['master'] paths: - '.github/workflows/bundled_gems.yml' - 'gems/bundled_gems' pull_request: + branches: ['master'] paths: - '.github/workflows/bundled_gems.yml' - 'gems/bundled_gems' + merge_group: schedule: - cron: '45 6 * * *' + workflow_dispatch: + +permissions: # added using https://github.com/step-security/secure-workflows + contents: read jobs: update: + permissions: + contents: write # for Git to git push + if: ${{ github.event_name != 'schedule' || github.repository == 'ruby/ruby' }} + name: update ${{ github.workflow }} + runs-on: ubuntu-latest + steps: - - name: git config - run: | - git config --global advice.detachedHead 0 - git config --global init.defaultBranch garbage + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} + + - uses: ./.github/actions/setup/directories + with: + # Skip overwriting MATZBOT_AUTO_UPDATE_TOKEN + checkout: '' # false (ref: https://github.com/actions/runner/issues/2238) - name: Set ENV run: | - echo "GNUMAKEFLAGS=-j$((1 + $(nproc --all)))" >> $GITHUB_ENV echo "TODAY=$(date +%F)" >> $GITHUB_ENV - - uses: actions/checkout@v2 - - - uses: actions/cache@v2 - with: - path: .downloaded-cache - key: downloaded-cache-${{ github.sha }} - restore-keys: | - downloaded-cache - - name: Download previous gems list run: | - data=bundled_gems.json mkdir -p .downloaded-cache - ln -s .downloaded-cache/$data . - curl -O -R -z ./$data https://stdgems.org/$data + for data in bundled_gems.json default_gems.json; do + ln -s .downloaded-cache/$data . + curl -O -R -z ./$data https://stdgems.org/$data + done - name: Update bundled gems list + id: bundled_gems run: | - ruby -i~ tool/update-bundled_gems.rb gems/bundled_gems + ruby -i~ tool/update-bundled_gems.rb gems/bundled_gems >> $GITHUB_OUTPUT + if: ${{ env.UPDATE_ENABLED == 'true' }} + + - name: Update spec/bundler/support/builders.rb + run: | + #!ruby + rake_version = File.read("gems/bundled_gems")[/^rake\s+(\S+)/, 1] + print ARGF.read.sub(/^ *def rake_version\s*\K".*?"/) {rake_version.dump} + shell: ruby -i~ {0} spec/bundler/support/builders.rb + if: ${{ env.UPDATE_ENABLED == 'true' }} - name: Maintain updated gems list in NEWS run: | - require 'json' - news = File.read("NEWS.md") - prev = news[/since the \*+(\d+\.\d+\.\d+)\*+/, 1] - prevs = [prev, prev.sub(/\.\d+\z/, '')] - %W[bundled].each do |type| - last = JSON.parse(File.read("#{type}_gems.json"))['gems'].filter_map do |g| - v = g['versions'].values_at(*prevs).compact.first - g = g['gem'] - g = 'RubyGems' if g == 'rubygems' - [g, v] if v - end.to_h - changed = File.foreach("gems/#{type}_gems").filter_map do |l| - next if l.start_with?("#") - g, v = l.split(" ", 3) - [g, v] unless last[g] == v - end - changed, added = changed.partition {|g, _| last[g]} - news.sub!(/^\*\s+The following #{type} gems? are updated\.(\n\s+\*\s+)\K.*(?:\1.*)*/) do - changed.map {|g, v|"#{g} #{v}"}.join($1) - end or exit - news.sub!(/^\*\s+The following default gems are now bundled.*(\n\s+\*\s+)\K.*(?:\1.*)*/) do - added.map {|g, v|"#{g} #{v}"}.join($1) - end if added - File.write("NEWS.md", news) - end - shell: ruby {0} + ruby tool/update-NEWS-gemlist.rb bundled + if: ${{ env.UPDATE_ENABLED == 'true' }} - name: Check diffs id: diff run: | - git add -- NEWS.md - git diff --no-ext-diff --ignore-submodules --quiet -- gems/bundled_gems - continue-on-error: true + news= gems= + git diff --color --no-ext-diff --ignore-submodules --exit-code -- NEWS.md || + news=true + git diff --color --no-ext-diff --ignore-submodules --exit-code -- gems/bundled_gems || + gems=true + git add -- NEWS.md gems/bundled_gems + git add -- spec/bundler/support/builders.rb + echo news=$news >> $GITHUB_OUTPUT + echo gems=$gems >> $GITHUB_OUTPUT + echo update=${news:-$gems} >> $GITHUB_OUTPUT - - name: Install libraries + - name: Commit + id: commit + run: | + git pull --ff-only origin ${GITHUB_REF#refs/heads/} + message="Update bundled gems list" + if [ -z "${gems}" ]; then + git commit --message="[DOC] ${message} at ${GITHUB_SHA:0:30}" + else + git commit --message="${message} as of ${TODAY}" + fi + env: + TODAY: ${{ steps.bundled_gems.outputs.latest_date || env.TODAY }} + EMAIL: svn-admin@ruby-lang.org + GIT_AUTHOR_NAME: git + GIT_COMMITTER_NAME: git + gems: ${{ steps.diff.outputs.gems }} + if: ${{ steps.diff.outputs.update }} + + - name: Development revision of bundled gems run: | - set -x - sudo apt-get update -q || : - sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev bison autoconf ruby - if: ${{ steps.diff.outcome == 'failure' }} + #!ruby + file = "gems/bundled_gems" + + SECONDS_IN_DAY = 86400 + today = Time.new("#{ENV['TODAY']}Z") + if !(december = today.month == 12) + days = 30 + elsif (days = 26 - today.day).positive? + days += 4 + else + puts "::info:: just after released" + exit + end + + since = "#{today.year-1}-12-26" + ref = ENV['GITHUB_REF'] + puts "::group::\e[94mfetching \e[1m#{file}\e[22m since \e[1m#{since}\e[22m from \e[1m#{ref}\e[m" + system(*%W[git fetch --shallow-since=#{since} --no-tags origin #{ref}], exception: true) + puts "::endgroup::" + + puts "\e[94mchecking development version bundled gems older than \e[1m#{days}\e[22m days\e[m" + limit = today.to_i - days * SECONDS_IN_DAY + old = 0 + IO.popen(%W"git blame --line-porcelain -- #{file}") do |blame| + while head = blame.gets("\n\t") and s = blame.gets + next unless (gem = s.split(/\s+|#.*/)).size > 3 + time = head[/^committer-time \K\d+/].to_i + next if (d = limit - time) <= 0 + d /= SECONDS_IN_DAY + line = head[/\A\h+ \d+ \K\d+/].to_i + level = if d < days; 'warning'; else old += 1; 'error'; end + d += days + puts "::#{level} file=#{file},line=#{line},title=Older than #{d} days::#{gem[0]} #{gem[3]}" + end + end + abort "::error title=Too long-standing gems::The release comes soon." if december and old.nonzero? + shell: ruby {0} + env: + file: ${{ steps.logs.outputs.file }} + days: ${{ steps.logs.outputs.days }} + + - name: Install libraries + uses: ./.github/actions/setup/ubuntu + if: ${{ steps.diff.outputs.gems }} - name: Build run: | ./autogen.sh ./configure -C --disable-install-doc make - if: ${{ steps.diff.outcome == 'failure' }} + if: ${{ steps.diff.outputs.gems }} + + - name: Prepare bundled gems + run: | + make -s prepare-gems + if: ${{ steps.diff.outputs.gems }} - name: Test bundled gems run: | make -s test-bundled-gems - git add -- gems/bundled_gems timeout-minutes: 30 env: - RUBY_TESTOPTS: "-q --tty=no" - TEST_BUNDLED_GEMS_ALLOW_FAILURES: "" - if: ${{ steps.diff.outcome == 'failure' }} - - - name: Show diffs - id: show - run: | - git diff --cached --color --no-ext-diff --ignore-submodules --exit-code -- - continue-on-error: true + RUBY_TESTOPTS: '-q --tty=no' + TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' + if: ${{ steps.diff.outputs.gems }} - - name: Commit + - name: Push run: | - git pull --ff-only origin ${GITHUB_REF#refs/heads/} - message="Update bundled gems list at " - if [ ${{ steps.diff.outcome }} = success ]; then - git commit --message="${message}${GITHUB_SHA:0:30} [ci skip]" - else - git commit --message="${message}${TODAY}" - fi git push origin ${GITHUB_REF#refs/heads/} - env: - EMAIL: svn-admin@ruby-lang.org - GIT_AUTHOR_NAME: git - GIT_COMMITTER_NAME: git - if: ${{ github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull') && steps.show.outcome == 'failure' }} + if: >- + ${{ + github.repository == 'ruby/ruby' && + !startsWith(github.event_name, 'pull') && + steps.commit.outcome == 'success' + }} + + - uses: ./.github/actions/slack + with: + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index 6c7e8e5787..c5dec65e48 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -3,63 +3,57 @@ on: push: paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' pull_request: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' + merge_group: concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} +permissions: + contents: read + jobs: update-deps: + name: Dependency checks + strategy: matrix: - os: [ubuntu-20.04] + os: [ubuntu-latest] fail-fast: true + runs-on: ${{ matrix.os }} - if: ${{ !startsWith(github.event.head_commit.message, '[DOC]') && !contains(github.event.pull_request.labels.*.name, 'Documentation') }} + steps: - - name: Install libraries - run: | - set -x - sudo apt-get update -q || : - sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev bison autoconf ruby + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - uses: ./.github/actions/setup/ubuntu if: ${{ contains(matrix.os, 'ubuntu') }} - - name: Install libraries - run: | - brew upgrade - brew install gmp libffi openssl@1.1 zlib autoconf automake libtool readline + + - uses: ./.github/actions/setup/macos if: ${{ contains(matrix.os, 'macos') }} - - name: git config - run: | - git config --global advice.detachedHead 0 - git config --global init.defaultBranch garbage - - uses: actions/checkout@v2 - - uses: actions/cache@v2 + + - uses: ./.github/actions/setup/directories + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 with: - path: .downloaded-cache - key: downloaded-cache - - run: ./autogen.sh + ruby-version: '3.1' + bundler: none + - name: Run configure run: ./configure -C --disable-install-doc --disable-rubygems --with-gcc 'optflags=-O0' 'debugflags=-save-temps=obj -g' - - run: make all golf - - run: ruby tool/update-deps --fix + + - run: make fix-depends + - run: git diff --no-ext-diff --ignore-submodules --exit-code - - uses: k0kubun/action-slack@v2.0.0 + + - uses: ./.github/actions/slack with: - payload: | - { - "ci": "GitHub Actions", - "env": "${{ matrix.os }} / Dependencies need to update", - "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] - } - env: + label: ${{ matrix.os }} / Dependencies need to update SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot - if: ${{ failure() && github.event_name == 'push' }} + if: ${{ failure() }} diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml new file mode 100644 index 0000000000..2a2bd1df53 --- /dev/null +++ b/.github/workflows/check_misc.yml @@ -0,0 +1,127 @@ +name: Misc +on: [push, pull_request, merge_group] + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + checks: + name: Miscellaneous checks + + permissions: + contents: write # for Git to git push + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} + + - uses: ./.github/actions/setup/directories + with: + makeup: true + # Skip overwriting MATZBOT_AUTO_UPDATE_TOKEN + checkout: '' # false (ref: https://github.com/actions/runner/issues/2238) + + - name: Check for code styles + run: | + set -x + ruby tool/auto-style.rb "$GITHUB_OLD_SHA" "$GITHUB_NEW_SHA" + env: + GITHUB_OLD_SHA: ${{ github.event.pull_request.base.sha }} + GITHUB_NEW_SHA: ${{ github.event.pull_request.merge_commit_sha }} + # Skip 'push' events because post_push.yml fixes them on push + if: ${{ github.repository == 'ruby/ruby' && startsWith(github.event_name, 'pull') }} + + - name: Check for bash specific substitution in configure.ac + run: | + git grep -n '\${[A-Za-z_0-9]*/' -- configure.ac && exit 1 || : + + - name: Check for header macros + run: | + fail= + for header in ruby/*.h; do + git grep -l -F -e $header -e HAVE_`echo $header | tr a-z./ A-Z__` -- . > /dev/null && continue + fail=1 + echo $header + done + exit $fail + working-directory: include + + - id: now + run: | + date +"mon=%-m"%n"day=%-d" >> $GITHUB_OUTPUT + env: + TZ: Tokyo/Asia + + - id: deprecation + run: | + eval $(sed -n 's/^#define RUBY_API_VERSION_\(MAJOR\|MINOR\) /\1=/p' include/ruby/version.h) + if git --no-pager grep --color -o 'rb_warn_deprecated_to_remove_at('$MAJOR'\.'$MINOR',.*' -- '*.c' >&2; then + false + else + true + fi + continue-on-error: ${{ steps.now.outputs.mon < 12 }} + + - name: Check if to generate documents + id: rdoc + run: | + set -- $(sed 's/#.*//;/^rdoc /!d' gems/bundled_gems) + { echo version=$2; echo ref=$4; } >> $GITHUB_OUTPUT + echo RDOC='ruby -W0 --disable-gems tool/rdoc-srcdir -q' >> $GITHUB_ENV + + - name: Checkout rdoc + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + repository: ruby/rdoc + ref: ${{ steps.rdoc.outputs.ref }} + path: .bundle/gems/rdoc-${{ steps.rdoc.outputs.version }} + if: ${{ steps.rdoc.outputs.ref != '' }} + + - name: Generate rdoc + run: | + set -x + gempath=$(ruby -e 'print Gem.user_dir, "/bin"') + PATH=$gempath:$PATH + gem install --user bundler + bundle config --local path vendor/bundle + bundle install --jobs 4 + bundle exec rake generate + working-directory: .bundle/gems/rdoc-${{ steps.rdoc.outputs.version }} + if: ${{ steps.rdoc.outputs.ref != '' }} + + - name: Core docs coverage + run: | + $RDOC -C -x ^ext -x ^lib . + + - name: Generate docs + id: docs + run: | + $RDOC --op html . + echo htmlout=ruby-html-${GITHUB_SHA:0:10} >> $GITHUB_OUTPUT + # Generate only when document commit/PR + if: >- + ${{false + || contains(github.event.head_commit.message, '[ruby/rdoc]') + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + }} + + - name: Upload docs + uses: actions/upload-artifact@v6.0.0 + with: + path: html + name: ${{ steps.docs.outputs.htmlout }} + if: ${{ steps.docs.outcome == 'success' }} + + - uses: ./.github/actions/slack + with: + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7a6f716f1b..a92c93b476 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,61 +1,121 @@ -name: "Code scanning - action" +name: 'CodeQL' on: push: + branches: ['master'] paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' pull_request: paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' schedule: - - cron: '0 12 * * 4' + - cron: '0 12 * * *' + workflow_dispatch: concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} -jobs: - CodeQL-Build: +permissions: # added using https://github.com/step-security/secure-workflows + contents: read - # CodeQL runs on ubuntu-latest and windows-latest +jobs: + analyze: + name: Analyze runs-on: ubuntu-latest - if: ${{ !startsWith(github.event.head_commit.message, '[DOC]') && !contains(github.event.pull_request.labels.*.name, 'Documentation') }} + permissions: + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/autobuild to send a status report + # CodeQL fails to run pull requests from dependabot due to missing write access to upload results. + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} env: enable_install_doc: no + strategy: + fail-fast: false + matrix: + include: + - language: cpp + - language: ruby + steps: - - name: Install libraries - run: | - set -x - sudo apt-get update -q || : - sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev bison autoconf ruby + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Install libraries + if: ${{ contains(matrix.os, 'macos') }} + uses: ./.github/actions/setup/macos + + - name: Install libraries + if : ${{ matrix.os == 'ubuntu-latest' }} + uses: ./.github/actions/setup/ubuntu - - name: Checkout repository - uses: actions/checkout@v2 + - uses: ./.github/actions/setup/directories - - uses: actions/cache@v2 - with: - path: .downloaded-cache - key: downloaded-cache + - name: Remove an obsolete rubygems vendored file + if: ${{ matrix.os == 'ubuntu-latest' }} + run: sudo rm /usr/lib/ruby/vendor_ruby/rubygems/defaults/operating_system.rb - - name: Remove an obsolete rubygems vendored file - run: sudo rm /usr/lib/ruby/vendor_ruby/rubygems/defaults/operating_system.rb + - name: Initialize CodeQL + uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 + with: + languages: ${{ matrix.language }} + trap-caching: false + debug: true - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - config-file: ./.github/codeql/codeql-config.yml + - name: Autobuild + uses: github/codeql-action/autobuild@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 - - name: Set ENV - run: echo "GNUMAKEFLAGS=-j$((1 + $(nproc --all)))" >> $GITHUB_ENV + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 + with: + category: '/language:${{ matrix.language }}' + upload: False + output: sarif-results - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + - name: filter-sarif + uses: advanced-security/filter-sarif@f3b8118a9349d88f7b1c0c488476411145b6270d # v1.0.1 + with: + patterns: | + +**/*.rb + -lib/uri/mailto.rb:rb/overly-large-range + -lib/uri/rfc3986_parser.rb:rb/overly-large-range + -lib/bundler/vendor/uri/lib/uri/mailto.rb:rb/overly-large-range + -lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb:rb/overly-large-range + -test/ruby/test_io.rb:rb/non-constant-kernel-open + -test/open-uri/test_open-uri.rb:rb/non-constant-kernel-open + -test/open-uri/test_ssl.rb:rb/non-constant-kernel-open + -spec/ruby/core/io/binread_spec.rb:rb/non-constant-kernel-open + -spec/ruby/core/io/readlines_spec.rb:rb/non-constant-kernel-open + -spec/ruby/core/io/foreach_spec.rb:rb/non-constant-kernel-open + -spec/ruby/core/io/write_spec.rb:rb/non-constant-kernel-open + -spec/ruby/core/io/read_spec.rb:rb/non-constant-kernel-open + -spec/ruby/core/kernel/open_spec.rb:rb/non-constant-kernel-open + input: sarif-results/${{ matrix.language }}.sarif + output: sarif-results/${{ matrix.language }}.sarif + if: ${{ matrix.language == 'ruby' }} + continue-on-error: true - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 + with: + sarif_file: sarif-results/${{ matrix.language }}.sarif + continue-on-error: true diff --git a/.github/workflows/compilers.yml b/.github/workflows/compilers.yml index 2af9fa55d8..8c0ca54e0b 100644 --- a/.github/workflows/compilers.yml +++ b/.github/workflows/compilers.yml @@ -1,246 +1,334 @@ +# Some tests depending on this name 'Compilations' via $GITHUB_WORKFLOW. Make sure to update such tests when renaming this workflow. name: Compilations on: push: paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' pull_request: paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' + merge_group: concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} -# Github actions does not support YAML anchors. This creative use of -# environment variables (plus the "echo $GITHUB_ENV" hack) is to reroute that -# restriction. -env: - default_cc: clang-14 - append_cc: '' - crosshost: '' - - # -O1 is faster than -O3 in our tests... Majority of time are consumed trying - # to optimize binaries. Also Github Actions run on relatively modern CPUs - # compared to, say, GCC 4 or Clang 3. We don't specify `-march=native` - # because compilers tend not understand what the CPU is. - optflags: '-O1' - - # -g0 disables backtraces when SEGV. Do not set that. - debugflags: '-ggdb3' - - default_configure: >- - --enable-debug-env - --disable-install-doc - --with-ext=-test-/cxxanyargs,+ - append_configure: >- - --without-valgrind - --without-jemalloc - --without-gmp - - UPDATE_UNICODE: >- - UNICODE_FILES=. - UNICODE_PROPERTY_FILES=. - UNICODE_AUXILIARY_FILES=. - UNICODE_EMOJI_FILES=. - CONFIGURE_TTY: never - GITPULLOPTIONS: --no-tags origin ${{github.ref}} - RUBY_DEBUG: ci rgengc - RUBY_TESTOPTS: >- - -q - --color=always - --tty=no +permissions: + contents: read +# Each job is split so that they roughly take 30min to run through. jobs: - compile: - strategy: - fail-fast: false - matrix: - entry: - - { key: default_cc, name: gcc-11, value: gcc-11, container: gcc-11 } - - { key: default_cc, name: gcc-10, value: gcc-10, container: gcc-10 } - - { key: default_cc, name: gcc-9, value: gcc-9, container: gcc-9 } - - { key: default_cc, name: gcc-8, value: gcc-8, container: gcc-8 } - - { key: default_cc, name: gcc-7, value: gcc-7, container: gcc-7 } - - { key: default_cc, name: gcc-6, value: gcc-6, container: gcc-6 } - - { key: default_cc, name: gcc-5, value: gcc-5, container: gcc-5 } - - { key: default_cc, name: gcc-4.8, value: gcc-4.8, container: gcc-4.8 } - - key: default_cc - name: 'gcc-11 LTO' - value: 'gcc-11 -O2 -flto=auto -ffat-lto-objects' - container: gcc-11 - shared: '--disable-shared' - # check: true - - { key: default_cc, name: clang-14, value: clang-14, container: clang-14 } - - { key: default_cc, name: clang-13, value: clang-13, container: clang-13 } - - { key: default_cc, name: clang-12, value: clang-12, container: clang-12 } - - { key: default_cc, name: clang-11, value: clang-11, container: clang-11 } - - { key: default_cc, name: clang-10, value: clang-10, container: clang-10 } - - { key: default_cc, name: clang-9, value: clang-9, container: clang-9 } - - { key: default_cc, name: clang-8, value: clang-8, container: clang-8 } - - { key: default_cc, name: clang-7, value: clang-7, container: clang-7 } - - { key: default_cc, name: clang-6.0, value: clang-6.0, container: clang-6.0 } - - { key: default_cc, name: clang-5.0, value: clang-5.0, container: clang-5.0 } - - { key: default_cc, name: clang-4.0, value: clang-4.0, container: clang-4.0 } - - { key: default_cc, name: clang-3.9, value: clang-3.9, container: clang-3.9 } - - key: default_cc - name: 'clang-14 LTO' - value: 'clang-14 -O2 -flto=auto' - container: clang-14 - shared: '--disable-shared' - # check: true - - - { key: crosshost, name: aarch64-linux-gnu, value: aarch64-linux-gnu, container: crossbuild-essential-arm64 } -# - { key: crosshost, name: arm-linux-gnueabi, value: arm-linux-gnueabi } -# - { key: crosshost, name: arm-linux-gnueabihf, value: arm-linux-gnueabihf } -# - { key: crosshost, name: i686-w64-mingw32, value: i686-w64-mingw32 } -# - { key: crosshost, name: powerpc-linux-gnu, value: powerpc-linux-gnu } - - { key: crosshost, name: powerpc64le-linux-gnu, value: powerpc64le-linux-gnu, container: crossbuild-essential-ppc64el } - - { key: crosshost, name: s390x-linux-gnu, value: s390x-linux-gnu, container: crossbuild-essential-s390x } - - { key: crosshost, name: x86_64-w64-mingw32, value: x86_64-w64-mingw32, container: mingw-w64 } - - - { key: append_cc, name: c99, value: '-std=c99 -Werror=pedantic -pedantic-errors' } -# - { key: append_cc, name: c11, value: '-std=c11 -Werror=pedantic -pedantic-errors' } -# - { key: append_cc, name: c17, value: '-std=c17 -Werror=pedantic -pedantic-errors' } - - { key: append_cc, name: c2x, value: '-std=c2x -Werror=pedantic -pedantic-errors' } - - { key: CXXFLAGS, name: c++98, value: '-std=c++98 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' } -# - { key: CXXFLAGS, name: c++11, value: '-std=c++11 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' } -# - { key: CXXFLAGS, name: c++14, value: '-std=c++14 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' } -# - { key: CXXFLAGS, name: c++17, value: '-std=c++17 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' } - - { key: CXXFLAGS, name: c++2a, value: '-std=c++2a -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' } - - - { key: optflags, name: '-O0', value: '-O0 -march=x86-64 -mtune=generic' } -# - { key: optflags, name: '-O3', value: '-O3 -march=x86-64 -mtune=generic', check: true } - - - { key: append_configure, name: gmp, value: '--with-gmp' } - - { key: append_configure, name: jemalloc, value: '--with-jemalloc' } - - { key: append_configure, name: valgrind, value: '--with-valgrind' } - - { key: append_configure, name: 'coroutine=ucontext', value: '--with-coroutine=ucontext' } - - { key: append_configure, name: 'coroutine=pthread', value: '--with-coroutine=pthread' } - - { key: append_configure, name: disable-jit-support, value: '--disable-jit-support' } - - { key: append_configure, name: disable-dln, value: '--disable-dln' } - - { key: append_configure, name: disable-rubygems, value: '--disable-rubygems' } - - - { key: cppflags, name: OPT_THREADED_CODE=1, value: '-DOPT_THREADED_CODE=1' } - - { key: cppflags, name: OPT_THREADED_CODE=2, value: '-DOPT_THREADED_CODE=2' } - - { key: cppflags, name: OPT_THREADED_CODE=3, value: '-DOPT_THREADED_CODE=3' } - - - { key: cppflags, name: NDEBUG, value: '-DNDEBUG' } - - { key: cppflags, name: RUBY_DEBUG, value: '-DRUBY_DEBUG' } -# - { key: cppflags, name: ARRAY_DEBUG, value: '-DARRAY_DEBUG' } -# - { key: cppflags, name: BIGNUM_DEBUG, value: '-DBIGNUM_DEBUG' } -# - { key: cppflags, name: CCAN_LIST_DEBUG, value: '-DCCAN_LIST_DEBUG' } -# - { key: cppflags, name: CPDEBUG=-1, value: '-DCPDEBUG=-1' } -# - { key: cppflags, name: ENC_DEBUG, value: '-DENC_DEBUG' } -# - { key: cppflags, name: GC_DEBUG, value: '-DGC_DEBUG' } -# - { key: cppflags, name: HASH_DEBUG, value: '-DHASH_DEBUG' } -# - { key: cppflags, name: ID_TABLE_DEBUG, value: '-DID_TABLE_DEBUG' } -# - { key: cppflags, name: RGENGC_DEBUG=-1, value: '-DRGENGC_DEBUG=-1' } -# - { key: cppflags, name: SYMBOL_DEBUG, value: '-DSYMBOL_DEBUG' } -# - { key: cppflags, name: THREAD_DEBUG=-1, value: '-DTHREAD_DEBUG=-1' } - -# - { key: cppflags, name: RGENGC_CHECK_MODE, value: '-DRGENGC_CHECK_MODE' } -# - { key: cppflags, name: TRANSIENT_HEAP_CHECK_MODE, value: '-DTRANSIENT_HEAP_CHECK_MODE' } -# - { key: cppflags, name: VM_CHECK_MODE, value: '-DVM_CHECK_MODE' } - - - { key: cppflags, name: USE_EMBED_CI=0, value: '-DUSE_EMBED_CI=0' } - - { key: cppflags, name: USE_FLONUM=0, value: '-DUSE_FLONUM=0' } -# - { key: cppflags, name: USE_GC_MALLOC_OBJ_INFO_DETAILS, value: '-DUSE_GC_MALLOC_OBJ_INFO_DETAILS' } - - { key: cppflags, name: USE_LAZY_LOAD, value: '-DUSE_LAZY_LOAD' } -# - { key: cppflags, name: USE_RINCGC=0, value: '-DUSE_RINCGC=0' } -# - { key: cppflags, name: USE_SYMBOL_GC=0, value: '-DUSE_SYMBOL_GC=0' } -# - { key: cppflags, name: USE_THREAD_CACHE=0, value: '-DUSE_THREAD_CACHE=0' } -# - { key: cppflags, name: USE_TRANSIENT_HEAP=0, value: '-DUSE_TRANSIENT_HEAP=0' } -# - { key: cppflags, name: USE_RUBY_DEBUG_LOG=1, value: '-DUSE_RUBY_DEBUG_LOG=1' } - - - { key: cppflags, name: DEBUG_FIND_TIME_NUMGUESS, value: '-DDEBUG_FIND_TIME_NUMGUESS' } - - { key: cppflags, name: DEBUG_INTEGER_PACK, value: '-DDEBUG_INTEGER_PACK' } -# - { key: cppflags, name: ENABLE_PATH_CHECK, value: '-DENABLE_PATH_CHECK' } - - - { key: cppflags, name: GC_DEBUG_STRESS_TO_CLASS, value: '-DGC_DEBUG_STRESS_TO_CLASS' } -# - { key: cppflags, name: GC_ENABLE_LAZY_SWEEP=0, value: '-DGC_ENABLE_LAZY_SWEEP=0' } -# - { key: cppflags, name: GC_PROFILE_DETAIL_MEMOTY, value: '-DGC_PROFILE_DETAIL_MEMOTY' } -# - { key: cppflags, name: GC_PROFILE_MORE_DETAIL, value: '-DGC_PROFILE_MORE_DETAIL' } - -# - { key: cppflags, name: CALC_EXACT_MALLOC_SIZE, value: '-DCALC_EXACT_MALLOC_SIZE' } -# - { key: cppflags, name: MALLOC_ALLOCATED_SIZE_CHECK, value: '-DMALLOC_ALLOCATED_SIZE_CHECK' } - -# - { key: cppflags, name: IBF_ISEQ_ENABLE_LOCAL_BUFFER, value: '-DIBF_ISEQ_ENABLE_LOCAL_BUFFER' } - -# - { key: cppflags, name: RGENGC_ESTIMATE_OLDMALLOC, value: '-DRGENGC_ESTIMATE_OLDMALLOC' } -# - { key: cppflags, name: RGENGC_FORCE_MAJOR_GC, value: '-DRGENGC_FORCE_MAJOR_GC' } -# - { key: cppflags, name: RGENGC_OBJ_INFO, value: '-DRGENGC_OBJ_INFO' } -# - { key: cppflags, name: RGENGC_OLD_NEWOBJ_CHECK, value: '-DRGENGC_OLD_NEWOBJ_CHECK' } -# - { key: cppflags, name: RGENGC_PROFILE, value: '-DRGENGC_PROFILE' } - -# - { key: cppflags, name: VM_DEBUG_BP_CHECK, value: '-DVM_DEBUG_BP_CHECK' } -# - { key: cppflags, name: VM_DEBUG_VERIFY_METHOD_CACHE, value: '-DVM_DEBUG_VERIFY_METHOD_CACHE' } - - - { key: cppflags, name: MJIT_FORCE_ENABLE, value: '-DMJIT_FORCE_ENABLE' } - - { key: cppflags, name: YJIT_FORCE_ENABLE, value: '-DYJIT_FORCE_ENABLE' } - - name: ${{ matrix.entry.name }} + compile-if: + name: 'omnibus compilations, trigger' runs-on: ubuntu-latest - container: - image: ghcr.io/ruby/ruby-ci-image:${{ matrix.entry.container || 'clang-14' }} - options: --user root - if: ${{ !startsWith(github.event.head_commit.message, '[DOC]') && !contains(github.event.pull_request.labels.*.name, 'Documentation') }} + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} steps: - - run: id + - run: true working-directory: - - run: mkdir build - working-directory: - - name: setenv - run: | - echo "${{ matrix.entry.key }}=${{ matrix.entry.value }}" >> $GITHUB_ENV - echo "GNUMAKEFLAGS=-sj$((1 + $(nproc --all)))" >> $GITHUB_ENV - - uses: actions/checkout@v2 + + compile1: + name: 'omnibus compilations, #1' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + # Set fetch-depth: 10 so that Launchable can receive commits information. + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - name: 'clang 18 LTO' + uses: './.github/actions/compilers' with: - path: src - - uses: actions/cache@v2 + tag: clang-18 + with_gcc: 'clang-18 -flto=auto' + optflags: '-O2' + enable_shared: false + timeout-minutes: 30 + - { uses: './.github/actions/compilers', name: '-O0', with: { optflags: '-O0 -march=x86-64 -mtune=generic' }, timeout-minutes: 5 } + # - { uses: './.github/actions/compilers', name: '-O3', with: { optflags: '-O3 -march=x86-64 -mtune=generic', check: true } } + + compile2: + name: 'omnibus compilations, #2' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - name: 'GCC 15 LTO' + uses: './.github/actions/compilers' with: - path: src/.downloaded-cache - key: downloaded-cache - - run: ./autogen.sh - working-directory: src - - name: Run configure - run: > - ../src/configure -C ${default_configure} ${append_configure} - ${{ matrix.entry.key == 'crosshost' && '--host="${crosshost}"' || '--with-gcc="${default_cc} ${append_cc}"' }} - ${{ matrix.entry.shared || '--enable-shared' }} - - run: make extract-extlibs - - run: make incs - - run: make - - run: make leaked-globals - - run: make test - - run: make install - if: ${{ matrix.entry.check }} - - run: make prepare-gems - if: ${{ matrix.entry.check }} - - run: make test-tool - if: ${{ matrix.entry.check }} - - run: make test-all TESTS='-- ruby -ext-' - if: ${{ matrix.entry.check }} - - run: make test-spec - if: ${{ matrix.entry.check }} - - - uses: k0kubun/action-slack@v2.0.0 + tag: gcc-15 + with_gcc: 'gcc-15 -flto=auto -ffat-lto-objects -Werror=lto-type-mismatch' + optflags: '-O2' + enable_shared: false + timeout-minutes: 10 + - { uses: './.github/actions/compilers', name: 'ext/Setup', with: { static_exts: 'etc json/* */escape' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GCC 15', with: { tag: 'gcc-15' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GCC 14', with: { tag: 'gcc-14' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GCC 13', with: { tag: 'gcc-13' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GCC 12', with: { tag: 'gcc-12' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GCC 11', with: { tag: 'gcc-11' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GCC 10', with: { tag: 'gcc-10' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GCC 9', with: { tag: 'gcc-9' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GCC 8', with: { tag: 'gcc-8' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GCC 7', with: { tag: 'gcc-7' }, timeout-minutes: 5 } + + compile3: + name: 'omnibus compilations, #3' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - { uses: './.github/actions/compilers', name: 'clang 22', with: { tag: 'clang-22' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 21', with: { tag: 'clang-21' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 20', with: { tag: 'clang-20' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 19', with: { tag: 'clang-19' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 18', with: { tag: 'clang-18' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 17', with: { tag: 'clang-17' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 16', with: { tag: 'clang-16' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 15', with: { tag: 'clang-15' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 14', with: { tag: 'clang-14' }, timeout-minutes: 5 } + + compile4: + name: 'omnibus compilations, #4' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - { uses: './.github/actions/compilers', name: 'clang 13', with: { tag: 'clang-13' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 12', with: { tag: 'clang-12' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 11', with: { tag: 'clang-11' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 10', with: { tag: 'clang-10' }, timeout-minutes: 5 } + # llvm-objcopy<=9 doesn't have --wildcard. It compiles, but leaves Rust symbols in libyjit.o and fail `make test-leaked-globals`. + - { uses: './.github/actions/compilers', name: 'clang 9', with: { tag: 'clang-9', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 8', with: { tag: 'clang-8', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 7', with: { tag: 'clang-7', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'clang 6', with: { tag: 'clang-6.0', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 5 } + + compile5: + name: 'omnibus compilations, #5' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + # -Wno-strict-prototypes is necessary with current clang-15 since + # older autoconf generate functions without prototype and -pedantic + # now implies strict-prototypes. Disabling the error but leaving the + # warning generates a lot of noise from use of ANYARGS in + # rb_define_method() and friends. + # See: https://github.com/llvm/llvm-project/commit/11da1b53d8cd3507959022cd790d5a7ad4573d94 + - { uses: './.github/actions/compilers', name: 'C99', with: { CFLAGS: '-std=c99 -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'C11', with: { CFLAGS: '-std=c11 -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'C17', with: { CFLAGS: '-std=c17 -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'C23', with: { CFLAGS: '-std=c2x -Werror=pedantic -pedantic-errors -Wno-strict-prototypes' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'C++98', with: { CXXFLAGS: '-std=c++98 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'C++11', with: { CXXFLAGS: '-std=c++11 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'C++14', with: { CXXFLAGS: '-std=c++14 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'C++17', with: { CXXFLAGS: '-std=c++17 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 5 } + + compile6: + name: 'omnibus compilations, #6' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - { uses: './.github/actions/compilers', name: 'C++20', with: { CXXFLAGS: '-std=c++20 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'C++23', with: { CXXFLAGS: '-std=c++23 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'C++26', with: { CXXFLAGS: '-std=c++26 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'gmp', with: { append_configure: '--with-gmp', test_all: 'ruby/test_bignum.rb', test_spec: "/github/workspace/src/spec/ruby/core/integer" }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'jemalloc', with: { append_configure: '--with-jemalloc' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'valgrind', with: { append_configure: '--with-valgrind' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'coroutine=ucontext', with: { append_configure: '--with-coroutine=ucontext' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'coroutine=pthread', with: { append_configure: '--with-coroutine=pthread' }, timeout-minutes: 5 } + + compile7: + name: 'omnibus compilations, #7' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - { uses: './.github/actions/compilers', name: 'disable-jit', with: { append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'disable-yjit', with: { append_configure: '--disable-yjit' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'disable-zjit', with: { append_configure: '--disable-zjit' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'disable-dln', with: { append_configure: '--disable-dln' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'enable-mkmf-verbose', with: { append_configure: '--enable-mkmf-verbose' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'disable-rubygems', with: { append_configure: '--disable-rubygems' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'RUBY_DEVEL', with: { append_configure: '--enable-devel' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'OPT_THREADED_CODE=0', with: { cppflags: '-DOPT_THREADED_CODE=0' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'OPT_THREADED_CODE=1', with: { cppflags: '-DOPT_THREADED_CODE=1' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'OPT_THREADED_CODE=2', with: { cppflags: '-DOPT_THREADED_CODE=2' }, timeout-minutes: 5 } + + compile8: + name: 'omnibus compilations, #8' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - { uses: './.github/actions/compilers', name: 'NDEBUG', with: { cppflags: '-DNDEBUG' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'RUBY_DEBUG', with: { cppflags: '-DRUBY_DEBUG' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'ARRAY_DEBUG', with: { cppflags: '-DARRAY_DEBUG' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'BIGNUM_DEBUG', with: { cppflags: '-DBIGNUM_DEBUG' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'CCAN_LIST_DEBUG', with: { cppflags: '-DCCAN_LIST_DEBUG' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'CPDEBUG=-1', with: { cppflags: '-DCPDEBUG=-1' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'ENC_DEBUG', with: { cppflags: '-DENC_DEBUG' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GC_DEBUG', with: { cppflags: '-DGC_DEBUG' }, timeout-minutes: 5 } + + compile9: + name: 'omnibus compilations, #9' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - { uses: './.github/actions/compilers', name: 'HASH_DEBUG', with: { cppflags: '-DHASH_DEBUG' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'ID_TABLE_DEBUG', with: { cppflags: '-DID_TABLE_DEBUG' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'RGENGC_DEBUG=-1', with: { cppflags: '-DRGENGC_DEBUG=-1' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'SYMBOL_DEBUG', with: { cppflags: '-DSYMBOL_DEBUG' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'RGENGC_CHECK_MODE', with: { cppflags: '-DRGENGC_CHECK_MODE' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'VM_CHECK_MODE', with: { cppflags: '-DVM_CHECK_MODE' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'USE_EMBED_CI=0', with: { cppflags: '-DUSE_EMBED_CI=0' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'USE_FLONUM=0', with: { cppflags: '-DUSE_FLONUM=0', append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 5 } + + compileX: + name: 'omnibus compilations, #10' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - { uses: './.github/actions/compilers', name: 'USE_LAZY_LOAD', with: { cppflags: '-DUSE_LAZY_LOAD' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'USE_SYMBOL_GC=0', with: { cppflags: '-DUSE_SYMBOL_GC=0' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'USE_THREAD_CACHE=0', with: { cppflags: '-DUSE_THREAD_CACHE=0' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'USE_RUBY_DEBUG_LOG=1', with: { cppflags: '-DUSE_RUBY_DEBUG_LOG=1' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'USE_DEBUG_COUNTER', with: { cppflags: '-DUSE_DEBUG_COUNTER=1' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'SHARABLE_MIDDLE_SUBSTRING', with: { cppflags: '-DSHARABLE_MIDDLE_SUBSTRING=1' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'DEBUG_FIND_TIME_NUMGUESS', with: { cppflags: '-DDEBUG_FIND_TIME_NUMGUESS' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'DEBUG_INTEGER_PACK', with: { cppflags: '-DDEBUG_INTEGER_PACK' }, timeout-minutes: 5 } + + compileB: + name: 'omnibus compilations, #11' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - { uses: './.github/actions/compilers', name: 'GC_DEBUG_STRESS_TO_CLASS', with: { cppflags: '-DGC_DEBUG_STRESS_TO_CLASS' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GC_ENABLE_LAZY_SWEEP=0', with: { cppflags: '-DGC_ENABLE_LAZY_SWEEP=0' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GC_PROFILE_DETAIL_MEMORY', with: { cppflags: '-DGC_PROFILE_DETAIL_MEMORY' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'GC_PROFILE_MORE_DETAIL', with: { cppflags: '-DGC_PROFILE_MORE_DETAIL' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'MALLOC_ALLOCATED_SIZE_CHECK', with: { cppflags: '-DMALLOC_ALLOCATED_SIZE_CHECK' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'RGENGC_ESTIMATE_OLDMALLOC', with: { cppflags: '-DRGENGC_ESTIMATE_OLDMALLOC' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'RGENGC_PROFILE', with: { cppflags: '-DRGENGC_PROFILE' }, timeout-minutes: 5 } + + compileC: + name: 'omnibus compilations, #12' + runs-on: ubuntu-latest + needs: compile-if + if: ${{ needs.compile-if.result == 'success' }} + timeout-minutes: 60 + services: { docuum: { image: 'stephanmisc/docuum', options: '--init', volumes: [ '/root', '/var/run/docker.sock:/var/run/docker.sock' ] } } + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } + - { uses: './.github/actions/compilers', name: 'VM_DEBUG_BP_CHECK', with: { cppflags: '-DVM_DEBUG_BP_CHECK' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'VM_DEBUG_VERIFY_METHOD_CACHE', with: { cppflags: '-DVM_DEBUG_VERIFY_METHOD_CACHE' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'YJIT_FORCE_ENABLE', with: { cppflags: '-DYJIT_FORCE_ENABLE' }, timeout-minutes: 5 } + - { uses: './.github/actions/compilers', name: 'UNIVERSAL_PARSER', with: { cppflags: '-DUNIVERSAL_PARSER' }, timeout-minutes: 5 } + + compilemax: + name: 'omnibus compilations, result' + runs-on: ubuntu-latest + if: ${{ always() }} + needs: + - 'compile1' + - 'compile2' + - 'compile3' + - 'compile4' + - 'compile5' + - 'compile6' + - 'compile7' + - 'compile8' + - 'compile9' + - 'compileX' + - 'compileB' + - 'compileC' + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github } + - uses: ./.github/actions/slack with: - payload: | - { - "ci": "GitHub Actions", - "env": "${{ github.workflow }} / ${{ matrix.entry.name }}", - "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] - } - env: + label: 'omnibus' SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot - if: ${{ failure() && github.event_name == 'push' }} + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} + - run: false + working-directory: + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} defaults: run: diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml new file mode 100644 index 0000000000..ac73991fe8 --- /dev/null +++ b/.github/workflows/cygwin.yml @@ -0,0 +1,71 @@ +name: Cygwin +on: + push: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + pull_request: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + make: + runs-on: windows-2022 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - run: git config --global core.autocrlf input + + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup Cygwin + uses: cygwin/cygwin-install-action@master + with: + packages: ruby gcc-core make autoconf libtool libssl-devel libyaml-devel libffi-devel zlib-devel rubygems + + - name: configure + run: | + ./autogen.sh + ./configure --disable-install-doc + shell: C:\cygwin\bin\bash.EXE --noprofile --norc -e -o igncr -o pipefail {0} + + - name: Extract bundled gems + run: | + make ruby -j5 + make extract-gems + shell: C:\cygwin\bin\bash.EXE --noprofile --norc -e -o igncr -o pipefail {0} + + - name: make all + timeout-minutes: 30 + run: make -j4 V=1 + shell: C:\cygwin\bin\bash.EXE --noprofile --norc -e -o igncr -o pipefail {0} + + - uses: ./.github/actions/slack + with: + label: Cygwin + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} diff --git a/.github/workflows/default_gems_list.yml b/.github/workflows/default_gems_list.yml new file mode 100644 index 0000000000..1c7e2195c8 --- /dev/null +++ b/.github/workflows/default_gems_list.yml @@ -0,0 +1,99 @@ +name: Update default gems list +on: [push, pull_request, merge_group] + +env: + UPDATE_NEWS_ENABLED: true + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + update_default_gems_list: + name: Update default gems list + + permissions: + contents: write # for Git to git push + + runs-on: ubuntu-latest + + if: ${{ github.repository == 'ruby/ruby' }} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} + + - id: gems + run: true + if: ${{ github.ref == 'refs/heads/master' }} + + - uses: ./.github/actions/setup/directories + with: + makeup: true + # Skip overwriting MATZBOT_AUTO_UPDATE_TOKEN + checkout: '' # false (ref: https://github.com/actions/runner/issues/2238) + if: ${{ steps.gems.outcome == 'success' }} + + - name: Download previous gems list + run: | + data=default_gems.json + mkdir -p .downloaded-cache + ln -s .downloaded-cache/$data . + curl -O -R -z ./$data https://stdgems.org/$data + if: ${{ steps.gems.outcome == 'success' }} + + - name: Make default gems list + run: | + #!ruby + require 'rubygems' + $:.unshift "lib" + rgver = File.foreach("lib/rubygems.rb") do |line| + break $1 if /^\s*VERSION\s*=\s*"([^"]+)"/ =~ line + end + gems = Dir.glob("{ext,lib}/**/*.gemspec").map do |f| + spec = Gem::Specification.load(f) + "#{spec.name} #{spec.version}" + end.sort + File.open("gems/default_gems", "w") do |f| + f.puts "RubyGems #{rgver}" + f.puts gems + end + shell: ruby --disable=gems {0} + if: ${{ steps.gems.outcome == 'success' }} + + - name: Maintain updated gems list in NEWS + run: | + ruby tool/update-NEWS-gemlist.rb default + if: ${{ steps.gems.outcome == 'success' && env.UPDATE_NEWS_ENABLED == 'true' }} + + - name: Check diffs + id: diff + run: | + git diff --color --no-ext-diff --ignore-submodules --exit-code NEWS.md || + echo update=true >> $GITHUB_OUTPUT + if: ${{ steps.gems.outcome == 'success' }} + + - name: Commit + run: | + git pull --ff-only origin ${GITHUB_REF#refs/heads/} + git commit --message="Update default gems list at ${GITHUB_SHA:0:30} [ci skip]" NEWS.md + git push origin ${GITHUB_REF#refs/heads/} + env: + EMAIL: svn-admin@ruby-lang.org + GIT_AUTHOR_NAME: git + GIT_COMMITTER_NAME: git + if: >- + ${{ + github.repository == 'ruby/ruby' && + !startsWith(github.event_name, 'pull') && + steps.diff.outputs.update + }} + + - uses: ./.github/actions/slack + with: + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} diff --git a/.github/workflows/dependabot_automerge.yml b/.github/workflows/dependabot_automerge.yml new file mode 100644 index 0000000000..a95c7005c4 --- /dev/null +++ b/.github/workflows/dependabot_automerge.yml @@ -0,0 +1,32 @@ +# from https://github.com/gofiber/swagger/blob/main/.github/workflows/dependabot_automerge.yml +name: Dependabot auto-merge +on: + pull_request: + +permissions: + contents: write + pull-requests: write + +jobs: + automerge: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'ruby/ruby' + steps: + - name: Dependabot metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + id: metadata + + - name: Wait for status checks + uses: lewagon/wait-on-check-action@3603e826ee561ea102b58accb5ea55a1a7482343 # v1.4.1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ github.event.pull_request.head.sha || github.sha }} + check-regexp: 'make \(check, .*\)' + wait-interval: 30 + + - name: Auto-merge for Dependabot PRs + if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch' }} + run: gh pr merge --auto --rebase "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.MATZBOT_DEPENDABOT_MERGE_TOKEN }} diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000000..16dbac1afa --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,12 @@ +name: "Pull Request Labeler" +on: +- pull_request_target + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v6 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 0000000000..29adcab39a --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,202 @@ +name: macOS +on: + push: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + pull_request: + # Do not use paths-ignore for required status checks + # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + make: + strategy: + matrix: + include: + - test_task: check + os: macos-14 + - test_task: check + os: macos-14 + configure_args: '--with-gcc=gcc-14' + - test_task: check + os: macos-14 + configure_args: '--with-jemalloc --with-opt-dir=$(brew --prefix jemalloc)' + - test_task: check + os: macos-14 + configure_args: '--with-gmp' + - test_task: test-all + test_opts: --repeat-count=2 + os: macos-14 + - test_task: test-bundler-parallel + os: macos-14 + - test_task: test-bundled-gems + os: macos-14 + - test_task: check + os: macos-15 + fail-fast: false + + env: + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + + runs-on: ${{ matrix.os }} + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - name: Install libraries + uses: ./.github/actions/setup/macos + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + clean: true + dummy-files: ${{ matrix.test_task == 'check' }} + # Set fetch-depth: 0 so that Launchable can receive commits information. + fetch-depth: 10 + + - name: make sure that kern.coredump=1 + run: | + sysctl -n kern.coredump + sudo sysctl -w kern.coredump=1 + sudo chmod -R +rwx /cores/ + + - name: Delete unused SDKs + # To free up disk space to not run out during the run + run: | + sudo rm -rf ~/.dotnet + sudo rm -rf /Library/Android + sudo rm -rf /Library/Developer/CoreSimulator + continue-on-error: true + + - name: Run configure + run: ../src/configure -C --disable-install-doc ${ruby_configure_args} ${{ matrix.configure_args }} + + - run: make prepare-gems + if: ${{ matrix.test_task == 'test-bundled-gems' }} + + - run: make + + - run: make hello + + - name: runirb + run: | + echo IRB::VERSION | make runirb RUNOPT="-- -f" + + - name: Set test options for skipped tests + run: | + set -x + TESTS="$(echo "${{ matrix.skipped_tests }}" | sed 's| |$$/ -n!/|g;s|^|-n!/|;s|$|$$/|')" + echo "TESTS=${TESTS}" >> $GITHUB_ENV + if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} + + - name: Set up Launchable + id: launchable + uses: ./.github/actions/launchable/setup + with: + os: ${{ matrix.os }} + test-opts: ${{ matrix.test_opts }} + launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} + builddir: build + srcdir: src + continue-on-error: true + timeout-minutes: 3 + + - name: Set extra test options + run: | + echo "TESTS=$TESTS ${{ matrix.test_opts }}" >> $GITHUB_ENV + echo "RUBY_TEST_TIMEOUT_SCALE=10" >> $GITHUB_ENV # With --repeat-count=2, flaky test by timeout occurs frequently for some reason + if: matrix.test_opts + + - name: make ${{ matrix.test_task }} + run: | + test -n "${LAUNCHABLE_STDOUT}" && exec 1> >(tee "${LAUNCHABLE_STDOUT}") + test -n "${LAUNCHABLE_STDERR}" && exec 2> >(tee "${LAUNCHABLE_STDERR}") + + ulimit -c unlimited + make -s ${{ matrix.test_task }} ${TESTS:+TESTS="$TESTS"} + timeout-minutes: 90 + env: + RUBY_TESTOPTS: '-q --tty=no' + TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' + PRECHECK_BUNDLED_GEMS: 'no' + LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} + LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} + + - name: make skipped tests + run: | + make -s test-all TESTS="${TESTS//-n!\//-n/}" + env: + GNUMAKEFLAGS: '' + RUBY_TESTOPTS: '-v --tty=no' + PRECHECK_BUNDLED_GEMS: 'no' + if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} + continue-on-error: ${{ matrix.continue-on-skipped_tests || false }} + + - name: CAPI extensions + uses: ./.github/actions/capiext + with: + builddir: build + env: + RUBY_TESTOPTS: '-v --tty=no' + if: ${{ contains(matrix.extra_checks, 'capi') }} + + - uses: ./.github/actions/slack + with: + label: ${{ matrix.os }} / ${{ matrix.test_task }} + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + + - name: Resolve job ID + id: job_id + uses: actions/github-script@main + env: + matrix: ${{ toJson(matrix) }} + with: + script: | + const { data: workflow_run } = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.runId + }); + const matrix = JSON.parse(process.env.matrix); + const job_name = `${context.job}${matrix ? ` (${Object.values(matrix).join(", ")})` : ""}`; + return workflow_run.jobs.find((job) => job.name === job_name).id; + + result: + if: ${{ always() }} + name: ${{ github.workflow }} result + runs-on: macos-latest + needs: [make] + steps: + - run: exit 1 + working-directory: + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} + +defaults: + run: + working-directory: build diff --git a/.github/workflows/mingw.yml b/.github/workflows/mingw.yml index 01fef582c5..5c639ad48b 100644 --- a/.github/workflows/mingw.yml +++ b/.github/workflows/mingw.yml @@ -3,148 +3,241 @@ on: push: paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' pull_request: paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' + merge_group: concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} +permissions: + contents: read + # Notes: # Actions console encoding causes issues, see test-all & test-spec steps # jobs: make: - runs-on: windows-2022 + runs-on: windows-${{ matrix.os }} + name: ${{ github.workflow }} (${{ matrix.msystem }}) + env: MSYSTEM: ${{ matrix.msystem }} - MSYS2_ARCH: x86_64 - CHOST: "x86_64-w64-mingw32" - CFLAGS: "-march=x86-64 -mtune=generic -O3 -pipe -fstack-protector-strong" - CXXFLAGS: "-march=x86-64 -mtune=generic -O3 -pipe" - CPPFLAGS: "-D_FORTIFY_SOURCE=2 -D__USE_MINGW_ANSI_STDIO=1 -DFD_SETSIZE=2048" - LDFLAGS: "-pipe -fstack-protector-strong" - UPDATE_UNICODE: "UNICODE_FILES=. UNICODE_PROPERTY_FILES=. UNICODE_AUXILIARY_FILES=. UNICODE_EMOJI_FILES=." - GITPULLOPTIONS: --no-tags origin ${{github.ref}} + MSYS2_ARCH: >- + ${{ + contains(matrix.msystem, 'arm64') && 'aarch64' || + contains(matrix.msystem, '64') && 'x86_64' || 'i686' + }} + MINGW_PACKAGE_PREFIX: >- + mingw-w${{ + endsWith(matrix.msystem, '64') && '64' || '32' + }}-${{ + startsWith(matrix.msystem, 'clang') && 'clang' || + startsWith(matrix.msystem, 'ucrt') && 'ucrt' || + 'mingw' + }}-${{ + contains(matrix.msystem, 'arm64') && 'aarch64' || + endsWith(matrix.msystem, '64') && 'x86_64' || 'i686' + }} + CFLAGS: '-mtune=generic -O3 -pipe' + CXXFLAGS: '-mtune=generic -O3 -pipe' + CPPFLAGS: '-D_FORTIFY_SOURCE=2 -D__USE_MINGW_ANSI_STDIO=1 -DFD_SETSIZE=2048' + LDFLAGS: '-pipe' + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + strategy: matrix: include: - - msystem: "MINGW64" - base_ruby: 2.6 - test_task: [ "check" ] # to make job names consistent - - msystem: "UCRT64" - base_ruby: head - test_task: [ "check" ] # to make job names consistent + # To mitigate flakiness of MinGW CI, we test only one runtime that newer MSYS2 uses. + # Ruby 3.2 is the first Windows Ruby to use OpenSSL 3.x + - msystem: 'UCRT64' + os: 2022 + test_task: 'check' + test-all-opts: '--name=!/TestObjSpace#test_reachable_objects_during_iteration/' + - msystem: 'CLANGARM64' + os: 11-arm + test_task: 'check' fail-fast: false - if: ${{ !startsWith(github.event.head_commit.message, '[DOC]') && !contains(github.event.pull_request.labels.*.name, 'Documentation') }} + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + steps: - - run: mkdir build - working-directory: - - name: git config - run: | - git config --global core.autocrlf false - git config --global core.eol lf - git config --global advice.detachedHead 0 - git config --global init.defaultBranch garbage - - uses: actions/checkout@v2 - with: - path: src - - uses: actions/cache@v2 - with: - path: src/.downloaded-cache - key: downloaded-cache - - name: Set up Ruby & MSYS2 - uses: ruby/setup-ruby@v1 + - uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2.30.0 + id: msys2 with: - ruby-version: ${{ matrix.base_ruby }} - - name: set env + msystem: ${{ matrix.msystem }} + update: true + install: >- + git + make + ruby + autoconf + ${{ env.MINGW_PACKAGE_PREFIX }}-gcc + ${{ env.MINGW_PACKAGE_PREFIX }}-ragel + ${{ env.MINGW_PACKAGE_PREFIX }}-openssl + ${{ env.MINGW_PACKAGE_PREFIX }}-libyaml + ${{ env.MINGW_PACKAGE_PREFIX }}-libffi + + - name: Set up env + id: setup-env + working-directory: run: | - echo "GNUMAKEFLAGS=-j$((2 * NUMBER_OF_PROCESSORS))" >> $GITHUB_ENV - echo "TEST_JOBS=$((15 * NUMBER_OF_PROCESSORS / 10))" >> $GITHUB_ENV + $msys2 = ${env:MSYS2_LOCATION} + $msystem = ${env:MSYSTEM}.ToLower() + echo $msys2\usr\bin $msys2\$msystem\bin | + Tee-Object ${env:GITHUB_PATH} -Append -Encoding utf-8 - - name: where check + # Use the fast device for the temporary directory. + # %TEMP% is inconsistent with %TMP% and test-all expects they are consistent. + # https://github.com/actions/virtual-environments/issues/712#issuecomment-613004302 + $tmp = ${env:RUNNER_TEMP} + echo HOME=$home TMP=$tmp TEMP=$tmp TMPDIR=$tmp | + Tee-Object ${env:GITHUB_ENV} -Append -Encoding utf-8 + shell: pwsh # cmd.exe does not strip spaces before `|`. + env: + MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }} + MSYSTEM: ${{ matrix.msystem }} + + - name: Remove Strawberry Perl pkg-config + working-directory: + # `pkg-config.bat` included in Strawberry Perl is written in + # Perl and doesn't work when another msys2 `perl` precede its + # own `perl`. + # + # ``` + # Can't find C:\Strawberry\perl\bin\pkg-config.bat on PATH, '.' not in PATH. + # ``` run: | - # show where - mv /c/Windows/System32/libcrypto-1_1-x64.dll /c/Windows/System32/libcrypto-1_1-x64.dll_ - mv /c/Windows/System32/libssl-1_1-x64.dll /c/Windows/System32/libssl-1_1-x64.dll_ - result=true - for e in gcc.exe ragel.exe make.exe bison.exe libcrypto-1_1-x64.dll libssl-1_1-x64.dll; do - echo '##['group']'$'\033[93m'$e$'\033[m' - where $e || result=false - echo '##['endgroup']' - done - $result - - - name: autogen + Get-Command pkg-config.bat | % { ren $_.path ($_.path + "~") } + shell: pwsh + + - name: Misc system & package info + working-directory: run: | - ./autogen.sh - working-directory: src + group() { echo ::group::$'\e[94;1m'"$*"$'\e[m'; } + endgroup() { echo ::endgroup::; } + + group Path + cygpath -wa / . $(type -p cygpath bash sh) + endgroup + + I() { + group $1 + run Where type -pa $1 && { [ $# -eq 1 ] || run Version "$@"; } || + failed+=($1) + endgroup + } + run() { local w m=$1; shift; w="$("$@")" && show "$m" && indent "$w"; } + indent() { [ -z "$1" ] || echo "$1" | /bin/sed '/^$/!s/^/ /'; } + show() { echo $'\e[96m'"$*"$'\e[m'; } + + failed=() + + I gcc.exe --version + I ragel.exe --version + I make.exe --version + I openssl.exe version + I libcrypto-3-x64.dll + I libssl-3-x64.dll + + group Packages + pacman -Qs $MINGW_PACKAGE_PREFIX-* | /bin/sed -n "s,local/$MINGW_PACKAGE_PREFIX-,,p" + endgroup + + [ ${#failed[@]} -eq 0 ] + shell: sh + + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + # Set fetch-depth: 10 so that Launchable can receive commits information. + fetch-depth: 10 - name: configure run: > ../src/configure --disable-install-doc --prefix=/. --build=$CHOST --host=$CHOST --target=$CHOST - - - name: update - run: | - make incs - - - name: download gems - run: | - make update-gems + shell: sh + env: + CHOST: ${{ env.MSYS2_ARCH }}-w64-mingw32 - name: make all - timeout-minutes: 20 - run: | - make - - - run: make leaked-globals + timeout-minutes: 30 + run: make -j4 - name: make install - run: | - make DESTDIR=../install install-nodoc + run: make DESTDIR=../install install-nodoc + + - name: Set up Launchable + uses: ./.github/actions/launchable/setup + with: + os: windows-2022 + launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} + builddir: build + srcdir: src + test-tasks: '["test", "test-all", "test-spec"]' + continue-on-error: true + timeout-minutes: 3 - name: test - timeout-minutes: 5 - run: | - make test + timeout-minutes: 30 + run: make test test-tool + env: + GNUMAKEFLAGS: '' + RUBY_TESTOPTS: '-v --tty=no' + if: ${{ matrix.test_task == 'check' || matrix.test_task == 'test' }} - name: test-all timeout-minutes: 45 run: | - # Actions uses UTF8, causes test failures, similar to normal OS setup - chcp.com 437 - make test-all + make ${{ StartsWith(matrix.test_task, 'test/') && matrix.test_task || 'test-all' }} env: - RUBY_TESTOPTS: -j${{env.TEST_JOBS}} --retry --job-status=normal --show-skip --timeout-scale=1.5 + RUBY_TESTOPTS: >- + --retry --job-status=normal --show-skip --timeout-scale=1.5 -j4 + ${{ matrix.test-all-opts }} + ${{ env.TESTS }} BUNDLER_VERSION: + if: ${{ matrix.test_task == 'check' || matrix.test_task == 'test-all' || StartsWith(matrix.test_task, 'test/') }} - name: test-spec timeout-minutes: 10 run: | - make test-spec + make ${{ StartsWith(matrix.test_task, 'spec/') && matrix.test_task || 'test-spec' }} + if: ${{ matrix.test_task == 'check' || matrix.test_task == 'test-spec' || StartsWith(matrix.test_task, 'spec/') }} - - uses: k0kubun/action-slack@v2.0.0 + - uses: ./src/.github/actions/slack with: - payload: | - { - "ci": "GitHub Actions", - "env": "${{ github.workflow }} / ${{ matrix.test_task }}", - "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] - } - env: + label: ${{ matrix.msystem }} / ${{ matrix.test_task }} SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot - if: ${{ failure() && github.event_name == 'push' }} + if: ${{ failure() }} defaults: run: working-directory: build - shell: sh + shell: cmd diff --git a/.github/workflows/mjit.yml b/.github/workflows/mjit.yml deleted file mode 100644 index 75e5b1088c..0000000000 --- a/.github/workflows/mjit.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: MJIT -on: - push: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' - pull_request: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' - -concurrency: - group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} - cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} - -jobs: - make: - strategy: - matrix: - test_task: [ "check" ] # to make job names consistent - jit_opts: [ "--mjit", "--mjit-wait" ] - fail-fast: false - runs-on: ubuntu-latest - if: ${{ !startsWith(github.event.head_commit.message, '[DOC]') && !contains(github.event.pull_request.labels.*.name, 'Documentation') }} - env: - TESTOPTS: '-q --tty=no' - RUN_OPTS: '--disable-gems ${{ matrix.jit_opts }} --mjit-debug=-ggdb3' - GITPULLOPTIONS: --no-tags origin ${{github.ref}} - steps: - - run: mkdir build - working-directory: - - name: Install libraries - run: | - set -x - sudo apt-get update -q || : - sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev bison autoconf ruby - - name: git config - run: | - git config --global advice.detachedHead 0 - git config --global init.defaultBranch garbage - - uses: actions/checkout@v2 - with: - path: src - - uses: actions/cache@v2 - with: - path: src/.downloaded-cache - key: downloaded-cache - - name: Fixed world writable dirs - run: | - chmod -v go-w $HOME $HOME/.config - sudo chmod -R go-w /usr/share - sudo bash -c 'IFS=:; for d in '"$PATH"'; do chmod -v go-w $d; done' || : - - name: Set ENV - run: | - echo "GNUMAKEFLAGS=-j$((1 + $(nproc --all)))" >> $GITHUB_ENV - - run: ./autogen.sh - working-directory: src - - name: Run configure - run: ../src/configure -C --disable-install-doc cppflags=-DVM_CHECK_MODE - - run: make incs - - run: make - - run: sudo make -s install - - run: sudo apt-get install gdb # used by test / test-all failure - - name: Run test - run: | - ulimit -c unlimited - make -s test RUN_OPTS="$RUN_OPTS" - timeout-minutes: 60 - - name: Run test-all - run: | - ulimit -c unlimited - make -s test-all RUN_OPTS="$RUN_OPTS" - timeout-minutes: 60 - - name: Run test-spec - run: | - ulimit -c unlimited - make -s test-spec RUN_OPTS="$RUN_OPTS" - timeout-minutes: 60 - - uses: k0kubun/action-slack@v2.0.0 - with: - payload: | - { - "ci": "GitHub Actions", - "env": "${{ github.workflow }} / ${{ matrix.test_task }} ${{ matrix.jit_opts }}", - "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot - if: ${{ failure() && github.event_name == 'push' }} - -defaults: - run: - working-directory: build diff --git a/.github/workflows/modgc.yml b/.github/workflows/modgc.yml new file mode 100644 index 0000000000..1d14934df8 --- /dev/null +++ b/.github/workflows/modgc.yml @@ -0,0 +1,176 @@ +name: ModGC +on: + push: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + pull_request: + # Do not use paths-ignore for required status checks + # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + check: + strategy: + matrix: + gc: + - name: default + - name: mmtk + mmtk_build: release + os: [macos-latest, ubuntu-latest] + include: + - test_task: check + fail-fast: false + + env: + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + RUBY_DEBUG: ci + + runs-on: ${{ matrix.os }} + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - name: Install libraries (macOS) + uses: ./.github/actions/setup/macos + if: ${{ contains(matrix.os, 'macos') }} + + - name: Install libraries (Ubuntu) + uses: ./.github/actions/setup/ubuntu + if: ${{ contains(matrix.os, 'ubuntu') }} + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 + with: + ruby-version: '3.1' + bundler: none + if: ${{ contains(matrix.os, 'ubuntu') }} + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + clean: true + dummy-files: false + # Set fetch-depth: 10 so that Launchable can receive commits information. + fetch-depth: 10 + + - name: make sure that kern.coredump=1 + run: | + sysctl -n kern.coredump + sudo sysctl -w kern.coredump=1 + sudo chmod -R +rwx /cores/ + if: ${{ contains(matrix.os, 'macos') }} + + - name: Delete unused SDKs + # To free up disk space to not run out during the run + run: | + sudo rm -rf ~/.dotnet + sudo rm -rf /Library/Android + sudo rm -rf /Library/Developer/CoreSimulator + continue-on-error: true + if: ${{ contains(matrix.os, 'macos') }} + + - name: Setup Ruby GC Directory + run: | + echo "MODULAR_GC_DIR=$HOME/ruby_gc" >> $GITHUB_ENV + + - name: Run configure + env: + arch: ${{ matrix.arch }} + run: >- + $SETARCH ../src/configure -C --disable-install-doc --with-modular-gc=${{ env.MODULAR_GC_DIR }} + ${arch:+--target=$arch-$OSTYPE --host=$arch-$OSTYPE} + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Set MMTk environment variables + run: | + echo 'EXCLUDES=../src/test/.excludes-mmtk' >> $GITHUB_ENV + echo 'MSPECOPT=-B../src/spec/mmtk.mspec' >> $GITHUB_ENV + if: ${{ matrix.gc.name == 'mmtk' }} + + - run: $SETARCH make + + - name: Build Modular GC + run: | + echo "RUBY_GC_LIBRARY=${{ matrix.gc.name }}" >> $GITHUB_ENV + make install-modular-gc MODULAR_GC=${{ matrix.gc.name }} MMTK_BUILD=${{ matrix.gc.mmtk_build }} + make distclean-modular-gc MODULAR_GC=${{ matrix.gc.name }} + + - run: $SETARCH make hello + + - name: Set test options for skipped tests + run: | + set -x + TESTS="$(echo "${{ matrix.skipped_tests }}" | sed 's| |$$/ -n!/|g;s|^|-n!/|;s|$|$$/|')" + echo "TESTS=${TESTS}" >> $GITHUB_ENV + if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} + + - name: Set up Launchable + id: launchable + uses: ./.github/actions/launchable/setup + with: + os: ${{ matrix.os || 'ubuntu-22.04' }} + test-opts: ${{ matrix.configure }} + launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} + builddir: build + srcdir: src + continue-on-error: true + timeout-minutes: 3 + + - name: make ${{ matrix.test_task }} + run: | + test -n "${LAUNCHABLE_STDOUT}" && exec 1> >(tee "${LAUNCHABLE_STDOUT}") + test -n "${LAUNCHABLE_STDERR}" && exec 2> >(tee "${LAUNCHABLE_STDERR}") + + $SETARCH make -s ${{ matrix.test_task }} \ + ${TESTS:+TESTS="$TESTS"} \ + ${{ !contains(matrix.test_task, 'bundle') && 'RUBYOPT=-w' || '' }} + timeout-minutes: ${{ matrix.gc.timeout || 40 }} + env: + RUBY_TESTOPTS: '-q --tty=no' + TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' + PRECHECK_BUNDLED_GEMS: 'no' + LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} + LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} + + - name: make skipped tests + run: | + $SETARCH make -s test-all TESTS="${TESTS//-n!\//-n/}" + env: + GNUMAKEFLAGS: '' + RUBY_TESTOPTS: '-v --tty=no' + if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} + continue-on-error: ${{ matrix.continue-on-skipped_tests || false }} + + - uses: ./.github/actions/slack + with: + label: ${{ matrix.test_task }} ${{ matrix.configure }}${{ matrix.arch }} + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + +defaults: + run: + working-directory: build diff --git a/.github/workflows/parse_y.yml b/.github/workflows/parse_y.yml new file mode 100644 index 0000000000..87facc8a55 --- /dev/null +++ b/.github/workflows/parse_y.yml @@ -0,0 +1,100 @@ +name: parse.y +on: + push: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + pull_request: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + make: + strategy: + matrix: + include: + - test_task: check + - test_task: test-bundler-parallel + - test_task: test-bundled-gems + fail-fast: false + + env: + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + RUBY_DEBUG: ci + SETARCH: ${{ matrix.arch && format('setarch {0}', matrix.arch) }} + + runs-on: ubuntu-22.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - uses: ./.github/actions/setup/ubuntu + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 + with: + ruby-version: '3.1' + bundler: none + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + clean: true + dummy-files: ${{ matrix.test_task == 'check' }} + + - name: Run configure + run: ../src/configure -C --disable-install-doc cppflags=-DRUBY_DEBUG --with-parser=parse.y + + - run: make + + - run: make TESTRUN_SCRIPT='-renvutil -v -e "exit EnvUtil.current_parser == %[parse.y]"' run + env: + RUNOPT0: -I$(tooldir)/lib + + - name: make ${{ matrix.test_task }} + run: make -s ${{ matrix.test_task }} RUN_OPTS="$RUN_OPTS" SPECOPTS="$SPECOPTS" + env: + RUBY_TESTOPTS: ${{ matrix.testopts }} + EXCLUDES: '../src/test/.excludes-parsey' + RUN_OPTS: ${{ matrix.run_opts || '--parser=parse.y' }} + SPECOPTS: ${{ matrix.specopts || '-T --parser=parse.y' }} + TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' + + - uses: ./.github/actions/slack + with: + label: ${{ matrix.run_opts }} + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + +defaults: + run: + working-directory: build diff --git a/.github/workflows/post_push.yml b/.github/workflows/post_push.yml new file mode 100644 index 0000000000..318444c0a2 --- /dev/null +++ b/.github/workflows/post_push.yml @@ -0,0 +1,85 @@ +name: Post-push +on: + push: + branches: + - master + - 'ruby_*_*' +jobs: + hooks: + name: Post-push hooks + runs-on: ubuntu-latest + if: ${{ github.repository == 'ruby/ruby' }} + steps: + - name: Sync git.ruby-lang.org + run: | + mkdir -p ~/.ssh + (umask 066; printenv RUBY_GIT_SYNC_PRIVATE_KEY > ~/.ssh/id_ed25519) + ssh-keyscan -t ed25519 git.ruby-lang.org >> ~/.ssh/known_hosts + ssh -i ~/.ssh/id_ed25519 git-sync@git.ruby-lang.org "sudo -u git /home/git/git.ruby-lang.org/bin/update-ruby.sh $GITHUB_REF" + env: + GITHUB_REF: ${{ github.ref }} + RUBY_GIT_SYNC_PRIVATE_KEY: ${{ secrets.RUBY_GIT_SYNC_PRIVATE_KEY }} + if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/ruby_') }} + + - name: Fetch changesets on bugs.ruby-lang.org + run: | + curl "https://bugs.ruby-lang.org/sys/fetch_changesets?key=${REDMINE_SYS_API_KEY}" -s --fail-with-body -w '* status: %{http_code}\n' + env: + REDMINE_SYS_API_KEY: ${{ secrets.REDMINE_SYS_API_KEY }} + if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/ruby_') }} + + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 500 # for notify-slack-commits + token: ${{ secrets.MATZBOT_AUTO_UPDATE_TOKEN }} + + - name: Notify commit to Slack + run: ruby tool/notify-slack-commits.rb "$GITHUB_OLD_SHA" "$GITHUB_NEW_SHA" refs/heads/master + env: + GITHUB_OLD_SHA: ${{ github.event.before }} + GITHUB_NEW_SHA: ${{ github.event.after }} + SLACK_WEBHOOK_URL_ALERTS: ${{ secrets.SLACK_WEBHOOK_URL_ALERTS }} + SLACK_WEBHOOK_URL_COMMITS: ${{ secrets.SLACK_WEBHOOK_URL_COMMITS }} + SLACK_WEBHOOK_URL_RUBY_JP: ${{ secrets.SLACK_WEBHOOK_URL_RUBY_JP }} + if: ${{ github.ref == 'refs/heads/master' }} + + - name: Notify commit to ruby-cvs + run: | + SENDMAIL="ssh -i ${HOME}/.ssh/id_ed25519 git-sync@git.ruby-lang.org /usr/sbin/sendmail" \ + ruby tool/commit-email.rb . ruby-cvs@g.ruby-lang.org \ + "$GITHUB_OLD_SHA" "$GITHUB_NEW_SHA" "$GITHUB_REF" \ + --viewer-uri "https://github.com/ruby/ruby/commit/" \ + --error-to cvs-admin@ruby-lang.org + env: + GITHUB_OLD_SHA: ${{ github.event.before }} + GITHUB_NEW_SHA: ${{ github.event.after }} + GITHUB_REF: ${{ github.ref }} + if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/ruby_') }} + + - name: Auto-correct code styles + run: | + set -x + ruby tool/auto-style.rb "$GITHUB_OLD_SHA" "$GITHUB_NEW_SHA" refs/heads/master + env: + GITHUB_OLD_SHA: ${{ github.event.before }} + GITHUB_NEW_SHA: ${{ github.event.after }} + GIT_AUTHOR_NAME: git + GIT_COMMITTER_NAME: git + EMAIL: svn-admin@ruby-lang.org + if: ${{ github.ref == 'refs/heads/master' }} + + - name: Push PR notes to GitHub + run: ruby tool/notes-github-pr.rb "$(pwd)/.git" "$GITHUB_OLD_SHA" "$GITHUB_NEW_SHA" refs/heads/master + env: + GITHUB_OLD_SHA: ${{ github.event.before }} + GITHUB_NEW_SHA: ${{ github.event.after }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_NAME: git + GIT_COMMITTER_NAME: git + EMAIL: svn-admin@ruby-lang.org + if: ${{ github.ref == 'refs/heads/master' }} + + - uses: ./.github/actions/slack + with: + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} diff --git a/.github/workflows/pr-playground.yml b/.github/workflows/pr-playground.yml new file mode 100644 index 0000000000..f3c0556429 --- /dev/null +++ b/.github/workflows/pr-playground.yml @@ -0,0 +1,127 @@ +name: Post Playground link to PR +on: + pull_request_target: + types: [labeled] + workflow_run: + workflows: ["WebAssembly"] + types: [completed] + +jobs: + post-summary: + name: Post Playground link + runs-on: ubuntu-latest + permissions: + pull-requests: write + # Post a comment only if the PR status check is passed and the PR is labeled with `Playground`. + # Triggered twice: when the PR is labeled and when PR build is passed. + if: >- + ${{ false + || (true + && github.event_name == 'pull_request_target' + && contains(github.event.pull_request.labels.*.name, 'Playground')) + || (true + && github.event_name == 'workflow_run' + && github.event.workflow_run.conclusion == 'success' + && github.event.workflow_run.event == 'pull_request') + }} + steps: + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs/promises'); + + const buildWorkflowPath = '.github/workflows/wasm.yml'; + const findSuccessfuBuildRun = async (pr) => { + const opts = github.rest.actions.listWorkflowRunsForRepo.endpoint.merge({ + owner: context.repo.owner, + repo: context.repo.repo, + status: 'success', + branch: pr.head.ref, + }); + const runs = await github.paginate(opts); + const buildRun = runs.find(run => run.path == buildWorkflowPath); + return buildRun; + } + + const postComment = async (body, pr) => { + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + }); + + const commentOpts = { owner: context.repo.owner, repo: context.repo.repo, body: comment }; + + const existingComment = comments.find(comment => comment.body.startsWith(magicComment)); + if (existingComment) { + core.info(`Updating existing comment: ${existingComment.html_url}`); + await github.rest.issues.updateComment({ + ...commentOpts, comment_id: existingComment.id + }); + } else { + await github.rest.issues.createComment({ + ...commentOpts, issue_number: pr.number + }); + } + } + + const derivePRNumber = async () => { + if (context.payload.pull_request) { + return context.payload.pull_request.number; + } + // Workaround for https://github.com/orgs/community/discussions/25220 + + const { data: { artifacts } } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + const artifact = artifacts.find(artifact => artifact.name == 'github-pr-info'); + if (!artifact) { + throw new Error('Cannot find github-pr-info.txt artifact'); + } + + const { data } = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: artifact.id, + archive_format: 'zip', + }); + + await fs.writeFile('pr-info.zip', Buffer.from(data)); + await exec.exec('unzip', ['pr-info.zip']); + return await fs.readFile('github-pr-info.txt', 'utf8'); + } + + const prNumber = await derivePRNumber(); + + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + + core.info(`Checking if the PR ${prNumber} is labeled with Playground...`); + if (!pr.labels.some(label => label.name == 'Playground')) { + core.info(`The PR is not labeled with Playground.`); + return; + } + + core.info(`Checking if the build is successful for ${pr.head.ref} in ${pr.head.repo.owner.login}/${pr.head.repo.name}...`); + const buildRun = await findSuccessfuBuildRun(pr); + if (!buildRun) { + core.info(`No successful build run found for ${buildWorkflowPath} on ${pr.head.ref} yet.`); + return; + } + core.info(`Found a successful build run: ${buildRun.html_url}`); + + const runLink = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; + const magicComment = `<!-- AUTO-GENERATED-COMMENT-PR-PLAYGROUND -->`; + const comment = `${magicComment} + **Try on Playground**: https://ruby.github.io/play-ruby?run=${buildRun.id} + This is an automated comment by [\`pr-playground.yml\`](${runLink}) workflow. + `; + core.info(`Comment: ${comment}`); + await postComment(comment, pr); + diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..3caeee9a3b --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,107 @@ +name: Publish Ruby packages + +on: + repository_dispatch: + types: + - release + workflow_dispatch: + inputs: + version: + description: 'Version of the Ruby package to release' + required: true + default: '3.3.4' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.1 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3.4 + + - name: Store Ruby version + run: | + echo "RUBY_VERSION=${{ github.event.client_payload.version || github.event.inputs.version }}" >> $GITHUB_ENV + + - name: Store ABI version + run: echo "ABI_VERSION=$(echo ${{ env.RUBY_VERSION }} | cut -d '.' -f 1-2)" >> $GITHUB_ENV + + - name: Copy draft package `/tmp` to `/pub` directory + run: tool/release.sh ${{ env.RUBY_VERSION }} + env: + AWS_ACCESS_KEY_ID: ${{ secrets.FTP_R_L_O_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.FTP_R_L_O_AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: us-west-2 + + - name: Purge URLs of release package + run: | + curl -X POST \ + -H "Fastly-Key: ${{ secrets.FASTLY_PURGE_TOKEN }}" \ + https://api.fastly.com/purge/cache.ruby-lang.org/pub/ruby/${{ env.ABI_VERSION }}/ruby-${{ env.RUBY_VERSION }}.tar.gz + curl -X POST \ + -H "Fastly-Key: ${{ secrets.FASTLY_PURGE_TOKEN }}" \ + https://api.fastly.com/purge/cache.ruby-lang.org/pub/ruby/${{ env.ABI_VERSION }}/ruby-${{ env.RUBY_VERSION }}.tar.xz + curl -X POST \ + -H "Fastly-Key: ${{ secrets.FASTLY_PURGE_TOKEN }}" \ + https://api.fastly.com/purge/cache.ruby-lang.org/pub/ruby/${{ env.ABI_VERSION }}/ruby-${{ env.RUBY_VERSION }}.zip + + - name: Create a release on GitHub + run: | + RELEASE_TAG=$(ruby tool/ruby-version.rb tag "${{ env.RUBY_VERSION }}") + echo $RELEASE_TAG + PREVIOUS_RELEASE_TAG=$(ruby tool/ruby-version.rb previous-tag "${{ env.RUBY_VERSION }}") + echo $PREVIOUS_RELEASE_TAG + tool/gen-github-release.rb $PREVIOUS_RELEASE_TAG $RELEASE_TAG --no-dry-run + env: + GITHUB_TOKEN: ${{ secrets.MATZBOT_AUTO_UPDATE_TOKEN }} + + - name: Update versions index + run: | + curl -L -X POST \ + -H "Authorization: Bearer ${{ secrets.MATZBOT_GITHUB_WORKFLOW_TOKEN }}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/ruby/actions/dispatches \ + -d '{"event_type": "update_index"}' + + - name: Build and push Docker images + run: | + curl -L -X POST \ + -H "Authorization: Bearer ${{ secrets.MATZBOT_GITHUB_WORKFLOW_TOKEN }}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/ruby/docker-images/actions/workflows/build.yml/dispatches \ + -d '{"ref": "master", "inputs": {"ruby_version": "${{ env.RUBY_VERSION }}"}}' + + - name: Build snapcraft packages + run: | + curl -L -X POST \ + -H "Authorization: Bearer ${{ secrets.MATZBOT_GITHUB_WORKFLOW_TOKEN }}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/ruby/snap.ruby/dispatches \ + -d '{"event_type": "build", "client_payload": {"ruby_version": "${{ env.RUBY_VERSION }}"}}' + + - name: Store the latest LTS version of OpenSSL + run: | + echo "OPENSSL_VERSION=`curl -s https://api.github.com/repos/openssl/openssl/releases | jq -r '.[].tag_name | select(startswith("openssl-3.0"))' | sort -Vr | head -n1 | cut -d'-' -f2`" >> $GITHUB_ENV + + - name: Update ruby-build definition + run: | + curl -L -X POST \ + -H "Authorization: Bearer ${{ secrets.RUBY_BUILD_WORKFLOW_TOKEN }}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/rbenv/ruby-build/dispatches \ + -d '{"event_type": "update-ruby", "client_payload": {"ruby_version": "${{ env.RUBY_VERSION }}", "openssl_version": "${{ env.OPENSSL_VERSION }}"}}' + + - name: Update all-ruby definition + run: | + curl -L -X POST \ + -H "Authorization: Bearer ${{ secrets.MATZBOT_GITHUB_WORKFLOW_TOKEN }}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/ruby/all-ruby/dispatches \ + -d '{"event_type": "update"}' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..5d4474d978 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +name: Start release workflow +on: + push: + tags: + - '*' + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Build release package + run: | + curl -L -X POST \ + -H "Authorization: Bearer ${{ secrets.MATZBOT_GITHUB_WORKFLOW_TOKEN }}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/ruby/actions/dispatches \ + -d '{"event_type": "${{ github.ref }}"}' diff --git a/.github/workflows/rust-warnings.yml b/.github/workflows/rust-warnings.yml new file mode 100644 index 0000000000..a2e3208e52 --- /dev/null +++ b/.github/workflows/rust-warnings.yml @@ -0,0 +1,60 @@ +# Surface Rust warnings on PRs that touch any Rust code. +# Not a required check so we never block people over new warnings +# that might come from a new Rust version being released. +name: Rust warnings +on: + pull_request: + types: + - opened + - synchronize + - reopened + paths: + - '**.rs' + - '!**.inc.rs' + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + rust-warnings: + env: + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + + runs-on: ubuntu-24.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Install Rust + run: rustup default beta + + - name: Rust warnings + shell: bash + run: | + set -eu + cargo check --quiet --all-features --message-format=json \ + | jq -r 'select(.message.level | IN("warning", "error")) | .message.rendered' \ + | tee messages.txt + (exit "${PIPESTATUS[0]}") && ! grep --quiet '[^[:space:]]' messages.txt + + - name: "📜 `rustdoc` warnings" + shell: bash + run: | + set -eu + cargo doc --document-private-items --all --no-deps --message-format=json \ + | jq -r 'select(.message.level | IN("warning", "error")) | .message.rendered' \ + | tee messages.txt + (exit "${PIPESTATUS[0]}") && ! grep --quiet '[^[:space:]]' messages.txt diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml new file mode 100644 index 0000000000..c607098997 --- /dev/null +++ b/.github/workflows/scorecards.yml @@ -0,0 +1,78 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '39 3 * * 5' + # push: + # branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. + if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore + # file_mode: git + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: results.sarif diff --git a/.github/workflows/spec_guards.yml b/.github/workflows/spec_guards.yml index 3f829650d5..cf4661555c 100644 --- a/.github/workflows/spec_guards.yml +++ b/.github/workflows/spec_guards.yml @@ -2,52 +2,66 @@ name: Rubyspec Version Guards Check on: push: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' + paths: + - '.github/workflows/spec_guards.yml' + - 'spec/**' + - '!spec/*.md' pull_request: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' + paths: + - '.github/workflows/spec_guards.yml' + - 'spec/**' + - '!spec/*.md' + merge_group: concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} +permissions: + contents: read + jobs: rubyspec: name: Rubyspec - runs-on: ubuntu-20.04 - if: ${{ !startsWith(github.event.head_commit.message, '[DOC]') && !contains(github.event.pull_request.labels.*.name, 'Documentation') }} + + runs-on: ubuntu-22.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + strategy: matrix: # Specs from ruby/spec should still run on all supported Ruby versions. # This also ensures the needed ruby_version_is guards are there, see spec/README.md. ruby: - - ruby-2.7 - - ruby-3.0 + - ruby-3.2 + - ruby-3.3 + - ruby-3.4 + - ruby-4.0 + fail-fast: false steps: - - uses: actions/checkout@v2 - - uses: ruby/setup-ruby@v1 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 with: ruby-version: ${{ matrix.ruby }} bundler: none + - run: gem install webrick + - run: ruby ../mspec/bin/mspec working-directory: spec/ruby - - uses: k0kubun/action-slack@v2.0.0 - with: - payload: | - { - "ci": "GitHub Actions", - "env": "${{ github.workflow }} / rubyspec @ ${{ matrix.ruby }}", - "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] - } env: + CHECK_LEAKS: true + + - uses: ./.github/actions/slack + with: + label: ${{ matrix.ruby }} SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot - if: ${{ failure() && github.event_name == 'push' }} + if: ${{ failure() }} diff --git a/.github/workflows/sync_default_gems.yml b/.github/workflows/sync_default_gems.yml new file mode 100644 index 0000000000..9ff97d5a4e --- /dev/null +++ b/.github/workflows/sync_default_gems.yml @@ -0,0 +1,77 @@ +name: Sync default gems + +env: + DEFAULT_GEM_SYNC_ENABLED: true + +on: + workflow_dispatch: + inputs: + gem: + required: true + description: 'Name of the gem to be synchronized' + type: string + before: + required: true + description: 'Gem commit SHA before sync' + type: string + after: + required: true + description: 'Gem commit SHA after sync' + type: string + +jobs: + sync_default_gems: + name: Sync default gem ${{ github.event.inputs.gem }} + + permissions: + contents: write # for Git to git push + + runs-on: ubuntu-latest + + if: ${{ github.repository == 'ruby/ruby' }} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + name: Check out ruby/ruby + with: + token: ${{ github.repository == 'ruby/ruby' && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 + with: + ruby-version: '3.4' + bundler: none + + - name: Run tool/sync_default_gems.rb + id: sync + run: | + ruby_before=$(git rev-parse HEAD) + set -x + ruby tool/sync_default_gems.rb "${gem_name}" "${gem_before}..${gem_after}" + if [[ "$(git rev-parse HEAD)" != "$ruby_before" ]]; then + echo update=true >> $GITHUB_OUTPUT + fi + env: + gem_name: ${{ github.event.inputs.gem }} + gem_before: ${{ github.event.inputs.before }} + gem_after: ${{ github.event.inputs.after }} + EMAIL: svn-admin@ruby-lang.org + GIT_AUTHOR_NAME: git + GIT_COMMITTER_NAME: git + + - name: Push + run: | + git pull --rebase origin ${GITHUB_REF#refs/heads/} + git push origin ${GITHUB_REF#refs/heads/} + if: ${{ steps.sync.outputs.update && env.DEFAULT_GEM_SYNC_ENABLED == 'true' }} + env: + EMAIL: svn-admin@ruby-lang.org + GIT_AUTHOR_NAME: git + GIT_COMMITTER_NAME: git + + - uses: ./.github/actions/slack + with: + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + label: "${{ github.event.inputs.gem }} (<https://github.com/${{ github.event.inputs.gem == 'rubygems' && 'rubygems' || 'ruby' }}/${{ github.event.inputs.gem }}/compare/${{ github.event.inputs.before }}...${{ github.event.inputs.after }}|diff>)" + event_name: workflow_dispatch + extra_channel_id: C05FPKAU743 # alerts-sync + if: ${{ failure() }} diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index ff79dad880..88c19b6fe6 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -3,126 +3,266 @@ on: push: paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' pull_request: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' + # Do not use paths-ignore for required status checks + # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks + merge_group: concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} +permissions: + contents: read + jobs: make: strategy: matrix: - test_task: ["check", "test-bundler-parallel", "test-bundled-gems"] - os: - - ubuntu-20.04 -# - ubuntu-18.04 - configure: ["", "cppflags=-DRUBY_DEBUG"] + # We enumerate every job in matrix.include to save build time include: - - test_task: "check" - os: ubuntu-20.04 - configure: "--host=i686-$OSTYPE" - - test_task: "test-all TESTS=--repeat-count=2" - os: ubuntu-20.04 - configure: "" + - test_task: check + configure: 'cppflags=-DVM_CHECK_MODE' + - test_task: check + arch: i686 + - test_task: check + configure: '--disable-yjit' + - test_task: check + configure: '--enable-shared --enable-load-relative' + - test_task: test-bundler-parallel + timeout: 50 + - test_task: test-bundled-gems + - test_task: check + os: ubuntu-24.04 + extra_checks: [capi] + # ubuntu-24.04-arm jobs don't start on ruby/ruby as of 2025-10-29 + #- test_task: check + # os: ubuntu-24.04-arm fail-fast: false - env: - GITPULLOPTIONS: --no-tags origin ${{github.ref}} + + env: &make-env + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} RUBY_DEBUG: ci - runs-on: ${{ matrix.os }} - if: ${{ !startsWith(github.event.head_commit.message, '[DOC]') && !contains(github.event.pull_request.labels.*.name, 'Documentation') }} - steps: - - run: mkdir build - working-directory: - - name: Set ENV - env: - configure: ${{matrix.configure}} - run: | - echo "GNUMAKEFLAGS=-j$((1 + $(nproc --all)))" >> $GITHUB_ENV - arch=`echo " $configure" | sed '/.* --host=/!d;s///;s/[- ].*//'` - echo "SETARCH=${arch:+setarch $arch}" >> $GITHUB_ENV - - name: Install libraries - run: | - set -x - arch="${SETARCH##* }" - arch=${arch:+:${arch/i[3-6]86/i386}} - ${arch:+sudo dpkg --add-architecture ${arch#:}} - sudo apt-get update -q || : - sudo apt-get install --no-install-recommends -q -y \ - ${arch:+cross}build-essential${arch/:/-} \ - libssl-dev${arch} libyaml-dev${arch} libreadline6-dev${arch} \ - zlib1g-dev${arch} libncurses5-dev${arch} libffi-dev${arch} \ - bison autoconf ruby - sudo apt-get install -q -y pkg-config${arch} || : - - name: git config - run: | - git config --global advice.detachedHead 0 - git config --global init.defaultBranch garbage - - uses: actions/checkout@v2 + + runs-on: ${{ matrix.os || 'ubuntu-22.04' }} + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: &make-steps + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - path: src - - uses: actions/cache@v2 + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - uses: ./.github/actions/setup/ubuntu with: - path: src/.downloaded-cache - key: downloaded-cache - - name: Fixed world writable dirs - run: | - chmod -v go-w $HOME $HOME/.config - sudo chmod -R go-w /usr/share - sudo bash -c 'IFS=:; for d in '"$PATH"'; do chmod -v go-w $d; done' || : - - run: ./autogen.sh - working-directory: src + arch: ${{ matrix.arch }} + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 + with: + ruby-version: '3.1' + bundler: none + if: >- + ${{ !endsWith(matrix.os, 'arm') + && !endsWith(matrix.os, 'ppc64le') && !endsWith(matrix.os, 's390x') }} + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + clean: true + dummy-files: ${{ matrix.test_task == 'check' }} + # Set fetch-depth: 10 so that Launchable can receive commits information. + fetch-depth: 10 + - name: Run configure env: - arch: ${{matrix.arch}} + arch: ${{ matrix.arch }} + configure: ${{ matrix.configure }} run: >- - $SETARCH ../src/configure -C --disable-install-doc ${{ matrix.configure }} - ${arch:+--target=$arch-$OSTYPE} - - run: $SETARCH make incs - - run: $SETARCH make - - run: $SETARCH make leaked-globals - if: ${{ matrix.test_task == 'check' }} + $SETARCH ../src/configure -C --disable-install-doc ${configure:-cppflags=-DRUBY_DEBUG} + ${arch:+--target=$arch-$OSTYPE --host=$arch-$OSTYPE} + - run: $SETARCH make prepare-gems - if: ${{ matrix.test_task == 'check' }} - - name: Create dummy files in build dir + if: ${{ matrix.test_task == 'test-bundled-gems' }} + + - run: $SETARCH make + + - run: $SETARCH make hello + + - name: runirb run: | - $SETARCH ./miniruby -e '(("a".."z").to_a+("A".."Z").to_a+("0".."9").to_a+%w[foo bar test zzz]).each{|basename|File.write("#{basename}.rb", "raise %(do not load #{basename}.rb)")}' - if: ${{ matrix.test_task == 'check' }} + echo IRB::VERSION | $SETARCH make runirb RUNOPT="-- -f" + + - name: Set test options for skipped tests + run: | + set -x + TESTS="$(echo "${{ matrix.skipped_tests }}" | sed 's| |$$/ -n!/|g;s|^|-n!/|;s|$|$$/|')" + echo "TESTS=${TESTS}" >> $GITHUB_ENV + if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} + + - name: Set up Launchable + id: launchable + uses: ./.github/actions/launchable/setup + with: + os: ${{ matrix.os || 'ubuntu-22.04' }} + test-opts: ${{ matrix.configure }} + launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} + builddir: build + srcdir: src + continue-on-error: true + timeout-minutes: 3 + + # Avoid possible test failures with the zlib applying the following patch + # on s390x CPU architecture. + # https://github.com/madler/zlib/pull/410 + - name: Disable DFLTCC + run: echo "DFLTCC=0" >> $GITHUB_ENV + if: ${{ endsWith(matrix.os, 's390x') }} + - name: make ${{ matrix.test_task }} run: | - $SETARCH make -s ${{ matrix.test_task }} ${TESTS:+TESTS=`echo "$TESTS" | sed 's| |$/ -n!/|g;s|^|-n!/|;s|$|$$/|'`} - timeout-minutes: 40 + test -n "${LAUNCHABLE_STDOUT}" && exec 1> >(tee "${LAUNCHABLE_STDOUT}") + test -n "${LAUNCHABLE_STDERR}" && exec 2> >(tee "${LAUNCHABLE_STDERR}") + + $SETARCH make -s ${{ matrix.test_task }} \ + ${TESTS:+TESTS="$TESTS"} \ + ${{ !contains(matrix.test_task, 'bundle') && 'RUBYOPT=-w' || '' }} + timeout-minutes: ${{ matrix.timeout || 40 }} env: - RUBY_TESTOPTS: "-q --tty=no" - TESTS: ${{ matrix.test_task == 'check' && matrix.skipped_tests || '' }} - TEST_BUNDLED_GEMS_ALLOW_FAILURES: "" + RUBY_TESTOPTS: '-q --tty=no' + TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' + PRECHECK_BUNDLED_GEMS: 'no' + LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} + LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} + - name: make skipped tests run: | - $SETARCH make -s test-all TESTS=`echo "$TESTS" | sed 's| |$/ -n/|g;s|^|-n/|;s|$|$$/|'` + $SETARCH make -s test-all TESTS="${TESTS//-n!\//-n/}" env: - GNUMAKEFLAGS: "" - RUBY_TESTOPTS: "-v --tty=no" - TESTS: ${{ matrix.skipped_tests }} - if: ${{ matrix.test_task == 'check' && matrix.skipped_tests != '' }} - - uses: k0kubun/action-slack@v2.0.0 + GNUMAKEFLAGS: '' + RUBY_TESTOPTS: '-v --tty=no' + if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} + continue-on-error: ${{ matrix.continue-on-skipped_tests || false }} + + - name: test-pc + run: | + DESTDIR=${RUNNER_TEMP-${TMPDIR-/tmp}}/installed + $SETARCH make test-pc "DESTDIR=$DESTDIR" + + - name: CAPI extensions + uses: ./.github/actions/capiext with: - payload: | - { - "ci": "GitHub Actions", - "env": "${{ matrix.os }} / ${{ matrix.test_task }}${{ matrix.configure }}", - "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] - } + builddir: build + make: '$SETARCH make' env: + RUBY_TESTOPTS: '-v --tty=no' + if: ${{ contains(matrix.extra_checks, 'capi') }} + + - uses: ./.github/actions/slack + with: + label: ${{ matrix.test_task }} ${{ matrix.configure }}${{ matrix.arch }}${{ matrix.os }} + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + + make-ibm: + strategy: + matrix: + include: + - test_task: check + os: ubuntu-24.04-ppc64le + - test_task: check + os: ubuntu-24.04-s390x + fail-fast: false + + env: *make-env + + runs-on: ${{ matrix.os }} + + if: >- + ${{github.repository == 'ruby/ruby' + && !(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: *make-steps + + # Separated from `make` job to avoid making it a required status check + ruby-bench: + strategy: + matrix: + include: + # Using the same setup as ZJIT jobs + - bench_opts: '--warmup=1 --bench=1 --excludes=lobsters' + + runs-on: ubuntu-24.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - uses: ./.github/actions/setup/ubuntu + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + + - name: Run configure + run: ../src/configure -C --disable-install-doc --prefix="$(pwd)/install" + + - run: make install + + - name: Checkout ruby-bench + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + repository: ruby/ruby-bench + path: ruby-bench + + # If you want to skip failing benchmark, consider using `--excludes`. + # e.g. `bench_opts: '--warmup=1 --bench=1 --excludes=railsbench,lobsters'` + - name: Run ruby-bench + run: ruby run_benchmarks.rb -e "ruby::../build/install/bin/ruby" ${{ matrix.bench_opts }} + working-directory: ruby-bench + + - uses: ./.github/actions/slack + with: + label: ruby-bench ${{ matrix.bench_opts }} ${{ matrix.ruby_opts }} SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot - if: ${{ failure() && github.event_name == 'push' }} + if: ${{ failure() }} + + result: + if: ${{ always() }} + name: ${{ github.workflow }} result + runs-on: ubuntu-latest + needs: [make] + steps: + - run: exit 1 + working-directory: + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} defaults: run: diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml new file mode 100644 index 0000000000..0d2a6f0545 --- /dev/null +++ b/.github/workflows/wasm.yml @@ -0,0 +1,179 @@ +name: WebAssembly +on: + push: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + pull_request: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: # added using https://github.com/step-security/secure-workflows + contents: read + +jobs: + make: + strategy: + matrix: + entry: +# # wasmtime can't compile non-optimized Asyncified binary due to locals explosion +# - { name: O0-debuginfo, optflags: '-O0', debugflags: '-g', wasmoptflags: '-O1' } +# - { name: O1, optflags: '-O1', debugflags: '' , wasmoptflags: '-O1' } + - { name: O2, optflags: '-O2', debugflags: '', wasmoptflags: '-O2' } +# - { name: O3, optflags: '-O3', debugflags: '' , wasmoptflags: '-O3' } +# # -O4 is equivalent to -O3 in clang, but it's different in wasm-opt +# - { name: O4, optflags: '-O3', debugflags: '' , wasmoptflags: '-O4' } +# - { name: Oz, optflags: '-Oz', debugflags: '' , wasmoptflags: '-Oz' } + fail-fast: false + + env: + RUBY_TESTOPTS: '-q --tty=no' + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + WASI_SDK_VERSION_MAJOR: 25 + WASI_SDK_VERSION_MINOR: 0 + BINARYEN_VERSION: 113 + WASMTIME_VERSION: v15.0.0 + + runs-on: ubuntu-22.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + + - name: Install libraries + run: | + set -ex + sudo apt-get update -q || : + sudo apt-get install --no-install-recommends -q -y ruby make autoconf git wget + + wasi_sdk_deb="wasi-sdk-${WASI_SDK_VERSION_MAJOR}.${WASI_SDK_VERSION_MINOR}-x86_64-linux.deb" + wget "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION_MAJOR}/${wasi_sdk_deb}" + sudo dpkg -i "$wasi_sdk_deb" + rm -f "$wasi_sdk_deb" + + mkdir build-sdk + pushd build-sdk + + wasmtime_url="https://github.com/bytecodealliance/wasmtime/releases/download/${WASMTIME_VERSION}/wasmtime-${WASMTIME_VERSION}-x86_64-linux.tar.xz" + wget -O - "$wasmtime_url" | tar xJf - + sudo ln -fs "$PWD/wasmtime-${WASMTIME_VERSION}-x86_64-linux/wasmtime" /usr/local/bin/wasmtime + + binaryen_tarball="binaryen-version_${BINARYEN_VERSION}-x86_64-linux.tar.gz" + binaryen_url="https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/${binaryen_tarball}" + wget -O - "$binaryen_url" | tar xfz - + sudo ln -fs "$PWD/binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt" /usr/local/bin/wasm-opt + working-directory: src + + - name: Set ENV + run: | + echo "WASI_SDK_PATH=/opt/wasi-sdk" >> $GITHUB_ENV + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 + with: + ruby-version: '3.1' + bundler: none + + - name: Build baseruby + run: | + set -ex + mkdir ../baseruby + pushd ../baseruby + ../src/configure --prefix=$PWD/install + make + make install + + - name: Download config.guess with wasi version + run: | + rm tool/config.guess tool/config.sub + ruby tool/downloader.rb -d tool -e gnu config.guess config.sub + working-directory: src + + - name: Run configure + run: | + ../src/configure \ + --host wasm32-unknown-wasi \ + --with-baseruby=$PWD/../baseruby/install/bin/ruby \ + --with-static-linked-ext \ + --with-ext=cgi/escape,continuation,coverage,date,digest/bubblebabble,digest,digest/md5,digest/rmd160,digest/sha1,digest/sha2,etc,fcntl,json,json/generator,json/parser,objspace,pathname,rbconfig/sizeof,ripper,stringio,strscan,monitor \ + LDFLAGS=" \ + -Xlinker --stack-first \ + -Xlinker -z -Xlinker stack-size=16777216 \ + " \ + optflags="${{ matrix.entry.optflags }}" \ + debugflags="${{ matrix.entry.debugflags }}" \ + wasmoptflags="${{ matrix.entry.wasmoptflags }} ${{ matrix.entry.debugflags }}" + + # miniruby may not be built when cross-compling + - run: make mini ruby + + - run: make install DESTDIR=$PWD/../install + - run: tar cfz ../install.tar.gz -C ../install . + + - name: Upload artifacts + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: ruby-wasm-install + path: ${{ github.workspace }}/install.tar.gz + - name: Show Playground URL to try the build + run: | + echo "Try on Playground: https://ruby.github.io/play-ruby?run=$GITHUB_RUN_ID" >> $GITHUB_STEP_SUMMARY + + - name: Run basictest + run: wasmtime run ./../build/miniruby --mapdir /::./ -- basictest/test.rb + working-directory: src + + - name: Run bootstraptest (no thread) + run: | + NO_THREAD_TESTS="$(grep -L Thread -R ./bootstraptest | awk -F/ '{ print $NF }' | uniq | sed -n 's/test_\(.*\).rb/\1/p' | paste -s -d, -)" + ruby ./bootstraptest/runner.rb --ruby="$(which wasmtime) run $PWD/../build/ruby --mapdir /::./ -- " --verbose "--sets=$NO_THREAD_TESTS" + working-directory: src + + - uses: ./.github/actions/slack + with: + label: ${{ matrix.entry.name }} + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + + # Workaround for https://github.com/orgs/community/discussions/25220 + - name: Save Pull Request number + if: ${{ github.event_name == 'pull_request' }} + run: echo "${{ github.event.pull_request.number }}" >> ${{ github.workspace }}/github-pr-info.txt + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + if: ${{ github.event_name == 'pull_request' }} + with: + name: github-pr-info + path: ${{ github.workspace }}/github-pr-info.txt + +defaults: + run: + working-directory: build diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 574bfbf474..1d44a5482c 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -3,131 +3,197 @@ on: push: paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' pull_request: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' + # Do not use paths-ignore for required status checks + # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks + merge_group: concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} +permissions: + contents: read + jobs: make: strategy: matrix: include: - - vs: 2019 - os: windows-2019 - vcvars: '"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"' - # - vs: 2022 - # os: windows-2022 - # vcvars: '"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"' + - os: 2022 + vc: 2022 + test_task: check + - os: 2025 + vc: 2022 + test_task: check + - os: 11-arm + test_task: 'btest test-basic test-tool' # check and test-spec are broken yet. + target: arm64 + - os: 2025 + vc: 2022 + test_task: test-bundled-gems fail-fast: false - runs-on: ${{ matrix.os }} - if: ${{ !startsWith(github.event.head_commit.message, '[DOC]') && !contains(github.event.pull_request.labels.*.name, 'Documentation') }} - name: VisualStudio ${{ matrix.vs }} + + runs-on: windows-${{ matrix.os }} + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + name: Windows ${{ matrix.os }}/Visual C++ ${{ matrix.vc }} (${{ matrix.test_task }}) + env: - GITPULLOPTIONS: --no-tags origin ${{github.ref}} - VCVARS: ${{ matrix.vcvars }} - PATCH: C:\msys64\usr\bin\patch.exe + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + VCPKG_DEFAULT_TRIPLET: ${{ matrix.target || 'x64' }}-windows + steps: - run: md build working-directory: - - uses: msys2/setup-msys2@v2 - id: setup-msys2 + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 with: - update: true - install: >- - patch - if: ${{ matrix.os != 'windows-2019' }} - - name: patch path - shell: msys2 {0} - run: echo PATCH=$(cygpath -wa $(command -v patch)) >> $GITHUB_ENV - if: ${{ steps.setup-msys2.outcome == 'success' }} - - uses: actions/cache@v2 + # windows-11-arm has only 3.4.1, 3.4.2, 3.4.3, head + ruby-version: ${{ !endsWith(matrix.os, 'arm') && '3.1' || '3.4' }} + bundler: none + windows-toolchain: none + + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - path: C:\vcpkg\downloads - key: ${{ runner.os }}-vcpkg-download-${{ matrix.os }}-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-vcpkg-download-${{ matrix.os }}- - ${{ runner.os }}-vcpkg-download- - - name: Install libraries with vcpkg - run: | - vcpkg --triplet x64-windows install readline zlib - - uses: actions/cache@v2 + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - uses: ./.github/actions/setup/directories with: - path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey - key: ${{ runner.os }}-chocolatey-${{ matrix.os }}-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-chocolatey-${{ matrix.os }}- - ${{ runner.os }}-chocolatey- - - name: Install libraries with chocolatey + srcdir: src + builddir: build + make-command: nmake + clean: true + + - name: Install tools with scoop run: | - # Using Choco-Install for retries, but it doesn't detect failures properly - # if you pass multiple package names in a single command. - Choco-Install -PackageName openssl - Choco-Install -PackageName winflexbison3 + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + iwr -useb get.scoop.sh | iex + Join-Path (Resolve-Path ~).Path "scoop\shims" >> $Env:GITHUB_PATH + scoop install vcpkg uutils-coreutils shell: pwsh - - name: git config - run: | - git config --global core.autocrlf false - git config --global core.eol lf - git config --global advice.detachedHead 0 - git config --global init.defaultBranch garbage - - uses: actions/checkout@v2 + + - name: Restore vcpkg artifact + id: restore-vcpkg + uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: - path: src - - uses: actions/cache@v2 + path: src\vcpkg_installed + key: windows-${{ matrix.os }}-vcpkg-${{ hashFiles('src/vcpkg.json') }} + + - name: Install libraries with vcpkg + run: | + vcpkg install --vcpkg-root=%USERPROFILE%\scoop\apps\vcpkg\current + working-directory: src + if: ${{ ! steps.restore-vcpkg.outputs.cache-hit }} + + - name: Save vcpkg artifact + uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: - path: src/.downloaded-cache - key: downloaded-cache + path: src\vcpkg_installed + key: windows-${{ matrix.os }}-vcpkg-${{ hashFiles('src/vcpkg.json') }} + if: ${{ ! steps.restore-vcpkg.outputs.cache-hit && (github.ref_name == 'master' || startsWith(github.ref_name, 'ruby_')) }} + - name: setup env + # Available Ruby versions: https://github.com/actions/runner-images/blob/main/images/windows/Windows2019-Readme.md#ruby # %TEMP% is inconsistent with %TMP% and test-all expects they are consistent. # https://github.com/actions/virtual-environments/issues/712#issuecomment-613004302 run: | - set | C:\msys64\usr\bin\sort > old.env - call %VCVARS% - set TMP=%USERPROFILE%\AppData\Local\Temp - set TEMP=%USERPROFILE%\AppData\Local\Temp + ::- Set up VC ${{ matrix.vc }} + set | sort > old.env + call ..\src\win32\vssetup.cmd ^ + -arch=${{ matrix.target || 'amd64' }} ^ + ${{ matrix.vcvars && '-vcvars_ver=' || '' }}${{ matrix.vcvars }} + nmake -f nul + set TMP=%RUNNER_TEMP% + set TEMP=%RUNNER_TEMP% + set MAKEFLAGS=l set /a TEST_JOBS=(15 * %NUMBER_OF_PROCESSORS% / 10) > nul - set | C:\msys64\usr\bin\sort > new.env - C:\msys64\usr\bin\comm -13 old.env new.env >> %GITHUB_ENV% + set RUBY_OPT_DIR=%GITHUB_WORKSPACE:\=/%/src/vcpkg_installed/%VCPKG_DEFAULT_TRIPLET% + set | sort > new.env + comm -13 old.env new.env >> %GITHUB_ENV% del *.env + + - name: baseruby version + run: ruby -v + + - name: compiler version + run: cl + + - name: volume info + run: Get-Volume + shell: pwsh + + # TODO: We should use `../src` instead of `D:/a/ruby/ruby/src` - name: Configure - run: | - ../src/win32/configure.bat --disable-install-doc --enable-bundled-libffi --with-opt-dir=C:/vcpkg/installed/x64-windows --with-openssl-dir="C:/Program Files/OpenSSL-Win64" + run: >- + ../src/win32/configure.bat --disable-install-doc + --with-opt-dir=%RUBY_OPT_DIR% + --with-gmp + + - run: nmake prepare-vcpkg + - run: nmake incs + - run: nmake extract-extlibs + + # On all other platforms, test-spec depending on extract-gems (in common.mk) is enough. + # But not for this Visual Studio workflow. So here we extract gems before building. + - run: nmake extract-gems + + # windows-11-arm runner cannot run `ruby tool/file2lastrev.rb --revision.h --output=revision.h` + - name: make revision.h + run: | + win32\lastrev.bat | win32\ifchange.bat --timestamp=.revision.time revision.h - + type revision.h + working-directory: src + - run: nmake - env: - YACC: win_bison - - run: nmake test - timeout-minutes: 5 - - run: nmake test-all - env: - RUBY_TESTOPTS: -j${{env.TEST_JOBS}} --job-status=normal - timeout-minutes: 60 - continue-on-error: ${{ matrix.continue-on-error || false }} - - run: nmake test-spec - timeout-minutes: 10 - continue-on-error: ${{ matrix.continue-on-error || false }} - - uses: k0kubun/action-slack@v2.0.0 + + - name: Set up Launchable + uses: ./.github/actions/launchable/setup with: - payload: | - { - "ci": "GitHub Actions", - "env": "VS${{ matrix.vs }} / ${{ matrix.test_task || 'check' }}", - "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] - } + os: windows-${{ matrix.os }} + launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} + builddir: build + srcdir: src + test-task: ${{ matrix.test_task || 'check' }} + continue-on-error: true + if: ${{ matrix.test_task != 'test-bundled-gems' }} + timeout-minutes: 3 + + - run: nmake ${{ matrix.test_task || 'check' }} env: + RUBY_TESTOPTS: -j${{ env.TEST_JOBS || 4 }} + timeout-minutes: 70 + + - uses: ./.github/actions/slack + with: + label: Windows ${{ matrix.os }} / VC ${{ matrix.vc }} / ${{ matrix.test_task || 'check' }} SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot - if: ${{ failure() && github.event_name == 'push' }} + if: ${{ failure() }} + + result: + if: ${{ always() }} + name: ${{ github.workflow }} result + runs-on: windows-latest + needs: [make] + steps: + - run: exit 1 + working-directory: + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} defaults: run: diff --git a/.github/workflows/wsl.yml b/.github/workflows/wsl.yml new file mode 100644 index 0000000000..640f18ce42 --- /dev/null +++ b/.github/workflows/wsl.yml @@ -0,0 +1,70 @@ +name: Ubuntu on WSL + +on: + push: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + pull_request: + # Do not use paths-ignore for required status checks + # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks + merge_group: + +jobs: + wsl: + runs-on: windows-2025 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - name: Install or update WSL + uses: Ubuntu/WSL/.github/actions/wsl-install@main + with: + distro: Ubuntu-24.04 + + - name: Install dependencies + uses: Ubuntu/WSL/.github/actions/wsl-bash@main + with: + distro: Ubuntu-24.04 + working-dir: /tmp/github/ + exec: | + DEBIAN_FRONTEND=noninteractive sudo apt update + DEBIAN_FRONTEND=noninteractive sudo apt install -y ruby build-essential autoconf libssl-dev libyaml-dev zlib1g-dev libgmp-dev libffi-dev + + - name: Check out the repository + uses: Ubuntu/WSL/.github/actions/wsl-checkout@main + with: + distro: Ubuntu-24.04 + working-dir: /tmp/github/ + submodules: true + + - name: Build + uses: Ubuntu/WSL/.github/actions/wsl-bash@main + with: + distro: Ubuntu-24.04 + working-dir: /tmp/github/ + exec: | + ./autogen.sh + ./configure --disable-install-doc + make ruby -j4 + make extract-gems + make -j4 + + - name: Test + uses: Ubuntu/WSL/.github/actions/wsl-bash@main + with: + distro: Ubuntu-24.04 + working-dir: /tmp/github/ + exec: | + ./ruby -v + # make check TESTS="-j4" MSPECOPT="-j" diff --git a/.github/workflows/yjit-macos.yml b/.github/workflows/yjit-macos.yml new file mode 100644 index 0000000000..a59b4d6508 --- /dev/null +++ b/.github/workflows/yjit-macos.yml @@ -0,0 +1,198 @@ +name: YJIT macOS +on: + push: + branches: + - master + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + pull_request: + types: + - opened + - synchronize + - reopened + # Do not use paths-ignore for required status checks + # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + cargo: + name: cargo test + + runs-on: macos-14 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - run: RUST_BACKTRACE=1 cargo test + working-directory: yjit + + # Also compile and test with all features enabled + - run: RUST_BACKTRACE=1 cargo test --all-features + working-directory: yjit + + # Check that we can build in release mode too + - run: cargo build --release + working-directory: yjit + + make: + strategy: + matrix: + include: + - test_task: 'check' + configure: '--enable-yjit' + yjit_opts: '--yjit' + - test_task: 'check' + configure: '--enable-yjit=dev' + yjit_opts: '--yjit-call-threshold=1 --yjit-verify-ctx --yjit-code-gc' + specopts: '-T --yjit-call-threshold=1 -T --yjit-verify-ctx -T --yjit-code-gc' + fail-fast: false + + env: + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + RUN_OPTS: ${{ matrix.yjit_opts }} + SPECOPTS: ${{ matrix.specopts }} + + runs-on: macos-14 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - name: Install libraries + uses: ./.github/actions/setup/macos + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + dummy-files: ${{ matrix.test_task == 'check' }} + # Set fetch-depth: 10 so that Launchable can receive commits information. + fetch-depth: 10 + + - name: Run configure + run: ../src/configure -C --disable-install-doc ${{ matrix.configure }} + + - run: make prepare-gems + if: ${{ matrix.test_task == 'test-bundled-gems' }} + + - run: make + + - name: Verify that --yjit-dump-disasm works + run: | + ./miniruby --yjit-call-threshold=1 --yjit-dump-disasm -e0 | \ + wc -l | \ + ruby -ne 'raise "Disassembly seems broken in dev build (output has too few lines)" unless $_.to_i > 10' + if: ${{ contains(matrix.configure, 'jit=dev') }} + + - name: Set ENV for YJIT + run: | + echo "RUBY_YJIT_ENABLE=1" >> $GITHUB_ENV + echo "RUBY_CRASH_REPORT=$(pwd)/rb_crash_%p.txt" >> $GITHUB_ENV + + - name: Set test options for skipped tests + run: | + set -x + TESTS="$(echo "${{ matrix.skipped_tests }}" | sed 's| |$$/ -n!/|g;s|^|-n!/|;s|$|$$/|')" + echo "TESTS=${TESTS}" >> $GITHUB_ENV + if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} + + - name: Set up Launchable + id: launchable + uses: ./.github/actions/launchable/setup + with: + os: macos-14 + test-opts: ${{ matrix.configure }} + launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} + builddir: build + srcdir: src + is-yjit: true + continue-on-error: true + timeout-minutes: 3 + + - name: make ${{ matrix.test_task }} + run: | + test -n "${LAUNCHABLE_STDOUT}" && exec 1> >(tee "${LAUNCHABLE_STDOUT}") + test -n "${LAUNCHABLE_STDERR}" && exec 2> >(tee "${LAUNCHABLE_STDERR}") + + set -x + make -s ${{ matrix.test_task }} ${TESTS:+TESTS="$TESTS"} \ + RUN_OPTS="$RUN_OPTS" \ + SPECOPTS="$SPECOPTS" + timeout-minutes: 60 + env: + RUBY_TESTOPTS: '-q --tty=no' + TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' + SYNTAX_SUGGEST_TIMEOUT: '5' + PRECHECK_BUNDLED_GEMS: 'no' + LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} + LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} + continue-on-error: ${{ matrix.continue-on-test_task || false }} + + - name: make skipped tests + run: | + make -s test-all TESTS="${TESTS//-n!\//-n/}" + env: + GNUMAKEFLAGS: '' + RUBY_TESTOPTS: '-v --tty=no' + PRECHECK_BUNDLED_GEMS: 'no' + if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} + continue-on-error: ${{ matrix.continue-on-skipped_tests || false }} + + - name: Dump crash logs + if: ${{ failure() }} + continue-on-error: true + run: | + tail --verbose --lines=+1 rb_crash_*.txt + exit 1 + + - uses: ./.github/actions/slack + with: + label: ${{ matrix.test_task }} ${{ matrix.configure }} ${{ matrix.yjit_opts }} + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + + result: + if: ${{ always() }} + name: ${{ github.workflow }} result + runs-on: macos-14 + needs: [make] + steps: + - name: ${{ github.workflow }} jobs have failed + run: exit 1 + working-directory: + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} + +defaults: + run: + working-directory: build diff --git a/.github/workflows/yjit-ubuntu.yml b/.github/workflows/yjit-ubuntu.yml index c479b8a624..150f0b3275 100644 --- a/.github/workflows/yjit-ubuntu.yml +++ b/.github/workflows/yjit-ubuntu.yml @@ -3,109 +3,237 @@ on: push: paths-ignore: - 'doc/**' + - '**/man/*' - '**.md' - '**.rdoc' + - '**/.document' + - '.*.yml' pull_request: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' + # Do not use paths-ignore for required status checks + # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks + merge_group: concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} +permissions: + contents: read + jobs: + cargo: + name: cargo test + + # GitHub Action's image seems to already contain a Rust 1.58.0. + runs-on: ubuntu-22.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + # For now we can't run cargo test --offline because it complains about the + # capstone dependency, even though the dependency is optional + #- run: cargo test --offline + + - run: RUST_BACKTRACE=1 cargo test + working-directory: yjit + + # Also compile and test with all features enabled + - run: RUST_BACKTRACE=1 cargo test --all-features + working-directory: yjit + + # Check that we can build in release mode too + - run: cargo build --release + working-directory: yjit + + lint: + name: cargo clippy + + # GitHub Action's image seems to already contain a Rust 1.58.0. + runs-on: ubuntu-22.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + # Check that we don't have linting errors in release mode, too + - run: cargo clippy --all-targets --all-features + working-directory: yjit + make: strategy: + fail-fast: false matrix: - test_task: ["check"] # "test-bundler-parallel", - os: - - ubuntu-20.04 -# - ubuntu-18.04 - yjit_opts: [ - "--yjit", - "--yjit --yjit-call-threshold=1", - ] - configure: ["", "cppflags=-DRUBY_DEBUG"] include: - - test_task: "test-all TESTS=--repeat-count=2" - os: ubuntu-20.04 - configure: "" - yjit_enable_env: RUBY_YJIT_ENABLE - - test_task: "test-bundled-gems" - os: ubuntu-20.04 - configure: "cppflags=-DRUBY_DEBUG" - yjit_enable_env: RUBY_YJIT_ENABLE - fail-fast: false + - test_task: 'yjit-bindgen' + hint: 'To fix: use patch in logs' + # Build with YJIT+ZJIT for output that works in the most number of configurations + configure: '--with-gcc=clang-14 --enable-yjit=dev --enable-zjit' + libclang_path: '/usr/lib/llvm-14/lib/libclang.so.1' + + - test_task: 'check' + # YJIT should be automatically built in release mode on x86-64 Linux with rustc present + #configure: "--enable-yjit RUSTC='rustc +1.58.0'" + configure: "RUSTC='rustc +1.58.0'" + rust_version: '1.58.0' + + - test_task: 'check' + configure: '--enable-yjit=dev' + + - test_task: 'check' + configure: '--enable-yjit=dev' + yjit_opts: '--yjit-call-threshold=1 --yjit-verify-ctx --yjit-code-gc' + specopts: '-T --yjit-call-threshold=1 -T --yjit-verify-ctx -T --yjit-code-gc' + + - test_task: 'test-bundled-gems' + configure: '--enable-yjit=dev' + env: - GITPULLOPTIONS: --no-tags origin ${{github.ref}} + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} RUN_OPTS: ${{ matrix.yjit_opts }} + YJIT_BENCH_OPTS: ${{ matrix.yjit_bench_opts }} + SPECOPTS: ${{ matrix.specopts }} RUBY_DEBUG: ci - runs-on: ${{ matrix.os }} - if: ${{ !startsWith(github.event.head_commit.message, '[DOC]') && !contains(github.event.pull_request.labels.*.name, 'Documentation') }} + RUST_BACKTRACE: 1 + + runs-on: ubuntu-22.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + steps: - - run: mkdir build - working-directory: - - name: Install libraries - run: | - set -x - sudo apt-get update -q || : - sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev bison autoconf ruby - - name: git config - run: | - git config --global advice.detachedHead 0 - git config --global init.defaultBranch garbage - - uses: actions/checkout@v2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - path: src - - uses: actions/cache@v2 + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - uses: ./.github/actions/setup/ubuntu + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 with: - path: src/.downloaded-cache - key: downloaded-cache - - name: Fixed world writable dirs - run: | - chmod -v go-w $HOME $HOME/.config - sudo chmod -R go-w /usr/share - sudo bash -c 'IFS=:; for d in '"$PATH"'; do chmod -v go-w $d; done' || : - - name: Set ENV - run: | - echo "GNUMAKEFLAGS=-j$((1 + $(nproc --all)))" >> $GITHUB_ENV - - run: ./autogen.sh - working-directory: src + ruby-version: '3.1' + bundler: none + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + dummy-files: ${{ matrix.test_task == 'check' }} + # Set fetch-depth: 10 so that Launchable can receive commits information. + fetch-depth: 10 + + - name: Install Rust + if: ${{ matrix.rust_version }} + run: rustup install ${{ matrix.rust_version }} --profile minimal + + - name: Remove cargo + # Since this tests a `rustc` build for release, remove `cargo` to ensure + # that only `rustc` is used. + if: ${{ contains(matrix.configure, 'rustc') }} + run: sudo rm $(which -a cargo | uniq) + - name: Run configure - run: ../src/configure -C --disable-install-doc ${{ matrix.configure }} + run: ../src/configure -C --disable-install-doc --prefix=$(pwd)/install ${{ matrix.configure }} + - run: make incs - - run: make - - run: make leaked-globals - if: ${{ matrix.test_task == 'check' }} + - run: make prepare-gems - if: ${{ matrix.test_task == 'check' }} - - name: Create dummy files in build dir + if: ${{ matrix.test_task == 'test-bundled-gems' }} + + - run: make + + - name: Verify that --yjit-dump-disasm works run: | - ./miniruby -e '(("a".."z").to_a+("A".."Z").to_a+("0".."9").to_a+%w[foo bar test zzz]).each{|basename|File.write("#{basename}.rb", "raise %(do not load #{basename}.rb)")}' - if: ${{ matrix.test_task == 'check' }} - - name: Enable YJIT through ENV - run: echo "RUBY_YJIT_ENABLE=1" >> $GITHUB_ENV - if: ${{ matrix.yjit_enable_env }} - - run: make -s ${{ matrix.test_task }} RUN_OPTS="$RUN_OPTS" - timeout-minutes: 60 - env: - RUBY_TESTOPTS: "-q --tty=no" - TEST_BUNDLED_GEMS_ALLOW_FAILURES: "" - - uses: k0kubun/action-slack@v2.0.0 + ./miniruby --yjit-call-threshold=1 --yjit-dump-disasm -e0 | \ + wc -l | \ + ruby -ne 'raise "Disassembly seems broken in dev build (output has too few lines)" unless $_.to_i > 10' + if: ${{ contains(matrix.configure, 'jit=dev') }} + + - name: Set ENV for YJIT + run: | + echo "RUBY_YJIT_ENABLE=1" >> $GITHUB_ENV + echo "RUBY_CRASH_REPORT=$(pwd)/rb_crash_%p.txt" >> $GITHUB_ENV + + # Check that the binary was built with YJIT + - name: Check YJIT enabled + run: ./miniruby --yjit -v | grep "+YJIT" + + - name: Set up Launchable + id: launchable + uses: ./.github/actions/launchable/setup with: - payload: | - { - "ci": "GitHub Actions", - "env": "${{ matrix.os }} / ${{ matrix.test_task }}${{ matrix.configure }}", - "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] - } + os: ubuntu-22.04 + test-opts: ${{ matrix.configure }} + launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} + builddir: build + srcdir: src + is-yjit: true + continue-on-error: true + timeout-minutes: 3 + + - name: make ${{ matrix.test_task }} + run: | + test -n "${LAUNCHABLE_STDOUT}" && exec 1> >(tee "${LAUNCHABLE_STDOUT}") + test -n "${LAUNCHABLE_STDERR}" && exec 2> >(tee "${LAUNCHABLE_STDERR}") + + set -x + make -s ${{ matrix.test_task }} ${TESTS:+TESTS="$TESTS"} \ + RUN_OPTS="$RUN_OPTS" MSPECOPT=--debug SPECOPTS="$SPECOPTS" \ + YJIT_BENCH_OPTS="$YJIT_BENCH_OPTS" YJIT_BINDGEN_DIFF_OPTS="$YJIT_BINDGEN_DIFF_OPTS" + timeout-minutes: 90 env: + RUBY_TESTOPTS: '-q --tty=no' + TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' + PRECHECK_BUNDLED_GEMS: 'no' + SYNTAX_SUGGEST_TIMEOUT: '5' + YJIT_BINDGEN_DIFF_OPTS: '--exit-code' + LIBCLANG_PATH: ${{ matrix.libclang_path }} + LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} + LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} + continue-on-error: ${{ matrix.continue-on-test_task || false }} + + - name: Dump crash logs + if: ${{ failure() }} + continue-on-error: true + run: | + tail --verbose --lines=+1 rb_crash_*.txt + exit 1 + + - uses: ./.github/actions/slack + with: + label: ${{ matrix.test_task }} ${{ matrix.configure }} SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot - if: ${{ failure() && github.event_name == 'push' }} + if: ${{ failure() }} + + result: + if: ${{ always() }} + name: ${{ github.workflow }} result + runs-on: ubuntu-latest + needs: [make] + steps: + - run: exit 1 + working-directory: + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} defaults: run: diff --git a/.github/workflows/yjit_asm_tests.yml b/.github/workflows/yjit_asm_tests.yml deleted file mode 100644 index 8a9052dd41..0000000000 --- a/.github/workflows/yjit_asm_tests.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: YJIT x86 assembler tests - -on: - push: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' - pull_request: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' - -concurrency: - group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} - cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} - -jobs: - test: - runs-on: ubuntu-latest - if: ${{ !startsWith(github.event.head_commit.message, '[DOC]') && !contains(github.event.pull_request.labels.*.name, 'Documentation') }} - steps: - - name: Install dependencies - run: | - set -x - sudo apt-get update -q || : - sudo apt-get install --no-install-recommends -q -y build-essential - - name: git config - run: | - git config --global advice.detachedHead 0 - git config --global init.defaultBranch garbage - - uses: actions/checkout@v2 - with: - path: src - - name: Run ASM tests - run: ./misc/test_yjit_asm.sh - working-directory: src diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml new file mode 100644 index 0000000000..a638907811 --- /dev/null +++ b/.github/workflows/zjit-macos.yml @@ -0,0 +1,214 @@ +name: ZJIT macOS +on: + push: + branches: + - master + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + pull_request: + types: + - opened + - synchronize + - reopened + # Do not use paths-ignore for required status checks + # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + make: + strategy: + fail-fast: false + matrix: + include: + - test_task: 'check' + run_opts: '--zjit-call-threshold=1' + specopts: '-T --zjit-call-threshold=1' + configure: '--enable-zjit=dev' + + - test_task: 'check' + run_opts: '--zjit-disable-hir-opt --zjit-call-threshold=1' + specopts: '-T --zjit-disable-hir-opt -T --zjit-call-threshold=1' + configure: '--enable-zjit=dev' + + - test_task: 'zjit-check' # zjit-test + quick feedback of test_zjit.rb + configure: '--enable-yjit=dev --enable-zjit' + rust_version: "1.85.0" + + - test_task: 'ruby' + hint: 'combo build test' + configure: '--enable-yjit --enable-zjit' + + env: + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + RUN_OPTS: ${{ matrix.run_opts }} + SPECOPTS: ${{ matrix.specopts }} + TESTOPTS: ${{ matrix.testopts }} + ZJIT_RB_BUG: 1 + + runs-on: macos-14 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - name: Install libraries + uses: ./.github/actions/setup/macos + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + dummy-files: ${{ matrix.test_task == 'check' }} + # Set fetch-depth: 10 so that Launchable can receive commits information. + fetch-depth: 10 + + - name: Install Rust + if: ${{ matrix.rust_version }} + run: | + rustup install ${{ matrix.rust_version }} --profile minimal + rustup default ${{ matrix.rust_version }} + + - uses: taiki-e/install-action@v2 + with: + tool: nextest@0.9 + if: ${{ matrix.test_task == 'zjit-check' }} + + - name: Run configure + run: ../src/configure -C --disable-install-doc ${{ matrix.configure }} + + - run: make + + - name: Verify that --zjit-dump-disasm works + run: | + ./miniruby --zjit-call-threshold=1 --zjit-dump-disasm -e0 | \ + wc -l | \ + ruby -ne 'raise "Disassembly seems broken in dev build (output has too few lines)" unless $_.to_i > 10' + if: ${{ contains(matrix.configure, 'jit=dev') }} + + - name: Set ENV for ZJIT + run: | + echo "RUBY_CRASH_REPORT=$(pwd)/rb_crash_%p.txt" >> $GITHUB_ENV + + - name: make ${{ matrix.test_task }} + run: | + set -x + make -s ${{ matrix.test_task }} ${TESTS:+TESTS="$TESTS"} \ + RUN_OPTS="$RUN_OPTS" \ + SPECOPTS="$SPECOPTS" \ + TESTOPTS="$TESTOPTS" + timeout-minutes: 60 + env: + RUBY_TESTOPTS: '-q --tty=no' + EXCLUDES: '../src/test/.excludes-zjit' + TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' + SYNTAX_SUGGEST_TIMEOUT: '5' + PRECHECK_BUNDLED_GEMS: 'no' + continue-on-error: ${{ matrix.continue-on-test_task || false }} + + - name: Dump crash logs + if: ${{ failure() }} + continue-on-error: true + run: | + tail --verbose --lines=+1 rb_crash_*.txt + exit 1 + + - uses: ./.github/actions/slack + with: + label: ${{ matrix.test_task }} ${{ matrix.configure }} + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + + result: + if: ${{ always() }} + name: ${{ github.workflow }} result + runs-on: macos-14 + needs: [make] + steps: + - run: exit 1 + working-directory: + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} + + # Separated from `make` job to avoid making it a required status check for now + ruby-bench: + strategy: + matrix: + include: + # Test --call-threshold=2 with 2 iterations in total + - ruby_opts: '--zjit-call-threshold=2' + bench_opts: '--warmup=1 --bench=1 --excludes=lobsters' + configure: '--enable-zjit=dev_nodebug' # --enable-zjit=dev is too slow + + runs-on: macos-14 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - uses: ./.github/actions/setup/macos + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + + - name: Run configure + run: ../src/configure -C --disable-install-doc --prefix="$(pwd)/install" ${{ matrix.configure }} + + - run: make install + + # setup/directories set MAKEFLAGS=-j4 for macOS, which randomly fails sqlite3.gem builds + - name: Unset MAKEFLAGS + run: echo "MAKEFLAGS=" >> "$GITHUB_ENV" + + - name: Checkout ruby-bench + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + repository: ruby/ruby-bench + path: ruby-bench + + # If you want to skip failing benchmark, consider using `--excludes`. + # e.g. `bench_opts: '--warmup=1 --bench=1 --excludes=railsbench,lobsters'` + - name: Run ruby-bench + run: ruby run_benchmarks.rb -e "zjit::../build/install/bin/ruby ${{ matrix.ruby_opts }}" ${{ matrix.bench_opts }} + working-directory: ruby-bench + + - uses: ./.github/actions/slack + with: + label: ruby-bench ${{ matrix.bench_opts }} ${{ matrix.ruby_opts }} + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + +defaults: + run: + working-directory: build diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml new file mode 100644 index 0000000000..28bfec963e --- /dev/null +++ b/.github/workflows/zjit-ubuntu.yml @@ -0,0 +1,267 @@ +name: ZJIT Ubuntu +on: + push: + branches: + - master + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + pull_request: + types: + - opened + - synchronize + - reopened + # Do not use paths-ignore for required status checks + # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + lint: + name: cargo clippy + + runs-on: ubuntu-22.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - run: cargo clippy --all-targets --all-features + working-directory: zjit + + make: + strategy: + fail-fast: false + matrix: + include: + - test_task: 'check' + run_opts: '--zjit-call-threshold=1' + specopts: '-T --zjit-call-threshold=1' + configure: '--enable-zjit=dev' + + - test_task: 'check' + run_opts: '--zjit-disable-hir-opt --zjit-call-threshold=1' + specopts: '-T --zjit-disable-hir-opt -T --zjit-call-threshold=1' + configure: '--enable-zjit=dev' + + # The optimizer benefits from at least 1 iteration of profiling. Also, many + # regression tests in bootstraptest/test_yjit.rb assume call-threshold=2. + - test_task: 'btest' + run_opts: '--zjit-call-threshold=2' + configure: '--enable-zjit=dev' + + - test_task: 'zjit-check' # zjit-test + quick feedback of test_zjit.rb + configure: '--enable-yjit --enable-zjit=dev' + rust_version: '1.85.0' + + - test_task: 'zjit-bindgen' + hint: 'To fix: use patch in logs' + # Build with YJIT+ZJIT for output that works in the most number of configurations + configure: '--enable-zjit=dev --enable-yjit --with-gcc=clang-16' + clang_path: '/usr/bin/clang-16' + runs-on: 'ubuntu-24.04' # for clang-16 + + - test_task: 'test-bundled-gems' + configure: '--enable-zjit=dev' + run_opts: '--zjit-call-threshold=1' + + env: + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + RUN_OPTS: ${{ matrix.run_opts }} + YJIT_BENCH_OPTS: ${{ matrix.yjit_bench_opts }} + SPECOPTS: ${{ matrix.specopts }} + TESTOPTS: ${{ matrix.testopts }} + RUBY_DEBUG: ci + BUNDLE_JOBS: 8 # for yjit-bench + RUST_BACKTRACE: 1 + ZJIT_RB_BUG: 1 + + runs-on: ${{ matrix.runs-on || 'ubuntu-22.04' }} + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - uses: ./.github/actions/setup/ubuntu + + - uses: ruby/setup-ruby@b90be12699fdfcbee4440c2bba85f6f460446bb0 # v1.279.0 + with: + ruby-version: '3.1' + bundler: none + + - uses: taiki-e/install-action@v2 + with: + tool: nextest@0.9 + if: ${{ matrix.test_task == 'zjit-check' }} + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + dummy-files: ${{ matrix.test_task == 'check' }} + # Set fetch-depth: 10 so that Launchable can receive commits information. + fetch-depth: 10 + + - name: Install Rust + if: ${{ matrix.rust_version }} + run: | + rustup install ${{ matrix.rust_version }} --profile minimal + rustup default ${{ matrix.rust_version }} + + - name: Install rustfmt + if: ${{ matrix.test_task == 'zjit-bindgen' }} + run: rustup component add rustfmt + + - name: Run configure + run: ../src/configure -C --disable-install-doc --prefix=$(pwd)/install ${{ matrix.configure }} + + - run: make incs + + - run: make prepare-gems + if: ${{ matrix.test_task == 'test-bundled-gems' }} + + - run: make + + - name: Verify that --zjit-dump-disasm works + run: | + ./miniruby --zjit-call-threshold=1 --zjit-dump-disasm -e0 | \ + wc -l | \ + ruby -ne 'raise "Disassembly seems broken in dev build (output has too few lines)" unless $_.to_i > 10' + if: ${{ contains(matrix.configure, 'jit=dev') }} + + # Check that the binary was built with ZJIT + - name: Check ZJIT enabled + run: ./miniruby --zjit -v | grep "+ZJIT" + if: ${{ matrix.configure != '--disable-zjit' }} + + - name: Set ENV for ZJIT + run: | + echo "RUBY_CRASH_REPORT=$(pwd)/rb_crash_%p.txt" >> $GITHUB_ENV + + - name: make ${{ matrix.test_task }} + run: | + set -x + make -s ${{ matrix.test_task }} ${TESTS:+TESTS="$TESTS"} \ + RUN_OPTS="$RUN_OPTS" MSPECOPT=--debug SPECOPTS="$SPECOPTS" \ + TESTOPTS="$TESTOPTS" \ + ZJIT_BINDGEN_DIFF_OPTS="$ZJIT_BINDGEN_DIFF_OPTS" + timeout-minutes: 90 + env: + RUBY_TESTOPTS: '-q --tty=no' + EXCLUDES: '../src/test/.excludes-zjit' + TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' + PRECHECK_BUNDLED_GEMS: 'no' + SYNTAX_SUGGEST_TIMEOUT: '5' + ZJIT_BINDGEN_DIFF_OPTS: '--exit-code' + CLANG_PATH: ${{ matrix.clang_path }} + continue-on-error: ${{ matrix.continue-on-test_task || false }} + + - name: Dump crash logs + if: ${{ failure() }} + continue-on-error: true + run: | + tail --verbose --lines=+1 rb_crash_*.txt + exit 1 + + - uses: ./.github/actions/slack + with: + label: ${{ matrix.test_task }} ${{ matrix.configure }} + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + + result: + if: ${{ always() }} + name: ${{ github.workflow }} result + runs-on: ubuntu-22.04 + needs: [make] + steps: + - name: ${{ github.workflow }} jobs have failed + run: exit 1 + working-directory: + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} + + # Separated from `make` job to avoid making it a required status check for now + ruby-bench: + strategy: + matrix: + include: + # Test --call-threshold=2 with 2 iterations in total + - ruby_opts: '--zjit-call-threshold=2' + bench_opts: '--warmup=1 --bench=1 --excludes=lobsters' + configure: '--enable-zjit=dev_nodebug' # --enable-zjit=dev is too slow + + runs-on: ubuntu-24.04 + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - uses: ./.github/actions/setup/ubuntu + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + + - name: Run configure + run: ../src/configure -C --disable-install-doc --prefix="$(pwd)/install" ${{ matrix.configure }} + + - run: make install + + - name: Checkout ruby-bench + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + repository: ruby/ruby-bench + path: ruby-bench + + # If you want to skip failing benchmark, consider using `--excludes`. + # e.g. `bench_opts: '--warmup=1 --bench=1 --excludes=railsbench,lobsters'` + - name: Run ruby-bench + run: ruby run_benchmarks.rb -e "zjit::../build/install/bin/ruby ${{ matrix.ruby_opts }}" ${{ matrix.bench_opts }} + working-directory: ruby-bench + + - uses: ./.github/actions/slack + with: + label: ruby-bench ${{ matrix.bench_opts }} ${{ matrix.ruby_opts }} + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + +defaults: + run: + working-directory: build diff --git a/.gitignore b/.gitignore index 30ab84f3f4..6cf5fb5f32 100644 --- a/.gitignore +++ b/.gitignore @@ -14,17 +14,20 @@ *.inc *.log *.o +*.o.tmp *.obj *.old *.orig *.pch *.pdb *.rbinc +*.rbbin *.rej *.s *.sav *.sl *.so +*.so.* *.swp *.yarb *~ @@ -48,6 +51,7 @@ y.tab.c *.gcno *.gcov *.vscode +!misc/.vscode lcov*.info # / @@ -121,6 +125,7 @@ lcov*.info /repack /revision.h /revision.tmp +/ripper.tmp.y /riscos /rubicon /ruby @@ -134,17 +139,19 @@ lcov*.info /test.rb /test-coverage.dat /tmp +/vcpkg_installed /transdb.h /uncommon.mk /verconf.h /verconf.mk /web -/yasmdata.rb # /bin/ /bin/*.exe /bin/*.dll +/bin/goruby +/bin/ruby # /benchmark/ /benchmark/bm_require.data @@ -160,6 +167,7 @@ lcov*.info # /coroutine/ !/coroutine/**/*.s +!/coroutine/**/*.S # /enc/trans/ /enc/trans/*.c @@ -199,8 +207,10 @@ lcov*.info # /ext/ripper/ /ext/ripper/eventids1.c +/ext/ripper/eventids1.h /ext/ripper/.eventids2-check /ext/ripper/eventids2table.c +/ext/ripper/ripper_init.c /ext/ripper/ripper.* /ext/ripper/ids1 /ext/ripper/ids2 @@ -219,7 +229,10 @@ lcov*.info /lib/ruby/[1-9]*.* /lib/ruby/vendor_ruby -# /spec/bundler +# /misc/ +/misc/**/__pycache__ + +# for `make test-bundler` /.rspec_status # /tool/ @@ -229,7 +242,39 @@ lcov*.info # /win32/ /win32/*.ico -# MJIT -/rb_mjit_header.h -/mjit_config.h -/include/ruby-*/*/rb_mjit_min_header-*.h + +# YJIT +/yjit-bench +/yjit_exit_locations.dump + +# Rust +/target + +# /wasm/ +/wasm/tests/*.wasm + +# prism +/lib/prism/compiler.rb +/lib/prism/dispatcher.rb +/lib/prism/dot_visitor.rb +/lib/prism/dsl.rb +/lib/prism/inspect_visitor.rb +/lib/prism/mutation_compiler.rb +/lib/prism/node.rb +/lib/prism/reflection.rb +/lib/prism/serialize.rb +/lib/prism/visitor.rb +/prism/api_node.c +/prism/ast.h +/prism/diagnostic.c +/prism/diagnostic.h +/prism/node.c +/prism/prettyprint.c +/prism/serialize.c +/prism/token_type.c +/prism/srcs.mk + +# tool/update-NEWS-gemlist.rb +/bundled_gems.json +/default_gems.json +/gems/default_gems diff --git a/.indent.pro b/.indent.pro new file mode 100644 index 0000000000..1d61cbcad1 --- /dev/null +++ b/.indent.pro @@ -0,0 +1,32 @@ +-bap +-nbbb +-nbc +-br +-brs +-nbs +-ncdb +-nce +-cdw +-cli2 +-cbi2 +-ndj +-ncs +-nfc1 +-i4 +-l120 +-lp +-npcs +-psl +-sc +-sob +-sbi4 +-nut +-par + +-TID +-TVALUE +-Tst_data_t +-Tst_index_t +-Tst_table +-Trb_data_type_t +-TFILE diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000..213a0f4916 --- /dev/null +++ b/.mailmap @@ -0,0 +1,431 @@ +git[bot] <svn-admin@ruby-lang.org> +git[bot] <svn-admin@ruby-lang.org> git <svn@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> +svn[bot] <svn-admin@ruby-lang.org> svn <svn@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# a_matsuda +Akira Matsuda <ronnie@dio.jp> +Akira Matsuda <ronnie@dio.jp> <a_matsuda@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# aamine +Minero Aoki <aamine@loveruby.net> +Minero Aoki <aamine@loveruby.net> <aamine@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# akira +akira yamada <akira@ruby-lang.org> +## akira yamada <akira@ruby-lang.org> <akira@rice.p.arika.org> +akira yamada <akira@ruby-lang.org> <akira@arika.org> +akira yamada <akira@ruby-lang.org> <akira@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# akiyoshi +AKIYOSHI, Masamichi <masamichi.akiyoshi@hp.com> +AKIYOSHI, Masamichi <masamichi.akiyoshi@hp.com> <akiyoshi@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# akr +Tanaka Akira <akr@fsij.org> +Tanaka Akira <akr@fsij.org> <akr@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# arai +Koji Arai <jca02266@nifty.ne.jp> +Koji Arai <jca02266@nifty.ne.jp> <arai@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# arton +Akio Tajima <artonx@yahoo.co.jp> +Akio Tajima <artonx@yahoo.co.jp> <arton@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# aycabta +aycabta <aycabta@gmail.com> +aycabta <aycabta@gmail.com> <aycabta@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# ayumin +Ayumu AIZAWA <ayumu.aizawa@gmail.com> +Ayumu AIZAWA <ayumu.aizawa@gmail.com> <ayumin@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# azav +Alexander Zavorine <alexandre.zavorine@nokia.com> +Alexander Zavorine <alexandre.zavorine@nokia.com> <azav@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# charliesome +Charlie Somerville <charliesome@ruby-lang.org> +Charlie Somerville <charliesome@ruby-lang.org> <charliesome@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# dave +Dave Thomas <dave@pragprog.com> +Dave Thomas <dave@pragprog.com> <dave@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# davidflanagan +David Flanagan <davidflanagan@ruby-lang.org> +David Flanagan <davidflanagan@ruby-lang.org> <david@think32> +David Flanagan <davidflanagan@ruby-lang.org> <david@davidflanagan.com> +David Flanagan <davidflanagan@ruby-lang.org> <davidflanagan@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# dblack +David A. Black <dblack@rubypal.com> +David A. Black <dblack@rubypal.com> <dblack@wobblini.net> +David A. Black <dblack@rubypal.com> <dblack@superlink.net> +David A. Black <dblack@rubypal.com> <dblack@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# drbrain +Eric Hodel <drbrain@segment7.net> +Eric Hodel <drbrain@segment7.net> <drbrain@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# duerst +Martin Dürst <duerst@it.aoyama.ac.jp> +Martin Dürst <duerst@it.aoyama.ac.jp> <duerst@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# eban +WATANABE Hirofumi <eban@ruby-lang.org> +WATANABE Hirofumi <eban@ruby-lang.org> <eban@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# emboss +Martin Bosslet <Martin.Bosslet@gmail.com> +Martin Bosslet <Martin.Bosslet@gmail.com> <Martin.Bosslet@googlemail.com> +Martin Bosslet <Martin.Bosslet@gmail.com> <emboss@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# eregon +Benoit Daloze <eregontp@gmail.com> +Benoit Daloze <eregontp@gmail.com> <eregon@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# evan +Evan Phoenix <evan@ruby-lang.org> +Evan Phoenix <evan@ruby-lang.org> <evan@fallingsnow.net> +Evan Phoenix <evan@ruby-lang.org> <evan@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# glass +Masaki Matsushita <glass.saga@gmail.com> +Masaki Matsushita <glass.saga@gmail.com> <glass@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# gogotanaka +Kazuki Tanaka <gogotanaka@ruby-lang.org> +Kazuki Tanaka <gogotanaka@ruby-lang.org> <gogotanaka@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# gotoken +Kentaro Goto <gotoken@gmail.com> +Kentaro Goto <gotoken@gmail.com> <gotoken@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# gotoyuzo +GOTOU Yuuzou <gotoyuzo@notwork.org> +GOTOU Yuuzou <gotoyuzo@notwork.org> <gotoyuzo@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# gsinclair +Gavin Sinclair <gsinclair@soyabean.com.au> +Gavin Sinclair <gsinclair@soyabean.com.au> <gsinclair@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# H_Konishi +KONISHI Hiromasa <konishih@fd6.so-net.ne.jp> +KONISHI Hiromasa <konishih@fd6.so-net.ne.jp> <H_Konishi@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# headius +Charles Oliver Nutter <headius@headius.com> +Charles Oliver Nutter <headius@headius.com> <headius@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# hone +Terence Lee <hone@heroku.com> +Terence Lee <hone@heroku.com> <hone@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# hsbt +Hiroshi SHIBATA <hsbt@ruby-lang.org> +Hiroshi SHIBATA <hsbt@ruby-lang.org> <hsbt@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# iwamatsu +Nobuhiro Iwamatsu <iwamatsu@nigauri.org> +Nobuhiro Iwamatsu <iwamatsu@nigauri.org> <iwamatsu@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# jeg2 +James Edward Gray II <james@graysoftinc.com> +James Edward Gray II <james@graysoftinc.com> <jeg2@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# jim +Jim Weirich <jim@tardis.local> +Jim Weirich <jim@tardis.local> <jim@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# k0kubun +Takashi Kokubun <takashikkbn@gmail.com> +Takashi Kokubun <takashikkbn@gmail.com> <k0kubun@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# kanemoto +Yutaka Kanemoto <kanemoto@ruby-lang.org> +Yutaka Kanemoto <kanemoto@ruby-lang.org> <kanemoto@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# katsu +UENO Katsuhiro <katsu@blue.sky.or.jp> +UENO Katsuhiro <katsu@blue.sky.or.jp> <katsu@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# kazu +Kazuhiro NISHIYAMA <zn@mbf.nifty.com> +Kazuhiro NISHIYAMA <zn@mbf.nifty.com> <kazu@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# keiju +Keiju Ishitsuka <keiju@ishitsuka.com> +Keiju Ishitsuka <keiju@ishitsuka.com> <keiju@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# knu +Akinori MUSHA <knu@iDaemons.org> +Akinori MUSHA <knu@iDaemons.org> <knu@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# ko1 +Koichi Sasada <ko1@atdot.net> +Koichi Sasada <ko1@atdot.net> <ko1@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# kosaki +KOSAKI Motohiro <kosaki.motohiro@gmail.com> +KOSAKI Motohiro <kosaki.motohiro@gmail.com> <kosaki@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# kosako +K.Kosako <sndgk393@ybb.ne.jp> +K.Kosako <sndgk393@ybb.ne.jp> <kosako@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# kou +Sutou Kouhei <kou@clear-code.com> +Sutou Kouhei <kou@clear-code.com> <kou@cozmixng.org> +Sutou Kouhei <kou@clear-code.com> <kou@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# kouji +Kouji Takao <kouji.takao@gmail.com> +Kouji Takao <kouji.takao@gmail.com> <kouji@takao7.net> +Kouji Takao <kouji.takao@gmail.com> <kouji@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# ksaito +Kazuo Saito <ksaito@uranus.dti.ne.jp> +Kazuo Saito <ksaito@uranus.dti.ne.jp> <ksaito@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# ktsj +Kazuki Tsujimoto <kazuki@callcc.net> +Kazuki Tsujimoto <kazuki@callcc.net> <ktsj@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# luislavena +Luis Lavena <luislavena@gmail.com> +Luis Lavena <luislavena@gmail.com> <luislavena@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# mame +Yusuke Endoh <mame@ruby-lang.org> +## Yusuke Endoh <mame@ruby-lang.org> <mame@tsg.ne.jp> +Yusuke Endoh <mame@ruby-lang.org> <mame@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# marcandre +Marc-Andre Lafortune <github@marc-andre.ca> +Marc-Andre Lafortune <ruby-core@marc-andre.ca> +Marc-Andre Lafortune <ruby-core@marc-andre.ca> <marcandre@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# matz +Yukihiro "Matz" Matsumoto <matz@ruby.or.jp> +Yukihiro "Matz" Matsumoto <matz@ruby.or.jp> <matz@ruby-lang.org> +Yukihiro "Matz" Matsumoto <matz@ruby.or.jp> <matz@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# michal +Michal Rokos <michal@ruby-lang.org> +Michal Rokos <michal@ruby-lang.org> <michal@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# mneumann +Michael Neumann <mneumann@ruby-lang.org> +Michael Neumann <mneumann@ruby-lang.org> <mneumann@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# mrkn +Kenta Murata <mrkn@mrkn.jp> +Kenta Murata <mrkn@mrkn.jp> <muraken@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> +Kenta Murata <mrkn@mrkn.jp> <3959+mrkn@users.noreply.github.com> +Kenta Murata <mrkn@mrkn.jp> <mrkn@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# nagachika +nagachika <nagachika@ruby-lang.org> +nagachika <nagachika@ruby-lang.org> <nagachika@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# nagai +Hidetoshi NAGAI <nagai@ai.kyutech.ac.jp> +Hidetoshi NAGAI <nagai@ai.kyutech.ac.jp> <nagai@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# nahi +Hiroshi Nakamura <nahi@ruby-lang.org> +Hiroshi Nakamura <nahi@ruby-lang.org> <nahi@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# nari +Narihiro Nakamura <authornari@gmail.com> +Narihiro Nakamura <authornari@gmail.com> <nari@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# naruse +NARUSE, Yui <naruse@airemix.jp> +NARUSE, Yui <naruse@airemix.jp> <naruse@ruby-lang.org> +NARUSE, Yui <naruse@airemix.jp> <naruse@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# ngoto +Naohisa Goto <ngotogenome@gmail.com> +Naohisa Goto <ngotogenome@gmail.com> <ngoto@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# nobu +Nobuyoshi Nakada <nobu@ruby-lang.org> +Nobuyoshi Nakada <nobu@ruby-lang.org> <nobu@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# normal +Eric Wong <normal@ruby-lang.org> +Eric Wong <normal@ruby-lang.org> <e@80x24.org> +Eric Wong <normal@ruby-lang.org> <normal@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# ntalbott +Nathaniel Talbott <ntalbott@ruby-lang.org> +Nathaniel Talbott <ntalbott@ruby-lang.org> <ntalbott@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# ocean +Hirokazu Yamamoto <ocean@m2.ccsnet.ne.jp> +Hirokazu Yamamoto <ocean@m2.ccsnet.ne.jp> <ocean@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# odaira +Rei Odaira <rodaira@us.ibm.com> +Rei Odaira <rodaira@us.ibm.com> <Rei.Odaira@gmail.com> +Rei Odaira <rodaira@us.ibm.com> <odaira@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# okkez +okkez <okkez000@gmail.com> +okkez <okkez000@gmail.com> <okkez@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# rhe +Kazuki Yamaguchi <k@rhe.jp> +Kazuki Yamaguchi <k@rhe.jp> <rhe@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# ryan +Ryan Davis <ryand-github@zenspider.com> +Ryan Davis <ryand-github@zenspider.com> <ryand-ruby@zenspider.com> +Ryan Davis <ryand-github@zenspider.com> <ryan@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# samuel +Samuel Williams <samuel.williams@oriontransfer.co.nz> +Samuel Williams <samuel.williams@oriontransfer.co.nz> <samuel@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# seki +Masatoshi SEKI <m_seki@mva.biglobe.ne.jp> +Masatoshi SEKI <m_seki@mva.biglobe.ne.jp> <seki@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# ser +Sean Russell <ser@germane-software.com> +Sean Russell <ser@germane-software.com> <ser@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# shigek +Shigeo Kobayashi <shigek@ruby-lang.org> +Shigeo Kobayashi <shigek@ruby-lang.org> <shigek@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# shirosaki +Hiroshi Shirosaki <h.shirosaki@gmail.com> +Hiroshi Shirosaki <h.shirosaki@gmail.com> <shirosaki@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# sho-h +Sho Hashimoto <sho-h@ruby-lang.org> +Sho Hashimoto <sho-h@ruby-lang.org> <sho-h@netlab.jp> +Sho Hashimoto <sho-h@ruby-lang.org> <sho.hsmt@gmail.com> +Sho Hashimoto <sho-h@ruby-lang.org> <sho-h@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# shugo +Shugo Maeda <shugo@ruby-lang.org> +Shugo Maeda <shugo@ruby-lang.org> <shugo@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# shyouhei +卜部昌平 <shyouhei@ruby-lang.org> +卜部昌平 <shyouhei@ruby-lang.org> <shyouhei@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# siena +Siena. <siena@faculty.chiba-u.jp> +Siena. <siena@faculty.chiba-u.jp> <siena@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# sonots +sonots <sonots@gmail.com> +sonots <sonots@gmail.com> <sonots@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# sorah +Sorah Fukumori <her@sorah.jp> +Sorah Fukumori <her@sorah.jp> <sorah@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# stomar +Marcus Stollsteimer <sto.mar@web.de> +Marcus Stollsteimer <sto.mar@web.de> <stomar@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# suke +Masaki Suketa <masaki.suketa@nifty.ne.jp> +Masaki Suketa <masaki.suketa@nifty.ne.jp> <suke@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# tadd +Tadashi Saito <tad.a.digger@gmail.com> +Tadashi Saito <tad.a.digger@gmail.com> <tadd@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# tadf +Tadayoshi Funaba <tadf@dotrb.org> +Tadayoshi Funaba <tadf@dotrb.org> <tadf@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# takano32 +TAKANO Mitsuhiro <takano32@gmail.com> +TAKANO Mitsuhiro <takano32@gmail.com> <takano32@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# tarui +Masaya Tarui <tarui@ruby-lang.org> +Masaya Tarui <tarui@ruby-lang.org> <tarui@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# technorama +Technorama Ltd. <oss-ruby@technorama.net> +Technorama Ltd. <oss-ruby@technorama.net> <technorama@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# tenderlove +Aaron Patterson <tenderlove@ruby-lang.org> +Aaron Patterson <tenderlove@ruby-lang.org> <tenderlove@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# tmm1 +Aman Gupta <ruby@tmm1.net> +Aman Gupta <ruby@tmm1.net> <tmm1@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# ts +Guy Decoux <ts@moulon.inra.fr> +Guy Decoux <ts@moulon.inra.fr> <ts@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# ttate +Takaaki Tateishi <ttate@ttsky.net> +## Takaaki Tateishi <ttate@ttsky.net> <ttate@kt.jaist.ac.jp> +Takaaki Tateishi <ttate@ttsky.net> <ttate@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# uema2 +Takaaki Uematsu <uema2x@jcom.home.ne.jp> +Takaaki Uematsu <uema2x@jcom.home.ne.jp> <uema2@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# usa +U.Nakamura <usa@ruby-lang.org> +U.Nakamura <usa@ruby-lang.org> <usa@garbagecollect.jp> +U.Nakamura <usa@ruby-lang.org> <usa@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# wakou +Wakou Aoyama <wakou@ruby-lang.org> +Wakou Aoyama <wakou@ruby-lang.org> <wakou@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# wanabe +wanabe <s.wanabe@gmail.com> +wanabe <s.wanabe@gmail.com> <wanabe@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# watson1978 +Watson <watson1978@gmail.com> +Watson <watson1978@gmail.com> <watson1978@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# wew +William Webber <william@williamwebber.com> +William Webber <william@williamwebber.com> <wew@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# why +why the lucky stiff <why@ruby-lang.org> +why the lucky stiff <why@ruby-lang.org> <why@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# xibbar +Takeyuki FUJIOKA <xibbar@ruby-lang.org> +Takeyuki FUJIOKA <xibbar@ruby-lang.org> <xibbar@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# yugui +Yuki Yugui Sonoda <yugui@yugui.jp> +Yuki Yugui Sonoda <yugui@yugui.jp> <yugui@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# yui-knk +yui-knk <spiketeika@gmail.com> +yui-knk <spiketeika@gmail.com> <yui-knk@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# yuki +Yuki Nishijima <yuki24@hey.com> +Yuki Nishijima <yuki24@hey.com> <mail@yukinishijima.net> +Yuki Nishijima <yuki24@hey.com> <yuki@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# zsombor +Dee Zsombor <zsombor@ruby-lang.org> +Dee Zsombor <zsombor@ruby-lang.org> <zsombor@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> + +# zzak +zzak <zzakscott@gmail.com> +zzak <zzakscott@gmail.com> <zzak@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> diff --git a/.rdoc_options b/.rdoc_options new file mode 100644 index 0000000000..89265cafd4 --- /dev/null +++ b/.rdoc_options @@ -0,0 +1,39 @@ +--- +page_dir: doc +charset: UTF-8 +encoding: UTF-8 +main_page: index.md +title: Documentation for Ruby development version +visibility: :private +rdoc_include: +- doc + +exclude: +- \.gemspec\z +- lib/set/subclass_compatible.rb + +autolink_excluded_words: +- Box +- Class +- Method +- Module +- Process +- RDoc +- Ruby +- Set +- ZJIT +- YJIT + +canonical_root: https://docs.ruby-lang.org/en/master + +footer_content: + Ruby: + Documentation: index.html + Official Website: https://www.ruby-lang.org/ + Playground: https://ruby.github.io/play-ruby/ + Resources: + GitHub: https://github.com/ruby/ruby + Issue Tracker: https://bugs.ruby-lang.org/projects/ruby-master/issues + RubyGems: https://rubygems.org/ + Community: + X: https://x.com/rubylangorg diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f8c66f715e..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,236 +0,0 @@ -# -*- YAML -*- -# Copyright (C) 2011 Urabe, Shyouhei. All rights reserved. -# -# This file is a part of the programming language Ruby. Permission is hereby -# granted, to either redistribute or modify this file, provided that the -# conditions mentioned in the file COPYING are met. Consult the file for -# details. - -# We only manage non-amd64 free pipelines. -# https://docs.travis-ci.com/user/billing-overview/ - -language: c - -os: linux - -if: commit_message !~ /^\[DOC\]/ - -dist: focal - -git: - quiet: true - -cache: - ccache: true - directories: - - $HOME/config_2nd - - $HOME/.downloaded-cache - -env: - global: - # The tests skipped in `make test-all`. - - TEST_ALL_SKIPPED_TESTS= - # The tests executed separately by `make test-all`. - - TEST_ALL_SEPARATED_TESTS= - # Reset timestamps early - - _=$(touch NEWS && find . -type f -exec touch -r NEWS {} +) - - CONFIGURE_TTY=no - - CCACHE_COMPILERCHECK=none - - CCACHE_NOCOMPRESS=1 - - CCACHE_MAXSIZE=512Mi - - NPROC="`nproc`" - # JOBS and SETARCH are overridden when necessary; see below. - - JOBS=-j$((1+${NPROC})) - - SETARCH= - - RUBY_PREFIX=/tmp/ruby-prefix - - GEMS_FOR_TEST='timezone tzinfo' - # https://github.com/travis-ci/travis-build/blob/e411371dda21430a60f61b8f3f57943d2fe4d344/lib/travis/build/bash/travis_apt_get_options.bash#L7 - - travis_apt_get_options='--allow-downgrades --allow-remove-essential --allow-change-held-packages' - - travis_apt_get_options="-yq --no-install-suggests --no-install-recommends $travis_apt_get_options" - # -O1 is faster than -O3 in our tests. - - optflags=-O1 - # -g0 disables backtraces when SEGV. Do not set that. - - debugflags=-ggdb3 - -.org.ruby-lang.ci.matrix-definitions: - - - &gcc-10 - compiler: gcc-10 - before_install: - - tool/travis_retry.sh sudo bash -c "rm -rf '${TRAVIS_ROOT}/var/lib/apt/lists/'* && exec apt-get update -yq" - - >- - tool/travis_retry.sh sudo -E apt-get $travis_apt_get_options install - ccache - gcc-10 - g++-10 - libffi-dev - libgdbm-dev - libncurses-dev - libncursesw5-dev - libreadline-dev - libssl-dev - libyaml-dev - openssl - zlib1g-dev - - # -------- - - - &arm64-linux - name: arm64-linux - arch: arm64 - <<: *gcc-10 - - - &ppc64le-linux - name: ppc64le-linux - arch: ppc64le - <<: *gcc-10 - - - &s390x-linux - name: s390x-linux - arch: s390x - <<: *gcc-10 - - - &arm32-linux - name: arm32-linux - arch: arm64 - # https://packages.ubuntu.com/focal/crossbuild-essential-armhf - compiler: arm-linux-gnueabihf-gcc - env: - - SETARCH='setarch linux32 --verbose --32bit' - # The "TestReadline#test_interrupt_in_other_thread" started failing on arm32 - # from https://www.travis-ci.com/github/ruby/ruby/jobs/529005145 - - TEST_ALL_SKIPPED_TESTS=test_interrupt_in_other_thread - before_install: - - sudo dpkg --add-architecture armhf - - tool/travis_retry.sh sudo bash -c "rm -rf '${TRAVIS_ROOT}/var/lib/apt/lists/'* && exec apt-get update -yq" - - >- - tool/travis_retry.sh sudo -E apt-get $travis_apt_get_options install - ccache - crossbuild-essential-armhf - libc6:armhf - libstdc++-10-dev:armhf - libffi-dev:armhf - libgdbm-dev:armhf - libncurses-dev:armhf - libncursesw5-dev:armhf - libreadline-dev:armhf - libssl-dev:armhf - linux-libc-dev:armhf - zlib1g-dev:armhf - -matrix: - include: - # Build every commit (Allowed Failures): - - <<: *arm32-linux - # Comment out as the 2nd arm64 pipeline is unstable. - # - <<: *arm64-linux - - <<: *ppc64le-linux - - <<: *s390x-linux - allow_failures: - # We see multiple errors indicating errors on the Travis environment itself in a short while: - # https://app.travis-ci.com/github/ruby/ruby/jobs/544382885 - # https://app.travis-ci.com/github/ruby/ruby/jobs/544361370 - # It's not a fault of Ruby's arm32 support but just Travis arm32 seems unsable. - - name: arm32-linux - # - name: arm64-linux - # We see "Some worker was crashed." in about 40% of recent ppc64le-linux jobs - # e.g. https://app.travis-ci.com/github/ruby/ruby/jobs/530959548 - - name: ppc64le-linux - # Tentatively disable, because often hungs up **after** all tests - # have finished successfully and saving caches. - - name: s390x-linux - fast_finish: true - -before_script: - - . tool/ci_functions.sh - - |- - if [ -n "${TEST_ALL_SKIPPED_TESTS}" ]; then - TEST_ALL_OPTS="${TEST_ALL_OPTS} $(ci_to_excluded_test_opts "${TEST_ALL_SKIPPED_TESTS}")" - if [ -z "${TEST_ALL_SEPARATED_TESTS}" ]; then - TEST_ALL_SEPARATED_TESTS="${TEST_ALL_SKIPPED_TESTS}" - fi - fi - - |- - if [ -n "${TEST_ALL_SEPARATED_TESTS}" ]; then - TEST_ALL_OPTS_SEPARATED="$(ci_to_included_test_opts "${TEST_ALL_SEPARATED_TESTS}")" - fi - - echo TEST_ALL_OPTS="${TEST_ALL_OPTS}" TEST_ALL_OPTS_SEPARATED="${TEST_ALL_OPTS_SEPARATED}" - - rm -fr .ext autom4te.cache - - |- - [ -d ~/.downloaded-cache ] || - mkdir ~/.downloaded-cache - - ln -s ~/.downloaded-cache - - "> config.status" - - "> .rbconfig.time" - - sed -f tool/prereq.status template/Makefile.in common.mk > Makefile - - make -s $JOBS up - - make -s $JOBS srcs - - rm -f config.status Makefile rbconfig.rb .rbconfig.time - - |- - if [ -d ~/config_2nd ]; then - cp -pr ~/config_2nd build - else - mkdir build - fi - - mkdir config_1st config_2nd - - chmod -R a-w . - - chmod -R u+w build config_1st config_2nd - - cd build - - |- - case "$CC" in - gcc*) CC="ccache $CC${GCC_FLAGS:+ }$GCC_FLAGS -fno-diagnostics-color";; - clang*) CC="ccache $CC${GCC_FLAGS:+ }$GCC_FLAGS -fno-color-diagnostics";; - esac - - |- - [ ! -f config.cache ] || - [ "$CC" = "`sed -n s/^ac_cv_prog_CC=//p config.cache`" ] || - (set -x; exec rm config.cache) - - $SETARCH ../configure -C --disable-install-doc --prefix=$RUBY_PREFIX $CONFIG_FLAG - - cp -pr config.cache config.status .ext/include ../config_1st - - $SETARCH make reconfig - - cp -pr config.cache config.status .ext/include ../config_2nd - - (cd .. && exec diff -ru config_1st config_2nd) - - chmod u+w .. - - rm -rf ~/config_2nd - - mv ../config_2nd ~ - - chmod u-w .. - - $SETARCH make -s $JOBS - - make -s install - - |- - [ -z "${GEMS_FOR_TEST}" ] || - $RUBY_PREFIX/bin/gem install --no-document $GEMS_FOR_TEST - - echo "raise 'do not load ~/.irbrc in test'" > ~/.irbrc - -script: - - $SETARCH make -s test -o showflags TESTOPTS="${TESTOPTS=$JOBS -q --tty=no}" - - ../tool/travis_wait.sh $SETARCH make -s test-all -o exts TESTOPTS="$JOBS -q --tty=no ${TEST_ALL_OPTS}" RUBYOPT="-w" - # Run the failing tests separately returning ok status to check if it works, - # visualize them. - - | - if [ -n "${TEST_ALL_OPTS_SEPARATED}" ]; then - $SETARCH make -s test-all -o exts TESTOPTS="$JOBS -v --tty=no ${TEST_ALL_OPTS_SEPARATED}" RUBYOPT="-w" || : - fi - - $SETARCH make -s test-spec MSPECOPT=-ff # not using `-j` because sometimes `mspec -j` silently dies - - $SETARCH make -s -o showflags leaked-globals - -# We enable Travis on the specific branches or forked repositories here. -if: (repo = ruby/ruby AND (branch = master OR branch =~ /^ruby_\d_\d$/)) OR repo != ruby/ruby - -# We want to be notified when something happens. -notifications: - irc: - channels: - - "chat.freenode.net#ruby-core" - on_success: change # [always|never|change] # default: always - on_failure: always # [always|never|change] # default: always - template: - - "%{message} by @%{author}: See %{build_url}" - - webhooks: - urls: - - secure: mRsoS/UbqDkKkW5p3AEqM27d4SZnV6Gsylo3bm8T/deltQzTsGzZwrm7OIBXZv0UFZdE68XmPlyHfZFLSP2V9QZ7apXMf9/vw0GtcSe1gchtnjpAPF6lYBn7nMCbVPPx9cS0dwL927fjdRM1vj7IKZ2bk4F0lAJ25R25S6teqdk= # ruby-lang slack: ruby/simpler-alerts-bot (travis) - on_success: never - on_failure: always - - email: - - jaruga@ruby-lang.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7363c106a2..35e79dd3b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1 @@ -Please see the [official issue tracker], [doc/contributing.rdoc] and wiki [HowToContribute]. - -[official issue tracker]: https://bugs.ruby-lang.org -[doc/contributing.rdoc]: contributing.rdoc -[HowToContribute]: https://bugs.ruby-lang.org/projects/ruby/wiki/HowToContribute +See ["Contributing to Ruby"](https://docs.ruby-lang.org/en/master/contributing/contributing_md.html), which includes setup and build instructions. @@ -1,3 +1,5 @@ +{日本語}[rdoc-ref:COPYING.ja] + Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>. You can redistribute it and/or modify it under either the terms of the 2-clause BSDL (see the file BSDL), or the conditions below: diff --git a/COPYING.ja b/COPYING.ja index 230376bc60..5de2dbcc8f 100644 --- a/COPYING.ja +++ b/COPYING.ja @@ -1,3 +1,5 @@ +{English}[rdoc-ref:COPYING] + 本プログラムはフリーソフトウェアです.2-clause BSDL または以下に示す条件で本プログラムを再配布できます 2-clause BSDLについてはBSDLファイルを参照して下さい. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..9a4b2ebbba --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,193 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "capstone" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "015ef5d5ca1743e3f94af9509ba6bd2886523cfee46e48d15c2ef5216fd4ac9a" +dependencies = [ + "capstone-sys", + "libc", +] + +[[package]] +name = "capstone-sys" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2267cb8d16a1e4197863ec4284ffd1aec26fe7e57c58af46b02590a0235809a0" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "cc" +version = "1.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +dependencies = [ + "shlex", +] + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "insta" +version = "1.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" +dependencies = [ + "console", + "once_cell", + "similar", +] + +[[package]] +name = "jit" +version = "0.1.0" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "ruby" +version = "0.0.0" +dependencies = [ + "yjit", + "zjit", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "similar" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "yjit" +version = "0.1.0" +dependencies = [ + "capstone", + "jit", +] + +[[package]] +name = "zjit" +version = "0.0.1" +dependencies = [ + "capstone", + "insta", + "jit", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..521129d92d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,60 @@ +# This is the root Cargo [workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html) +# and the root package for all the rust code that are statically linked into ruby. Rust tooling +# limitations means all Rust code need to share a single archive library (staticlib) at the +# integration point with non-rust code. (See rustlang/rust#44322 and #104707 for a taste of +# the linking challenges.) +# +# Do not add required dependencies. This is a policy that helps downstream consumers and give +# us tight control over what we ship. All of the optional dependencies are used exclusively +# during development. +# +# Release builds avoid Cargo entirely because offline builds can fail even when none of the +# optional dependencies are built (rust-lang/cargo#10352). + +[workspace] +members = ["zjit", "yjit", "jit"] + +[package] +name = "ruby" +version = "0.0.0" +edition = "2024" +rust-version = "1.85.0" +publish = false # Don't publish to crates.io + +[dependencies] +yjit = { path = "yjit", optional = true } +zjit = { path = "zjit", optional = true } + +[lib] +crate-type = ["staticlib"] +path = "ruby.rs" + +[features] +disasm = ["yjit?/disasm", "zjit?/disasm"] +runtime_checks = ["yjit?/runtime_checks", "zjit?/runtime_checks"] +yjit = [ "dep:yjit" ] +zjit = [ "dep:zjit" ] + +[profile.dev] +opt-level = 0 +debug = true +debug-assertions = true +overflow-checks = true + +[profile.dev_nodebug] +inherits = "dev" + +[profile.stats] +inherits = "release" + +[profile.release] +# NOTE: --enable-yjit and zjit builds use `rustc` without going through Cargo. You +# might want to update the `rustc` invocation if you change this profile. +opt-level = 3 +# The extra robustness that comes from checking for arithmetic overflow is +# worth the performance cost for the compiler. +overflow-checks = true +# Generate debug info +debug = true +# Use ThinLTO. Much smaller output for a small amount of build time increase. +lto = "thin" @@ -58,12 +58,12 @@ mentioned below. [ccan/list/list.h] - This file is licensed under the {MIT License}[rdoc-label:label-MIT+License]. + This file is licensed under the {MIT License}[rdoc-ref:@MIT+License]. [coroutine] Unless otherwise specified, these files are licensed under the - {MIT License}[rdoc-label:label-MIT+License]. + {MIT License}[rdoc-ref:@MIT+License]. [include/ruby/onigmo.h] [include/ruby/oniguruma.h] @@ -546,7 +546,7 @@ mentioned below. [vsnprintf.c] - This file is under the {old-style BSD license}[rdoc-label:label-Old-style+BSD+license]. + This file is under the {old-style BSD license}[rdoc-ref:@Old-style+BSD+license]. >>> Copyright (c) 1990, 1993:: @@ -577,7 +577,7 @@ mentioned below. [missing/crypt.c] - This file is under the {old-style BSD license}[rdoc-label:label-Old-style+BSD+license]. + This file is under the {old-style BSD license}[rdoc-ref:@Old-style+BSD+license]. >>> Copyright (c) 1989, 1993:: @@ -588,7 +588,7 @@ mentioned below. [missing/setproctitle.c] - This file is under the {old-style BSD license}[rdoc-label:label-Old-style+BSD+license]. + This file is under the {old-style BSD license}[rdoc-ref:@Old-style+BSD+license]. >>> Copyright 2003:: Damien Miller @@ -702,54 +702,27 @@ mentioned below. OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -[ext/json/generator/generator.c] +[ext/json/vendor/fpconv.c] - The file contains the following copyright notice. + This file is under the {Boost Software License}[rdoc-ref:@Boost+Software+License+1.0]. - >>> - Copyright 2001-2004:: Unicode, Inc. - - Disclaimer:: - - This source code is provided as is by Unicode, Inc. No claims are - made as to fitness for any particular purpose. No warranties of any - kind are expressed or implied. The recipient agrees to determine - applicability of information provided. If this file has been - purchased on magnetic or optical media from Unicode, Inc., the - sole remedy for any claim will be exchange of defective media - within 90 days of receipt. - - Limitations on Rights to Redistribute This Code:: - - Unicode, Inc. hereby grants the right to freely use the information - supplied in this file in the creation of products supporting the - Unicode Standard, and to make copies of this file in any form - for internal or external distribution as long as this notice - remains attached. - -[ext/nkf/nkf-utf8/config.h] -[ext/nkf/nkf-utf8/nkf.c] -[ext/nkf/nkf-utf8/utf8tbl.c] - - These files are under the following license. So to speak, it is - copyrighted semi-public-domain software. +[ext/json/vendor/jeaiii-ltoa.h] >>> - Copyright (C) 1987:: Fujitsu LTD. (Itaru ICHIKAWA) + Copyright (c) 2024,2025 Enrico Thierbach - https://github.com/radiospiel + Copyright (c) 2022 James Edward Anhalt III - https://github.com/jeaiii/itoa + + {MIT License}[rdoc-ref:@MIT+License] - Everyone is permitted to do anything on this program - including copying, modifying, improving, - as long as you don't try to pretend that you wrote it. - i.e., the above copyright notice has to appear in all copies. - Binary distribution requires original version messages. - You don't have to ask before copying, redistribution or publishing. - THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE. +[ext/json/vendor/ryu.h] + This file is adapted from the Ryu algorithm by Ulf Adams https://github.com/ulfjack/ryu. + It is dual-licensed under {Apache License 2.0}[rdoc-ref:@Apache+License+2.0] OR + {Boost Software License 1.0}[rdoc-ref:@Boost+Software+License+1.0]. [ext/psych] [test/psych] - The files under these directories are under the following license, except for - ext/psych/yaml. + The files under these directories are under the following license. >>> Copyright 2009:: Aaron Patterson, et al. @@ -772,31 +745,6 @@ mentioned below. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -[ext/psych/yaml] - - The files under this directory are under the following license. - - >>> - Copyright (c) 2006:: Kirill Simonov - - Permission is hereby granted, free of charge, to any person obtaining a copy of - this software and associated documentation files (the "Software"), to deal in - the Software without restriction, including without limitation the rights to - use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies - of the Software, and to permit persons to whom the Software is furnished to do - so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - [ext/pty/pty.c] >>> @@ -905,7 +853,7 @@ mentioned below. >>> RubyGems is copyrighted free software by Chad Fowler, Rich Kilmer, Jim Weirich and others. You can redistribute it and/or modify it under - either the terms of the {MIT license}[rdoc-label:label-MIT+License], or the conditions + either the terms of the {MIT license}[rdoc-ref:@MIT+License], or the conditions below: 1. You may make and give away verbatim copies of the source form of the @@ -967,7 +915,7 @@ mentioned below. Portions copyright (c) 2010:: Andre Arko Portions copyright (c) 2009:: Engine Yard - {MIT License}[rdoc-label:label-MIT+License] + {MIT License}[rdoc-ref:@MIT+License] [lib/bundler/vendor/thor] @@ -976,17 +924,25 @@ mentioned below. >>> Copyright (c) 2008 Yehuda Katz, Eric Hodel, et al. - {MIT License}[rdoc-label:label-MIT+License] + {MIT License}[rdoc-ref:@MIT+License] -[lib/rubygems/resolver/molinillo] -[lib/bundler/vendor/molinillo] +[lib/rubygems/vendor/molinillo] molinillo is under the following license. >>> Copyright (c) 2014 Samuel E. Giddins segiddins@segiddins.me - {MIT License}[rdoc-label:label-MIT+License] + {MIT License}[rdoc-ref:@MIT+License] + +[lib/bundler/vendor/pub_grub] + + pub_grub is under the following license. + + >>> + Copyright (c) 2018 John Hawthorn + + {MIT License}[rdoc-ref:@MIT+License] [lib/bundler/vendor/connection_pool] @@ -995,7 +951,7 @@ mentioned below. >>> Copyright (c) 2011 Mike Perham - {MIT License}[rdoc-label:label-MIT+License] + {MIT License}[rdoc-ref:@MIT+License] [lib/bundler/vendor/net-http-persistent] @@ -1004,7 +960,7 @@ mentioned below. >>> Copyright (c) Eric Hodel, Aaron Patterson - {MIT License}[rdoc-label:label-MIT+License] + {MIT License}[rdoc-ref:@MIT+License] [lib/did_you_mean] [lib/did_you_mean.rb] @@ -1015,7 +971,7 @@ mentioned below. >>> Copyright (c) 2014-2016 Yuki Nishijima - {MIT License}[rdoc-label:label-MIT+License] + {MIT License}[rdoc-ref:@MIT+License] [lib/error_highlight] [lib/error_highlight.rb] @@ -1026,7 +982,7 @@ mentioned below. >>> Copyright (c) 2021 Yusuke Endoh - {MIT License}[rdoc-label:label-MIT+License] + {MIT License}[rdoc-ref:@MIT+License] [benchmark/so_ackermann.rb] [benchmark/so_array.rb] @@ -1109,3 +1065,236 @@ mentioned below. From ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change paragraph 3 above is now null and void. + +== Boost Software License 1.0 + +>>> + Boost Software License - Version 1.0 - August 17th, 2003 + + Permission is hereby granted, free of charge, to any person or organization + obtaining a copy of the software and accompanying documentation covered by + this license (the "Software") to use, reproduce, display, distribute, + execute, and transmit the Software, and to prepare derivative works of the + Software, and to permit third-parties to whom the Software is furnished to + do so, all subject to the following: + + The copyright notices in the Software and this entire statement, including + the above license grant, this restriction and the following disclaimer, + must be included in all copies of the Software, in whole or in part, and + all derivative works of the Software, unless such copies or derivative + works are solely in the form of machine-executable object code generated by + a source language processor. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + +== Apache License 2.0 + +>>> + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + a. You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + b. You must cause any modified files to carry prominent notices + stating that You changed the files; and + + c. You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + d. If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + >>> + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. @@ -1,657 +1,87 @@ -# NEWS for Ruby 3.1.0 +# NEWS for Ruby 4.1.0 This document is a list of user-visible feature changes -since the **3.0.0** release, except for bug fixes. +since the **4.0.0** release, except for bug fixes. Note that each entry is kept to a minimum, see links for details. ## Language changes -* The block argument can now be anonymous if the block will - only be passed to another method. [[Feature #11256]] - - ```ruby - def foo(&) - bar(&) - end - ``` - -* Pin operator now takes an expression. [[Feature #17411]] - - ```ruby - Prime.each_cons(2).lazy.find_all{_1 in [n, ^(n + 2)]}.take(3).to_a - #=> [[3, 5], [5, 7], [11, 13]] - ``` - -* Pin operator now supports instance, class, and global variables. - [[Feature #17724]] - - ```ruby - @n = 5 - Prime.each_cons(2).lazy.find{_1 in [n, ^@n]} - #=> [3, 5] - ``` - -* One-line pattern matching is no longer experimental. - -* Parentheses can be omitted in one-line pattern matching. - [[Feature #16182]] - - ```ruby - [0, 1] => _, x - {y: 2} => y: - x #=> 1 - y #=> 2 - ``` - -* Multiple assignment evaluation order has been made consistent with - single assignment evaluation order. With single assignment, Ruby - uses a left-to-right evaluation order. With this code: - - ```ruby - foo[0] = bar - ``` - - The following evaluation order is used: - - 1. `foo` - 2. `bar` - 3. `[]=` called on the result of `foo` - - In Ruby before 3.1.0, multiple assignment did not follow this - evaluation order. With this code: - - ```ruby - foo[0], bar.baz = a, b - ``` - - Versions of Ruby before 3.1.0 would evaluate in the following - order - - 1. `a` - 2. `b` - 3. `foo` - 4. `[]=` called on the result of `foo` - 5. `bar` - 6. `baz=` called on the result of `bar` - - Starting in Ruby 3.1.0, the evaluation order is now consistent with - single assignment, with the left-hand side being evaluated before - the right-hand side: - - 1. `foo` - 2. `bar` - 3. `a` - 4. `b` - 5. `[]=` called on the result of `foo` - 6. `baz=` called on the result of `bar` - - [[Bug #4443]] - -* Values in Hash literals and keyword arguments can be omitted. - [[Feature #14579]] - - For example, - - * `{x:, y:}` is a syntax sugar of `{x: x, y: y}`. - * `foo(x:, y:)` is a syntax sugar of `foo(x: x, y: y)`. - - Constant names, local variable names, and method names are allowed as - key names. Note that a reserved word is considered as a local - variable or method name even if it's a pseudo variable name such as - `self`. - -* Non main-Ractors can get instance variables (ivars) of classes/modules - if ivars refer to shareable objects. - [[Feature #17592]] - -* A command syntax is allowed in endless method definitions, i.e., - you can now write `def foo = puts "Hello"`. - Note that `private def foo = puts "Hello"` does not parse. - [[Feature #17398]] - -## Command line options - -* `--disable-gems` is now explicitly declared as "just for debugging". - Never use it in any real-world codebase. - [[Feature #17684]] - ## Core classes updates Note: We're only listing outstanding class updates. -* Array - - * Array#intersect? is added. [[Feature #15198]] - -* Class - - * Class#subclasses, which returns an array of classes - directly inheriting from the receiver, not - including singleton classes. - [[Feature #18273]] - - ```ruby - class A; end - class B < A; end - class C < B; end - class D < A; end - A.subclasses #=> [D, B] - B.subclasses #=> [C] - C.subclasses #=> [] - ``` - -* Enumerable - - * Enumerable#compact is added. [[Feature #17312]] - - * Enumerable#tally now accepts an optional hash to count. [[Feature #17744]] - - * Enumerable#each_cons and each_slice to return a receiver. [[GH-1509]] - - ```ruby - [1, 2, 3].each_cons(2){} - # 3.0 => nil - # 3.1 => [1, 2, 3] - - [1, 2, 3].each_slice(2){} - # 3.0 => nil - # 3.1 => [1, 2, 3] - ``` - -* Enumerator::Lazy - - * Enumerator::Lazy#compact is added. [[Feature #17312]] - -* File - - * File.dirname now accepts an optional argument for the level to - strip path components. [[Feature #12194]] - -* GC - - * "GC.measure_total_time = true" enables the measurement of GC. - Measurement can introduce overhead. It is enabled by default. - GC.measure_total_time returns the current setting. - GC.stat[:time] or GC.stat(:time) returns measured time - in milli-seconds. [[[Feature #10917]]] - - * GC.total_time returns measured time in nano-seconds. [[[Feature #10917]]] - -* Integer - - * Integer.try_convert is added. [[Feature #15211]] - -* Kernel - - - * Kernel#load now accepts a module as the second argument, - and will load the file using the given module as the - top-level module. [[Feature #6210]] - -* Marshal - - * Marshal.load now accepts a `freeze: true` option. - All returned objects are frozen except for `Class` and - `Module` instances. Strings are deduplicated. [[Feature #18148]] - -* MatchData - - * MatchData#match is added [[Feature #18172]] - - * MatchData#match_length is added [[Feature #18172]] - -* Method / UnboundMethod - - * Method#public?, Method#private?, Method#protected?, - UnboundMethod#public?, UnboundMethod#private?, - UnboundMethod#protected? have been added. [[Feature #11689]] - -* Module - - * Module#prepend now modifies the ancestor chain if the receiver - already includes the argument. Module#prepend still does not - modify the ancestor chain if the receiver has already prepended - the argument. [[Bug #17423]] - - * Module#private, #public, #protected, and #module_function will - now return their arguments. If a single argument is given, it - is returned. If no arguments are given, nil is returned. If - multiple arguments are given, they are returned as an array. - [[Feature #12495]] - -* Process - - * Process.\_fork is added. This is a core method for fork(2). - Do not call this method directly; it is called by existing - fork methods: Kernel.#fork, Process.fork, and IO.popen("-"). - Application monitoring libraries can overwrite this method to - hook fork events. [[Feature #17795]] - -* Struct - - * Passing only keyword arguments to Struct#initialize is warned. - You need to use a Hash literal to set a Hash to a first member. - [[Feature #16806]] - - * StructClass#keyword_init? is added [[Feature #18008]] +* Method -* String + * `Method#source_location`, `Proc#source_location`, and + `UnboundMethod#source_location` now return extended location + information with 5 elements: `[path, start_line, start_column, + end_line, end_column]`. The previous 2-element format `[path, + line]` can still be obtained by calling `.take(2)` on the result. + [[Feature #6012]] - * Update Unicode version to 13.0.0 [[Feature #17750]] - and Emoji version to 13.0 [[Feature #18029]] +* Set - * String#unpack and String#unpack1 now accept an `offset:` keyword - argument to start the unpacking after an arbitrary number of bytes - have been skipped. If `offset` is outside of the string bounds - `ArgumentError` is raised. [[Feature #18254]] + * A deprecated behavior, `Set#to_set`, `Range#to_set`, and + `Enumerable#to_set` accepting arguments, was removed. [[Feature #21390]] -* Thread - - * Thread#native_thread_id is added. [[Feature #17853]] - -* Thread::Backtrace - - * Thread::Backtrace.limit, which returns the value to limit backtrace - length set by `--backtrace-limit` command line option, is added. - [[Feature #17479]] - -* Thread::Queue - - * Thread::Queue.new now accepts an Enumerable of initial values. - [[Feature #17327]] - -* Time - - * Time.new now accepts optional `in:` keyword argument for the - timezone, as well as `Time.at` and `Time.now`, so that is now - you can omit minor arguments to `Time.new`. [[Feature #17485]] - - ```ruby - Time.new(2021, 12, 25, in: "+07:00") - #=> 2021-12-25 00:00:00 +0700 - ``` - - At the same time, time component strings are converted to - integers more strictly now. - - ```ruby - Time.new(2021, 12, 25, "+07:30") - #=> invalid value for Integer(): "+07:30" (ArgumentError) - ``` - - Ruby 3.0 or earlier returned probably unexpected result - `2021-12-25 07:00:00`, not `2021-12-25 07:30:00` nor - `2021-12-25 00:00:00 +07:30`. - - * Time#strftime supports RFC 3339 UTC for unknown offset local - time, `-0000`, as `%-z`. [[Feature #17544]] - -* TracePoint +## Stdlib updates - * TracePoint.allow_reentry is added to allow reenter while TracePoint - callback. - [[Feature #15912]] +### The following bundled gems are added. -* $LOAD_PATH - * $LOAD_PATH.resolve_feature_path does not raise. [[Feature #16043]] +We only list stdlib changes that are notable feature changes. -* Fiber Scheduler +Other changes are listed in the following sections. We also listed release +history from the previous bundled version that is Ruby 3.4.0 if it has GitHub +releases. - * Add support for `Addrinfo.getaddrinfo` using `address_resolve` hook. - [[Feature #17370]] +### The following bundled gem is promoted from default gems. - * Introduce non-blocking `Timeout.timeout` using `timeout_after` hook. - [[Feature #17470]] +* tsort 0.2.0 - * Introduce new scheduler hooks `io_read` and `io_write` along with a - low level `IO::Buffer` for zero-copy read/write. [[Feature #18020]] +### The following default gem is added. - * IO hooks `io_wait`, `io_read`, `io_write`, receive the original IO object - where possible. [[Bug #18003]] +### The following default gems are updated. - * Make `Monitor` fiber-safe. [[Bug #17827]] +* RubyGems 4.1.0.dev +* bundler 4.1.0.dev +* prism 1.8.0 +* stringio 3.2.1.dev +* strscan 3.1.7.dev - * Replace copy coroutine with pthread implementation. [[Feature #18015]] +### The following bundled gems are updated. -* Refinement +* minitest 6.0.1 +* test-unit 3.7.7 +* rss 0.3.2 +* net-imap 0.6.2 +* rbs 3.10.2 +* typeprof 0.31.1 +* debug 1.11.1 +* mutex_m 0.3.0 +* resolv-replace 0.2.0 +* rdoc 7.1.0 - * New class which represents a module created by Module#refine. - `include` and `prepend` are deprecated, and `import_methods` is added - instead. [[Bug #17429]] +### RubyGems and Bundler -## Stdlib updates +Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for details. -* The following default gem are updated. - * RubyGems 3.3.3 - * base64 0.1.1 - * benchmark 0.2.0 - * bigdecimal 3.1.1 - * bundler 2.3.3 - * cgi 0.3.1 - * csv 3.2.2 - * date 3.2.2 - * did_you_mean 1.6.1 - * digest 3.1.0 - * drb 2.1.0 - * erb 2.2.3 - * error_highlight 0.3.0 - * etc 1.3.0 - * fcntl 1.0.1 - * fiddle 1.1.0 - * fileutils 1.6.0 - * find 0.1.1 - * io-console 0.5.10 - * io-wait 0.2.1 - * ipaddr 1.2.3 - * irb 1.4.1 - * json 2.6.1 - * logger 1.5.0 - * net-http 0.2.0 - * net-protocol 0.1.2 - * nkf 0.1.1 - * open-uri 0.2.0 - * openssl 3.0.0 - * optparse 0.2.0 - * ostruct 0.5.2 - * pathname 0.2.0 - * pp 0.3.0 - * prettyprint 0.1.1 - * psych 4.0.3 - * racc 1.6.0 - * rdoc 6.4.0 - * readline 0.0.3 - * readline-ext 0.1.4 - * reline 0.3.0 - * resolv 0.2.1 - * rinda 0.1.1 - * ruby2_keywords 0.0.5 - * securerandom 0.1.1 - * set 1.0.2 - * stringio 3.0.1 - * strscan 3.0.1 - * tempfile 0.1.2 - * time 0.2.0 - * timeout 0.2.0 - * tmpdir 0.1.2 - * un 0.2.0 - * uri 0.11.0 - * yaml 0.2.0 - * zlib 2.1.1 -* The following bundled gems are updated. - * minitest 5.15.0 - * power_assert 2.0.1 - * rake 13.0.6 - * test-unit 3.5.3 - * rexml 3.2.5 - * rbs 2.1.0 - * typeprof 0.21.2 -* The following default gems are now bundled gems. - * net-ftp 0.1.3 - * net-imap 0.2.3 - * net-pop 0.1.1 - * net-smtp 0.3.1 - * matrix 0.4.2 - * prime 0.1.2 - * debug 1.4.0 - -* Coverage measurement now supports suspension. You can use `Coverage.suspend` - to stop the measurement temporarily, and `Coverage.resume` to restart it. - See [[Feature #18176]] in detail. - -* Random::Formatter is moved to random/formatter.rb, so that you can - use `Random#hex`, `Random#base64`, and so on without SecureRandom. - [[Feature #18190]] +## Supported platforms ## Compatibility issues -Note: Excluding feature bug fixes. - -* `rb_io_wait_readable`, `rb_io_wait_writable` and `rb_wait_for_single_fd` are - deprecated in favour of `rb_io_maybe_wait_readable`, - `rb_io_maybe_wait_writable` and `rb_io_maybe_wait` respectively. - `rb_thread_wait_fd` and `rb_thread_fd_writable` are deprecated. [[Bug #18003]] - ## Stdlib compatibility issues -* `ERB#initialize` warns `safe_level` and later arguments even without -w. - [[Feature #14256]] - -* `lib/debug.rb` is replaced with `debug.gem` - -* `Kernel#pp` in `lib/pp.rb` uses the width of `IO#winsize` by default. - This means that the output width is automatically changed depending on - your terminal size. [[Feature #12913]] - -* Psych 4.0 changes `Psych.load` as `safe_load` by the default. - You may need to use Psych 3.3.2 for migrating to this behavior. - [[Bug #17866]] - ## C API updates -* Documented. [[GH-4815]] - -* `rb_gc_force_recycle` is deprecated and has been changed to a no-op. - [[Feature #18290]] - ## Implementation improvements -* Inline cache mechanism is introduced for reading class variables. - [[Feature #17763]] - -* `instance_eval` and `instance_exec` now only allocate a singleton class when - required, avoiding extra objects and improving performance. [[GH-5146]] +### Ractor -* The performance of `Struct` accessors is improved. [[GH-5131]] - -* `mandatory_only?` builtin special form to improve performance on - builtin methods. [[GH-5112]] - -* Experimental feature Variable Width Allocation in the garbage collector. - This feature is turned off by default and can be enabled by compiling Ruby - with flag `USE_RVARGC=1` set. [[Feature #18045]] [[Feature #18239]] +A lot of work has gone into making Ractors more stable, performant, and usable. These improvements bring Ractor implementation closer to leaving experimental status. ## JIT -* Rename Ruby 3.0's `--jit` to `--mjit`, and alias `--jit` to `--yjit` - on non-Windows x86-64 platforms and to `--mjit` on others. - -### MJIT - -* The default `--mjit-max-cache` is changed from 100 to 10000. - -* JIT-ed code is no longer cancelled when a TracePoint for class events - is enabled. - -* The JIT compiler no longer skips compilation of methods longer than - 1000 instructions. - -* `--mjit-verbose` and `--mjit-warning` output "JIT cancel" when JIT-ed - code is disabled because TracePoint or GC.compact is used. - -### YJIT: New experimental in-process JIT compiler - -New JIT compiler available as an experimental feature. [[Feature #18229]] - -See [this blog post](https://shopify.engineering/yjit-just-in-time-compiler-cruby -) introducing the project. - -* Disabled by default, use `--yjit` command-line option to enable YJIT. - -* Performance improvements on benchmarks based on real-world software, - up to 22% on railsbench, 39% on liquid-render. - -* Fast warm-up times. - -* Limited to Unix-like x86-64 platforms for now. - -## Static analysis - -### RBS - -* Generics type parameters can be bounded ([PR](https://github.com/ruby/rbs/pull/844)). - - ```rbs - # `T` must be compatible with the `_Output` interface. - # `PrettyPrint[String]` is ok, but `PrettyPrint[Integer]` is a type error. - class PrettyPrint[T < _Output] - interface _Output - def <<: (String) -> void - end - - attr_reader output: T - - def initialize: (T output) -> void - end - ``` - -* Type aliases can be generic. ([PR](https://github.com/ruby/rbs/pull/823)) - - ```rbs - # Defines a generic type `list`. - type list[T] = [ T, list[T] ] - | nil - - type str_list = list[String] - type int_list = list[Integer] - ``` - -* [rbs collection](https://github.com/ruby/rbs/blob/master/docs/collection.md) has been introduced to manage gems’ RBSs. - -* Many signatures for built-in and standard libraries have been added/updated. - -* It includes many bug fixes and performance improvements too. - -See the [CHANGELOG.md](https://github.com/ruby/rbs/blob/master/CHANGELOG.md) for more information. - -### TypeProf - -* [Experimental IDE support](https://github.com/ruby/typeprof/blob/master/doc/ide.md) has been implemented. -* Many bug fixes and performance improvements since Ruby 3.0.0. - -## Debugger - -* A new debugger [debug.gem](https://github.com/ruby/debug) is bundled. - debug.gem is a fast debugger implementation, and it provides many features - like remote debugging, colorful REPL, IDE (VSCode) integration, and more. - It replaces `lib/debug.rb` standard library. - -* `rdbg` command is also installed into `bin/` directory to start and control - debugging execution. - -## error_highlight - -A built-in gem called error_highlight has been introduced. -It shows fine-grained error locations in the backtrace. - -Example: `title = json[:article][:title]` - -If `json` is nil, it shows: - -``` -$ ruby test.rb -test.rb:2:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError) - -title = json[:article][:title] - ^^^^^^^^^^ -``` - -If `json[:article]` returns nil, it shows: - -``` -$ ruby test.rb -test.rb:2:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError) - -title = json[:article][:title] - ^^^^^^^^ -``` - -This feature is enabled by default. -You can disable it by using a command-line option `--disable-error_highlight`. -See [the repository](https://github.com/ruby/error_highlight) in detail. - -## IRB Autocomplete and Document Display - -The IRB now has an autocomplete feature, where you can just type in the code, and the completion candidates dialog will appear. You can use Tab and Shift+Tab to move up and down. - -If documents are installed when you select a completion candidate, the documentation dialog will appear next to the completion candidates dialog, showing part of the content. You can read the full document by pressing Alt+d. - -## Miscellaneous changes - -* lib/objspace/trace.rb is added, which is a tool for tracing the object - allocation. Just by requiring this file, tracing is started *immediately*. - Just by `Kernel#p`, you can investigate where an object was created. - Note that just requiring this file brings a large performance overhead. - This is only for debugging purposes. Do not use this in production. - [[Feature #17762]] - -* Now exceptions raised in finalizers will be printed to `STDERR`, unless - `$VERBOSE` is `nil`. [[Feature #17798]] - -* `ruby -run -e httpd` displays URLs to access. [[Feature #17847]] - -* Add `ruby -run -e colorize` to colorize Ruby code using - `IRB::Color.colorize_code`. - -[Bug #4443]: https://bugs.ruby-lang.org/issues/4443 -[Feature #6210]: https://bugs.ruby-lang.org/issues/6210 -[Feature #10917]: https://bugs.ruby-lang.org/issues/10917 -[Feature #11256]: https://bugs.ruby-lang.org/issues/11256 -[Feature #11689]: https://bugs.ruby-lang.org/issues/11689 -[Feature #12194]: https://bugs.ruby-lang.org/issues/12194 -[Feature #12495]: https://bugs.ruby-lang.org/issues/12495 -[Feature #12913]: https://bugs.ruby-lang.org/issues/12913 -[Feature #14256]: https://bugs.ruby-lang.org/issues/14256 -[Feature #14579]: https://bugs.ruby-lang.org/issues/14579 -[Feature #15198]: https://bugs.ruby-lang.org/issues/15198 -[Feature #15211]: https://bugs.ruby-lang.org/issues/15211 -[Feature #15912]: https://bugs.ruby-lang.org/issues/15912 -[Feature #16043]: https://bugs.ruby-lang.org/issues/16043 -[Feature #16182]: https://bugs.ruby-lang.org/issues/16182 -[Feature #16806]: https://bugs.ruby-lang.org/issues/16806 -[Feature #17312]: https://bugs.ruby-lang.org/issues/17312 -[Feature #17327]: https://bugs.ruby-lang.org/issues/17327 -[Feature #17370]: https://bugs.ruby-lang.org/issues/17370 -[Feature #17398]: https://bugs.ruby-lang.org/issues/17398 -[Feature #17411]: https://bugs.ruby-lang.org/issues/17411 -[Bug #17423]: https://bugs.ruby-lang.org/issues/17423 -[Bug #17429]: https://bugs.ruby-lang.org/issues/17429 -[Feature #17470]: https://bugs.ruby-lang.org/issues/17470 -[Feature #17479]: https://bugs.ruby-lang.org/issues/17479 -[Feature #17485]: https://bugs.ruby-lang.org/issues/17485 -[Feature #17544]: https://bugs.ruby-lang.org/issues/17544 -[Feature #17592]: https://bugs.ruby-lang.org/issues/17592 -[Feature #17684]: https://bugs.ruby-lang.org/issues/17684 -[Feature #17724]: https://bugs.ruby-lang.org/issues/17724 -[Feature #17744]: https://bugs.ruby-lang.org/issues/17744 -[Feature #17750]: https://bugs.ruby-lang.org/issues/17750 -[Feature #17762]: https://bugs.ruby-lang.org/issues/17762 -[Feature #17763]: https://bugs.ruby-lang.org/issues/17763 -[Feature #17795]: https://bugs.ruby-lang.org/issues/17795 -[Feature #17798]: https://bugs.ruby-lang.org/issues/17798 -[Bug #17827]: https://bugs.ruby-lang.org/issues/17827 -[Feature #17847]: https://bugs.ruby-lang.org/issues/17847 -[Feature #17853]: https://bugs.ruby-lang.org/issues/17853 -[Bug #17866]: https://bugs.ruby-lang.org/issues/17866 -[Bug #18003]: https://bugs.ruby-lang.org/issues/18003 -[Feature #18008]: https://bugs.ruby-lang.org/issues/18008 -[Feature #18015]: https://bugs.ruby-lang.org/issues/18015 -[Feature #18020]: https://bugs.ruby-lang.org/issues/18020 -[Feature #18029]: https://bugs.ruby-lang.org/issues/18029 -[Feature #18045]: https://bugs.ruby-lang.org/issues/18045 -[Feature #18148]: https://bugs.ruby-lang.org/issues/18148 -[Feature #18172]: https://bugs.ruby-lang.org/issues/18172 -[Feature #18176]: https://bugs.ruby-lang.org/issues/18176 -[Feature #18190]: https://bugs.ruby-lang.org/issues/18190 -[Feature #18229]: https://bugs.ruby-lang.org/issues/18229 -[Feature #18239]: https://bugs.ruby-lang.org/issues/18239 -[Feature #18254]: https://bugs.ruby-lang.org/issues/18254 -[Feature #18273]: https://bugs.ruby-lang.org/issues/18273 -[Feature #18290]: https://bugs.ruby-lang.org/issues/18290 - -[GH-1509]: https://github.com/ruby/ruby/pull/1509 -[GH-4815]: https://github.com/ruby/ruby/pull/4815 -[GH-5112]: https://github.com/ruby/ruby/pull/5112 -[GH-5131]: https://github.com/ruby/ruby/pull/5131 -[GH-5146]: https://github.com/ruby/ruby/pull/5146 +[Feature #6012]: https://bugs.ruby-lang.org/issues/6012 +[Feature #21390]: https://bugs.ruby-lang.org/issues/21390 diff --git a/README.ja.md b/README.ja.md index bb69c09055..9bbc3a83a5 100644 --- a/README.ja.md +++ b/README.ja.md @@ -1,10 +1,10 @@ [](https://github.com/ruby/ruby/actions?query=workflow%3A"MinGW") -[](https://github.com/ruby/ruby/actions?query=workflow%3A"MJIT") [](https://github.com/ruby/ruby/actions?query=workflow%3A"Ubuntu") [](https://github.com/ruby/ruby/actions?query=workflow%3A"Windows") [](https://ci.appveyor.com/project/ruby/ruby/branch/master) [](https://app.travis-ci.com/ruby/ruby) -[](https://cirrus-ci.com/github/ruby/ruby/master) + +[English](rdoc-ref:README.md) # Rubyとは @@ -26,7 +26,7 @@ Rubyはテキスト処理関係の能力などに優れ,Perlと同じくらい * ダイナミックローディング (アーキテクチャによる) * 移植性が高い.多くのUnix-like/POSIX互換プラットフォーム上で動くだけでなく,Windows, macOS, Haikuなどの上でも動く cf. - https://github.com/ruby/ruby/blob/master/doc/contributing.rdoc#platform-maintainers + https://docs.ruby-lang.org/en/master/maintainers_md.html#label-Platform+Maintainers ## 入手法 @@ -41,26 +41,19 @@ https://www.ruby-lang.org/ja/downloads/ ミラーをGitHubに公開しています. 以下のコマンドでリポジトリを取得できます. - $ git clone https://github.com/ruby/ruby.git +```console +$ git clone https://github.com/ruby/ruby.git +``` 他のブランチの一覧は次のコマンドで見られます. - $ git ls-remote https://github.com/ruby/ruby.git +```console +$ git ls-remote https://github.com/ruby/ruby.git +``` Rubyリポジトリの本来のmasterは https://git.ruby-lang.org/ruby.git にあります. コミッタはこちらを使います. -### Subversion - -古いRubyのバージョンのソースコードは次のコマンドでも取得できます. - - $ svn co https://svn.ruby-lang.org/repos/ruby/branches/ruby_2_6/ ruby - -他のブランチの一覧は次のコマンドで見られます. - - $ svn ls https://svn.ruby-lang.org/repos/ruby/branches/ - - ## ホームページ RubyのホームページのURLは @@ -71,20 +64,20 @@ https://www.ruby-lang.org/ ## メーリングリスト -Rubyのメーリングリストがあります.参加希望の方は [ruby-list-request@ruby-lang.org] まで本文に +Rubyのメーリングリストがあります.参加希望の方は [ruby-list-request@ml.ruby-lang.org] まで件名に - subscribe + join と書いて送って下さい. Ruby開発者向けメーリングリストもあります.こちらではrubyのバグ,将来の仕様拡張など実装上の問題について議論されています. -参加希望の方は [ruby-dev-request@ruby-lang.org] までruby-listと同様の方法でメールしてください. +参加希望の方は [ruby-dev-request@ml.ruby-lang.org] までruby-listと同様の方法でメールしてください. Ruby拡張モジュールについて話し合うruby-extメーリングリストと数学関係の話題について話し合うruby-mathメーリングリストと 英語でrubyについて話し合うruby-talkメーリングリストもあります.参加方法はどれも同じです. -[ruby-list-request@ruby-lang.org]: mailto:ruby-list-request@ruby-lang.org?subject=Join%20Ruby%20Mailing%20List&body=subscribe -[ruby-dev-request@ruby-lang.org]: mailto:ruby-dev-request@ruby-lang.org?subject=Join%20Ruby%20Mailing%20List&body=subscribe +[ruby-list-request@ml.ruby-lang.org]: mailto:ruby-list-request@ml.ruby-lang.org?subject=join +[ruby-dev-request@ml.ruby-lang.org]: mailto:ruby-dev-request@ml.ruby-lang.org?subject=join ## コンパイル・インストール @@ -163,7 +156,7 @@ UNIXであれば `configure` がほとんどの差異を吸収してくれるは ## 配布条件 -[COPYING.ja](COPYING.ja) ファイルを参照してください. +[COPYING.ja](https://docs.ruby-lang.org/en/master/COPYING_ja.html) ファイルを参照してください. ## フィードバック @@ -1,12 +1,11 @@ [](https://github.com/ruby/ruby/actions?query=workflow%3A"MinGW") -[](https://github.com/ruby/ruby/actions?query=workflow%3A"MJIT") [](https://github.com/ruby/ruby/actions?query=workflow%3A"Ubuntu") [](https://github.com/ruby/ruby/actions?query=workflow%3A"Windows") -[](https://ci.appveyor.com/project/ruby/ruby/branch/master) [](https://app.travis-ci.com/ruby/ruby) -[](https://cirrus-ci.com/github/ruby/ruby/master) -# What's Ruby +[日本語](rdoc-ref:README.ja.md) + +# What is Ruby? Ruby is an interpreted object-oriented programming language often used for web development. It also offers many scripting features @@ -15,18 +14,17 @@ It is simple, straightforward, and extensible. ## Features of Ruby -* Simple Syntax -* **Normal** Object-oriented Features (e.g. class, method calls) -* **Advanced** Object-oriented Features (e.g. mix-in, singleton-method) -* Operator Overloading -* Exception Handling -* Iterators and Closures -* Garbage Collection -* Dynamic Loading of Object Files (on some architectures) -* Highly Portable (works on many Unix-like/POSIX compatible platforms as - well as Windows, macOS, etc.) cf. - https://github.com/ruby/ruby/blob/master/doc/maintainers.rdoc#label-Platform+Maintainers - +* Simple Syntax +* **Normal** Object-oriented Features (e.g. class, method calls) +* **Advanced** Object-oriented Features (e.g. mix-in, singleton-method) +* Operator Overloading +* Exception Handling +* Iterators and Closures +* Garbage Collection +* Dynamic Loading of Object Files (on some architectures) +* Highly Portable (works on many Unix-like/POSIX compatible platforms as + well as Windows, macOS, etc.) cf. + https://docs.ruby-lang.org/en/master/maintainers_md.html#label-Platform+Maintainers ## How to get Ruby @@ -35,7 +33,10 @@ like rvm, see: https://www.ruby-lang.org/en/downloads/ -### Git +You can download release packages and the snapshot of the repository. If you want to +download whole versions of Ruby, please visit https://www.ruby-lang.org/en/downloads/releases/. + +### Download with Git The mirror of the Ruby source tree can be checked out with the following command: @@ -49,117 +50,29 @@ to see the list of branches: You may also want to use https://git.ruby-lang.org/ruby.git (actual master of Ruby source) if you are a committer. -### Subversion - -Stable branches for older Ruby versions can be checked out with also the -following command: - - $ svn co https://svn.ruby-lang.org/repos/ruby/branches/ruby_2_6/ ruby - -Try the following command to see the list of branches: - - $ svn ls https://svn.ruby-lang.org/repos/ruby/branches/ +## How to build +See [Building Ruby](https://docs.ruby-lang.org/en/master/contributing/building_ruby_md.html) ## Ruby home page https://www.ruby-lang.org/ +## Documentation + +- [English](https://docs.ruby-lang.org/en/master/index.html) +- [Japanese](https://docs.ruby-lang.org/ja/master/index.html) + ## Mailing list There is a mailing list to discuss Ruby. To subscribe to this list, please send the following phrase: - subscribe - -in the mail body (not subject) to the address [ruby-talk-request@ruby-lang.org]. - -[ruby-talk-request@ruby-lang.org]: mailto:ruby-talk-request@ruby-lang.org?subject=Join%20Ruby%20Mailing%20List&body=subscribe - -## Requirements to build from repository - -1. GNU or BSD make -2. C99 compiler -3. autoconf 2.67 or higher -4. automake 1.15 or higher -5. bison 2.3 or higher -6. Ruby 2.2 or higher - -When building from a released version, only a C99 compiler and GNU or BSD make -is required. - -## How to compile and install - -1. If you want to use Microsoft Visual C++ to compile Ruby, read - [win32/README.win32](rdoc-ref:win32/README.win32) instead of this document. - -2. Run `./autogen.sh` to generate configure, when you build the source checked - out from the Git repository. - -3. Run `./configure`, which will generate `config.h` and `Makefile`. + join - Some C compiler flags may be added by default depending on your - environment. Specify `optflags=..` and `warnflags=..` as necessary to - override them. +in the mail subject (not body) to the address [ruby-talk-request@ml.ruby-lang.org]. -4. Edit `include/ruby/defines.h` if you need. Usually this step will not be needed. - -5. Optional: Remove comment mark(`#`) before the module names from `ext/Setup`. - - This step is only necessary if you want to link modules statically. - - If you don't want to compile dynamic extensions (probably on architectures - which do not allow dynamic loading), remove comment mark from the line - "`#option nodynamic`" in `ext/Setup`. - - Usually this step will not be needed. - -6. Run `make`. - - * On Mac, set RUBY\_CODESIGN environment variable with a signing identity. - It uses the identity to sign `ruby` binary. See also codesign(1). - -7. Optionally, run '`make check`' to check whether the compiled Ruby - interpreter works well. If you see the message "`check succeeded`", your - Ruby works as it should (hopefully). - -8. Run '`make install`'. - - This command will create the following directories and install files into - them. - - * `${DESTDIR}${prefix}/bin` - * `${DESTDIR}${prefix}/include/ruby-${MAJOR}.${MINOR}.${TEENY}` - * `${DESTDIR}${prefix}/include/ruby-${MAJOR}.${MINOR}.${TEENY}/${PLATFORM}` - * `${DESTDIR}${prefix}/lib` - * `${DESTDIR}${prefix}/lib/ruby` - * `${DESTDIR}${prefix}/lib/ruby/${MAJOR}.${MINOR}.${TEENY}` - * `${DESTDIR}${prefix}/lib/ruby/${MAJOR}.${MINOR}.${TEENY}/${PLATFORM}` - * `${DESTDIR}${prefix}/lib/ruby/site_ruby` - * `${DESTDIR}${prefix}/lib/ruby/site_ruby/${MAJOR}.${MINOR}.${TEENY}` - * `${DESTDIR}${prefix}/lib/ruby/site_ruby/${MAJOR}.${MINOR}.${TEENY}/${PLATFORM}` - * `${DESTDIR}${prefix}/lib/ruby/vendor_ruby` - * `${DESTDIR}${prefix}/lib/ruby/vendor_ruby/${MAJOR}.${MINOR}.${TEENY}` - * `${DESTDIR}${prefix}/lib/ruby/vendor_ruby/${MAJOR}.${MINOR}.${TEENY}/${PLATFORM}` - * `${DESTDIR}${prefix}/lib/ruby/gems/${MAJOR}.${MINOR}.${TEENY}` - * `${DESTDIR}${prefix}/share/man/man1` - * `${DESTDIR}${prefix}/share/ri/${MAJOR}.${MINOR}.${TEENY}/system` - - - If Ruby's API version is '*x.y.z*', the `${MAJOR}` is '*x*', the - `${MINOR}` is '*y*', and the `${TEENY}` is '*z*'. - - **NOTE**: teeny of the API version may be different from one of Ruby's - program version - - You may have to be a super user to install Ruby. - -If you fail to compile Ruby, please send the detailed error report with the -error log and machine/OS type, to help others. - -Some extension libraries may not get compiled because of lack of necessary -external libraries and/or headers, then you will need to run '`make distclean-ext`' -to remove old configuration after installing them in such case. +[ruby-talk-request@ml.ruby-lang.org]: mailto:ruby-talk-request@ml.ruby-lang.org?subject=join ## Copying @@ -167,17 +80,14 @@ See the file [COPYING](rdoc-ref:COPYING). ## Feedback -Questions about the Ruby language can be asked on the [Ruby-Talk] mailing list +Questions about the Ruby language can be asked on the [Ruby-Talk](https://www.ruby-lang.org/en/community/mailing-lists) mailing list or on websites like https://stackoverflow.com. -Bugs should be reported at https://bugs.ruby-lang.org. Read [HowToReport] for more information. - -[Ruby-Talk]: https://www.ruby-lang.org/en/community/mailing-lists -[HowToReport]: https://bugs.ruby-lang.org/projects/ruby/wiki/HowToReport +Bugs should be reported at https://bugs.ruby-lang.org. Read ["Reporting Issues"](https://docs.ruby-lang.org/en/master/contributing/reporting_issues_md.html) for more information. ## Contributing -See the file [CONTRIBUTING.md](rdoc-ref:CONTRIBUTING) +See ["Contributing to Ruby"](https://docs.ruby-lang.org/en/master/contributing/contributing_md.html), which includes setup and build instructions. ## The Author diff --git a/addr2line.c b/addr2line.c index f660be9129..19a6a425c1 100644 --- a/addr2line.c +++ b/addr2line.c @@ -8,10 +8,14 @@ **********************************************************************/ -#if defined(__clang__) +#if defined(__clang__) && defined(__has_warning) +#if __has_warning("-Wgnu-empty-initializer") #pragma clang diagnostic ignored "-Wgnu-empty-initializer" +#endif +#if __has_warning("-Wgcc-compat") #pragma clang diagnostic ignored "-Wgcc-compat" #endif +#endif #include "ruby/internal/config.h" #include "ruby/defines.h" @@ -52,13 +56,26 @@ # ifdef _AIX #pragma alloca # else -# ifndef alloca /* predefined by HP cc +Olibcalls */ +# ifndef alloca /* predefined by HP cc +Olibcalls */ void *alloca(); # endif # endif /* AIX */ -# endif /* HAVE_ALLOCA_H */ +# endif /* HAVE_ALLOCA_H */ +# ifndef UNREACHABLE +# define UNREACHABLE __builtin_unreachable() +# endif +# ifndef UNREACHABLE_RETURN +# define UNREACHABLE_RETURN(_) __builtin_unreachable() +# endif #endif /* __GNUC__ */ +#ifndef UNREACHABLE +# define UNREACHABLE abort() +#endif +#ifndef UNREACHABLE_RETURN +# define UNREACHABLE_RETURN(_) return (abort(), (_)) +#endif + #ifdef HAVE_DLADDR # include <dlfcn.h> #endif @@ -127,7 +144,7 @@ void *alloca(); #define DW_LNE_define_file 0x03 #define DW_LNE_set_discriminator 0x04 /* DWARF4 */ -PRINTF_ARGS(static int kprintf(const char *fmt, ...), 1, 2); +#define kprintf(...) fprintf(errout, "" __VA_ARGS__) typedef struct line_info { const char *dirname; @@ -159,12 +176,15 @@ typedef struct obj_info { struct dwarf_section debug_info; struct dwarf_section debug_line; struct dwarf_section debug_ranges; + struct dwarf_section debug_str_offsets; + struct dwarf_section debug_addr; struct dwarf_section debug_rnglists; struct dwarf_section debug_str; + struct dwarf_section debug_line_str; struct obj_info *next; } obj_info_t; -#define DWARF_SECTION_COUNT 6 +#define DWARF_SECTION_COUNT 9 static struct dwarf_section * obj_dwarf_section_at(obj_info_t *obj, int n) @@ -174,11 +194,14 @@ obj_dwarf_section_at(obj_info_t *obj, int n) &obj->debug_info, &obj->debug_line, &obj->debug_ranges, + &obj->debug_str_offsets, + &obj->debug_addr, &obj->debug_rnglists, - &obj->debug_str + &obj->debug_str, + &obj->debug_line_str }; if (n < 0 || DWARF_SECTION_COUNT <= n) { - abort(); + UNREACHABLE_RETURN(0); } return ary[n]; } @@ -197,13 +220,13 @@ uleb128(const char **p) unsigned long r = 0; int s = 0; for (;;) { - unsigned char b = (unsigned char)*(*p)++; - if (b < 0x80) { - r += (unsigned long)b << s; - break; - } - r += (b & 0x7f) << s; - s += 7; + unsigned char b = (unsigned char)*(*p)++; + if (b < 0x80) { + r += (unsigned long)b << s; + break; + } + r += (b & 0x7f) << s; + s += 7; } return r; } @@ -214,85 +237,103 @@ sleb128(const char **p) long r = 0; int s = 0; for (;;) { - unsigned char b = (unsigned char)*(*p)++; - if (b < 0x80) { - if (b & 0x40) { - r -= (0x80 - b) << s; - } - else { - r += (b & 0x3f) << s; - } - break; - } - r += (b & 0x7f) << s; - s += 7; + unsigned char b = (unsigned char)*(*p)++; + if (b < 0x80) { + if (b & 0x40) { + r -= (0x80 - b) << s; + } + else { + r += (b & 0x3f) << s; + } + break; + } + r += (b & 0x7f) << s; + s += 7; } return r; } static const char * -get_nth_dirname(unsigned long dir, const char *p) +get_nth_dirname(unsigned long dir, const char *p, FILE *errout) { if (!dir--) { - return ""; + return ""; } while (dir--) { - while (*p) p++; - p++; - if (!*p) { - kprintf("Unexpected directory number %lu in %s\n", - dir, binary_filename); - return ""; - } + while (*p) p++; + p++; + if (!*p) { + kprintf("Unexpected directory number %lu in %s\n", + dir, binary_filename); + return ""; + } } return p; } +static const char *parse_ver5_debug_line_header( + const char *p, int idx, uint8_t format, + obj_info_t *obj, const char **out_path, + uint64_t *out_directory_index, FILE *errout); + static void -fill_filename(int file, const char *include_directories, const char *filenames, line_info_t *line, obj_info_t *obj) +fill_filename(int file, uint8_t format, uint16_t version, const char *include_directories, + const char *filenames, line_info_t *line, obj_info_t *obj, FILE *errout) { int i; const char *p = filenames; const char *filename; unsigned long dir; - for (i = 1; i <= file; i++) { - filename = p; - if (!*p) { - /* Need to output binary file name? */ - kprintf("Unexpected file number %d in %s at %tx\n", - file, binary_filename, filenames - obj->mapped); - return; - } - while (*p) p++; - p++; - dir = uleb128(&p); - /* last modified. */ - uleb128(&p); - /* size of the file. */ - uleb128(&p); - - if (i == file) { - line->filename = filename; - line->dirname = get_nth_dirname(dir, include_directories); - } + if (version >= 5) { + const char *path; + uint64_t directory_index = -1; + parse_ver5_debug_line_header(filenames, file, format, obj, &path, &directory_index, errout); + line->filename = path; + parse_ver5_debug_line_header(include_directories, (int)directory_index, format, obj, &path, NULL, errout); + line->dirname = path; + } + else { + for (i = 1; i <= file; i++) { + filename = p; + if (!*p) { +#ifndef __APPLE__ + /* Need to output binary file name? */ + kprintf("Unexpected file number %d in %s at %tx\n", + file, binary_filename, filenames - obj->mapped); +#endif + return; + } + while (*p) p++; + p++; + dir = uleb128(&p); + /* last modified. */ + uleb128(&p); + /* size of the file. */ + uleb128(&p); + + if (i == file) { + line->filename = filename; + line->dirname = get_nth_dirname(dir, include_directories, errout); + } + } } } static void fill_line(int num_traces, void **traces, uintptr_t addr, int file, int line, - const char *include_directories, const char *filenames, - obj_info_t *obj, line_info_t *lines, int offset) + uint8_t format, uint16_t version, const char *include_directories, const char *filenames, + obj_info_t *obj, line_info_t *lines, int offset, FILE *errout) { int i; addr += obj->base_addr - obj->vmaddr; for (i = offset; i < num_traces; i++) { - uintptr_t a = (uintptr_t)traces[i]; - /* We assume one line code doesn't result >100 bytes of native code. + uintptr_t a = (uintptr_t)traces[i]; + /* We assume one line code doesn't result >100 bytes of native code. We may want more reliable way eventually... */ - if (addr < a && a < addr + 100) { - fill_filename(file, include_directories, filenames, &lines[i], obj); - lines[i].line = line; - } + if (addr < a && a < addr + 100) { + fill_filename(file, format, version, include_directories, filenames, &lines[i], obj, errout); + lines[i].line = line; + } } } @@ -315,7 +356,7 @@ struct LineNumberProgramHeader { }; static int -parse_debug_line_header(const char **pp, struct LineNumberProgramHeader *header) +parse_debug_line_header(obj_info_t *obj, const char **pp, struct LineNumberProgramHeader *header, FILE *errout) { const char *p = *pp; header->unit_length = *(uint32_t *)p; @@ -323,8 +364,8 @@ parse_debug_line_header(const char **pp, struct LineNumberProgramHeader *header) header->format = 4; if (header->unit_length == 0xffffffff) { - header->unit_length = *(uint64_t *)p; - p += sizeof(uint64_t); + header->unit_length = *(uint64_t *)p; + p += sizeof(uint64_t); header->format = 8; } @@ -332,7 +373,13 @@ parse_debug_line_header(const char **pp, struct LineNumberProgramHeader *header) header->version = *(uint16_t *)p; p += sizeof(uint16_t); - if (header->version > 4) return -1; + if (header->version > 5) return -1; + + if (header->version >= 5) { + /* address_size = *(uint8_t *)p++; */ + /* segment_selector_size = *(uint8_t *)p++; */ + p += 2; + } header->header_length = header->format == 4 ? *(uint32_t *)p : *(uint64_t *)p; p += header->format; @@ -353,20 +400,27 @@ parse_debug_line_header(const char **pp, struct LineNumberProgramHeader *header) /* header->standard_opcode_lengths = (uint8_t *)p - 1; */ p += header->opcode_base - 1; - header->include_directories = p; + if (header->version >= 5) { + header->include_directories = p; + p = parse_ver5_debug_line_header(p, -1, header->format, obj, NULL, NULL, errout); + header->filenames = p; + } + else { + header->include_directories = p; - /* temporary measure for compress-debug-sections */ - if (p >= header->cu_end) return -1; + /* temporary measure for compress-debug-sections */ + if (p >= header->cu_end) return -1; - /* skip include directories */ - while (*p) { - p = memchr(p, '\0', header->cu_end - p); - if (!p) return -1; - p++; - } - p++; + /* skip include directories */ + while (*p) { + p = memchr(p, '\0', header->cu_end - p); + if (!p) return -1; + p++; + } + p++; - header->filenames = p; + header->filenames = p; + } *pp = header->cu_start; @@ -375,7 +429,7 @@ parse_debug_line_header(const char **pp, struct LineNumberProgramHeader *header) static int parse_debug_line_cu(int num_traces, void **traces, const char **debug_line, - obj_info_t *obj, line_info_t *lines, int offset) + obj_info_t *obj, line_info_t *lines, int offset, FILE *errout) { const char *p = (const char *)*debug_line; struct LineNumberProgramHeader header; @@ -392,107 +446,109 @@ parse_debug_line_cu(int num_traces, void **traces, const char **debug_line, /* int epilogue_begin = 0; */ /* unsigned int isa = 0; */ - if (parse_debug_line_header(&p, &header)) + if (parse_debug_line_header(obj, &p, &header, errout)) return -1; is_stmt = header.default_is_stmt; -#define FILL_LINE() \ - do { \ - fill_line(num_traces, traces, addr, file, line, \ +#define FILL_LINE() \ + do { \ + fill_line(num_traces, traces, addr, file, line, \ + header.format, \ + header.version, \ header.include_directories, \ header.filenames, \ - obj, lines, offset); \ - /*basic_block = prologue_end = epilogue_begin = 0;*/ \ + obj, lines, offset, errout); \ + /*basic_block = prologue_end = epilogue_begin = 0;*/ \ } while (0) while (p < header.cu_end) { - unsigned long a; - unsigned char op = *p++; - switch (op) { - case DW_LNS_copy: - FILL_LINE(); - break; - case DW_LNS_advance_pc: - a = uleb128(&p) * header.minimum_instruction_length; - addr += a; - break; - case DW_LNS_advance_line: { - long a = sleb128(&p); - line += a; - break; - } - case DW_LNS_set_file: - file = (unsigned int)uleb128(&p); - break; - case DW_LNS_set_column: - /*column = (unsigned int)*/(void)uleb128(&p); - break; - case DW_LNS_negate_stmt: - is_stmt = !is_stmt; - break; - case DW_LNS_set_basic_block: - /*basic_block = 1; */ - break; - case DW_LNS_const_add_pc: - a = ((255UL - header.opcode_base) / header.line_range) * - header.minimum_instruction_length; - addr += a; - break; - case DW_LNS_fixed_advance_pc: - a = *(uint16_t *)p; - p += sizeof(uint16_t); - addr += a; - break; - case DW_LNS_set_prologue_end: - /* prologue_end = 1; */ - break; - case DW_LNS_set_epilogue_begin: - /* epilogue_begin = 1; */ - break; - case DW_LNS_set_isa: - /* isa = (unsigned int)*/(void)uleb128(&p); - break; - case 0: - a = uleb128(&p); - op = *p++; - switch (op) { - case DW_LNE_end_sequence: - /* end_sequence = 1; */ - FILL_LINE(); - addr = 0; - file = 1; - line = 1; - /* column = 0; */ - is_stmt = header.default_is_stmt; - /* end_sequence = 0; */ - /* isa = 0; */ - break; - case DW_LNE_set_address: - addr = *(unsigned long *)p; - p += sizeof(unsigned long); - break; - case DW_LNE_define_file: - kprintf("Unsupported operation in %s\n", - binary_filename); - break; - case DW_LNE_set_discriminator: - /* TODO:currently ignore */ - uleb128(&p); - break; - default: - kprintf("Unknown extended opcode: %d in %s\n", - op, binary_filename); - } - break; - default: { + unsigned long a; + unsigned char op = *p++; + switch (op) { + case DW_LNS_copy: + FILL_LINE(); + break; + case DW_LNS_advance_pc: + a = uleb128(&p) * header.minimum_instruction_length; + addr += a; + break; + case DW_LNS_advance_line: { + long a = sleb128(&p); + line += a; + break; + } + case DW_LNS_set_file: + file = (unsigned int)uleb128(&p); + break; + case DW_LNS_set_column: + /*column = (unsigned int)*/(void)uleb128(&p); + break; + case DW_LNS_negate_stmt: + is_stmt = !is_stmt; + break; + case DW_LNS_set_basic_block: + /*basic_block = 1; */ + break; + case DW_LNS_const_add_pc: + a = ((255UL - header.opcode_base) / header.line_range) * + header.minimum_instruction_length; + addr += a; + break; + case DW_LNS_fixed_advance_pc: + a = *(uint16_t *)p; + p += sizeof(uint16_t); + addr += a; + break; + case DW_LNS_set_prologue_end: + /* prologue_end = 1; */ + break; + case DW_LNS_set_epilogue_begin: + /* epilogue_begin = 1; */ + break; + case DW_LNS_set_isa: + /* isa = (unsigned int)*/(void)uleb128(&p); + break; + case 0: + a = uleb128(&p); + op = *p++; + switch (op) { + case DW_LNE_end_sequence: + /* end_sequence = 1; */ + FILL_LINE(); + addr = 0; + file = 1; + line = 1; + /* column = 0; */ + is_stmt = header.default_is_stmt; + /* end_sequence = 0; */ + /* isa = 0; */ + break; + case DW_LNE_set_address: + addr = *(unsigned long *)p; + p += sizeof(unsigned long); + break; + case DW_LNE_define_file: + kprintf("Unsupported operation in %s\n", + binary_filename); + break; + case DW_LNE_set_discriminator: + /* TODO:currently ignore */ + uleb128(&p); + break; + default: + kprintf("Unknown extended opcode: %d in %s\n", + op, binary_filename); + } + break; + default: { uint8_t adjusted_opcode = op - header.opcode_base; uint8_t operation_advance = adjusted_opcode / header.line_range; /* NOTE: this code doesn't support VLIW */ addr += operation_advance * header.minimum_instruction_length; line += header.line_base + (adjusted_opcode % header.line_range); - FILL_LINE(); - } - } + FILL_LINE(); + } + } } *debug_line = (char *)p; return 0; @@ -500,17 +556,17 @@ parse_debug_line_cu(int num_traces, void **traces, const char **debug_line, static int parse_debug_line(int num_traces, void **traces, - const char *debug_line, unsigned long size, - obj_info_t *obj, line_info_t *lines, int offset) + const char *debug_line, unsigned long size, + obj_info_t *obj, line_info_t *lines, int offset, FILE *errout) { const char *debug_line_end = debug_line + size; while (debug_line < debug_line_end) { - if (parse_debug_line_cu(num_traces, traces, &debug_line, obj, lines, offset)) - return -1; + if (parse_debug_line_cu(num_traces, traces, &debug_line, obj, lines, offset, errout)) + return -1; } if (debug_line != debug_line_end) { - kprintf("Unexpected size of .debug_line in %s\n", - binary_filename); + kprintf("Unexpected size of .debug_line in %s\n", + binary_filename); } return 0; } @@ -518,7 +574,7 @@ parse_debug_line(int num_traces, void **traces, /* read file and fill lines */ static uintptr_t fill_lines(int num_traces, void **traces, int check_debuglink, - obj_info_t **objp, line_info_t *lines, int offset); + obj_info_t **objp, line_info_t *lines, int offset, FILE *errout); static void append_obj(obj_info_t **objp) @@ -546,7 +602,7 @@ append_obj(obj_info_t **objp) // check the path pattern of "/usr/lib/debug/usr/bin/ruby.debug" static void follow_debuglink(const char *debuglink, int num_traces, void **traces, - obj_info_t **objp, line_info_t *lines, int offset) + obj_info_t **objp, line_info_t *lines, int offset, FILE *errout) { static const char global_debug_dir[] = "/usr/lib/debug"; const size_t global_debug_dir_len = sizeof(global_debug_dir) - 1; @@ -556,13 +612,13 @@ follow_debuglink(const char *debuglink, int num_traces, void **traces, p = strrchr(binary_filename, '/'); if (!p) { - return; + return; } p[1] = '\0'; len = strlen(binary_filename); if (len >= PATH_MAX - global_debug_dir_len) - len = PATH_MAX - global_debug_dir_len - 1; + len = PATH_MAX - global_debug_dir_len - 1; memmove(binary_filename + global_debug_dir_len, binary_filename, len); memcpy(binary_filename, global_debug_dir, global_debug_dir_len); len += global_debug_dir_len; @@ -572,21 +628,22 @@ follow_debuglink(const char *debuglink, int num_traces, void **traces, o2 = *objp; o2->base_addr = o1->base_addr; o2->path = o1->path; - fill_lines(num_traces, traces, 0, objp, lines, offset); + fill_lines(num_traces, traces, 0, objp, lines, offset, errout); } // check the path pattern of "/usr/lib/debug/.build-id/ab/cdef1234.debug" static void follow_debuglink_build_id(const char *build_id, size_t build_id_size, int num_traces, void **traces, - obj_info_t **objp, line_info_t *lines, int offset) + obj_info_t **objp, line_info_t *lines, int offset, FILE *errout) { static const char global_debug_dir[] = "/usr/lib/debug/.build-id/"; + static const char debug_suffix[] = ".debug"; const size_t global_debug_dir_len = sizeof(global_debug_dir) - 1; char *p; obj_info_t *o1 = *objp, *o2; size_t i; - if (PATH_MAX < global_debug_dir_len + 1 + build_id_size * 2 + 6) return; + if (PATH_MAX < global_debug_dir_len + build_id_size * 2 + sizeof(debug_suffix)) return; memcpy(binary_filename, global_debug_dir, global_debug_dir_len); p = binary_filename + global_debug_dir_len; @@ -597,13 +654,13 @@ follow_debuglink_build_id(const char *build_id, size_t build_id_size, int num_tr *p++ = tbl[n % 16]; if (i == 0) *p++ = '/'; } - strcpy(p, ".debug"); + memcpy(p, debug_suffix, sizeof(debug_suffix)); append_obj(objp); o2 = *objp; o2->base_addr = o1->base_addr; o2->path = o1->path; - fill_lines(num_traces, traces, 0, objp, lines, offset); + fill_lines(num_traces, traces, 0, objp, lines, offset, errout); } #endif @@ -807,7 +864,11 @@ enum DW_FORM_addrx1 = 0x29, DW_FORM_addrx2 = 0x2a, DW_FORM_addrx3 = 0x2b, - DW_FORM_addrx4 = 0x2c + DW_FORM_addrx4 = 0x2c, + + /* GNU extensions for referring to .gnu_debugaltlink dwz-compressed info */ + DW_FORM_GNU_ref_alt = 0x1f20, + DW_FORM_GNU_strp_alt = 0x1f21 }; /* Range list entry encodings */ @@ -827,16 +888,23 @@ enum { VAL_cstr = 1, VAL_data = 2, VAL_uint = 3, - VAL_int = 4 + VAL_int = 4, + VAL_addr = 5 }; # define ABBREV_TABLE_SIZE 256 typedef struct { obj_info_t *obj; const char *file; + uint8_t current_version; const char *current_cu; uint64_t current_low_pc; + uint64_t current_str_offsets_base; + uint64_t current_addr_base; + uint64_t current_rnglists_base; const char *debug_line_cu_end; + uint8_t debug_line_format; + uint16_t debug_line_version; const char *debug_line_files; const char *debug_line_directories; const char *p; @@ -861,6 +929,7 @@ typedef struct { const char *ptr; uint64_t uint64; int64_t int64; + uint64_t addr_idx; } as; uint64_t off; uint64_t at; @@ -869,8 +938,11 @@ typedef struct { int type; } DebugInfoValue; -/* TODO: Big Endian */ +#if defined(WORDS_BIGENDIAN) +#define MERGE_2INTS(a,b,sz) (((uint64_t)(a)<<sz)|(b)) +#else #define MERGE_2INTS(a,b,sz) (((uint64_t)(b)<<sz)|(a)) +#endif static uint16_t get_uint16(const uint8_t *p) @@ -973,6 +1045,9 @@ debug_info_reader_init(DebugInfoReader *reader, obj_info_t *obj) reader->pend = obj->debug_info.ptr + obj->debug_info.size; reader->debug_line_cu_end = obj->debug_line.ptr; reader->current_low_pc = 0; + reader->current_str_offsets_base = 0; + reader->current_addr_base = 0; + reader->current_rnglists_base = 0; } static void @@ -1011,16 +1086,18 @@ di_read_debug_abbrev_cu(DebugInfoReader *reader) } static int -di_read_debug_line_cu(DebugInfoReader *reader) +di_read_debug_line_cu(DebugInfoReader *reader, FILE *errout) { const char *p; struct LineNumberProgramHeader header; p = (const char *)reader->debug_line_cu_end; - if (parse_debug_line_header(&p, &header)) + if (parse_debug_line_header(reader->obj, &p, &header, errout)) return -1; reader->debug_line_cu_end = (char *)header.cu_end; + reader->debug_line_format = header.format; + reader->debug_line_version = header.version; reader->debug_line_directories = (char *)header.include_directories; reader->debug_line_files = (char *)header.filenames; @@ -1028,6 +1105,13 @@ di_read_debug_line_cu(DebugInfoReader *reader) } static void +set_addr_idx_value(DebugInfoValue *v, uint64_t n) +{ + v->as.addr_idx = n; + v->type = VAL_addr; +} + +static void set_uint_value(DebugInfoValue *v, uint64_t n) { v->as.uint64 = n; @@ -1074,19 +1158,46 @@ get_cstr_value(DebugInfoValue *v) } } -static void -debug_info_reader_read_value(DebugInfoReader *reader, uint64_t form, DebugInfoValue *v) +static const char * +resolve_strx(DebugInfoReader *reader, uint64_t idx) +{ + const char *p = reader->obj->debug_str_offsets.ptr + reader->current_str_offsets_base; + uint64_t off; + if (reader->format == 4) { + off = ((uint32_t *)p)[idx]; + } + else { + off = ((uint64_t *)p)[idx]; + } + return reader->obj->debug_str.ptr + off; +} + +static bool +debug_info_reader_read_addr_value_member(DebugInfoReader *reader, DebugInfoValue *v, int size) +{ + if (size == 4) { + set_uint_value(v, read_uint32(&reader->p)); + } else if (size == 8) { + set_uint_value(v, read_uint64(&reader->p)); + } else { + return false; + } + return true; +} + +#define debug_info_reader_read_addr_value(reader, v, mem) \ + if (!debug_info_reader_read_addr_value_member((reader), (v), (reader)->mem)) { \ + kprintf("unknown " #mem ":%d", (reader)->mem); \ + return false; \ + } + + +static bool +debug_info_reader_read_value(DebugInfoReader *reader, uint64_t form, DebugInfoValue *v, FILE *errout) { switch (form) { case DW_FORM_addr: - if (reader->address_size == 4) { - set_uint_value(v, read_uint32(&reader->p)); - } else if (reader->address_size == 8) { - set_uint_value(v, read_uint64(&reader->p)); - } else { - fprintf(stderr,"unknown address_size:%d", reader->address_size); - abort(); - } + debug_info_reader_read_addr_value(reader, v, address_size); break; case DW_FORM_block2: v->size = read_uint16(&reader->p); @@ -1138,13 +1249,12 @@ debug_info_reader_read_value(DebugInfoReader *reader, uint64_t form, DebugInfoVa set_uint_value(v, read_uleb128(reader)); break; case DW_FORM_ref_addr: - if (reader->format == 4) { - set_uint_value(v, read_uint32(&reader->p)); - } else if (reader->format == 8) { - set_uint_value(v, read_uint64(&reader->p)); + if (reader->current_version <= 2) { + // DWARF Version 2 specifies that references have + // the same size as an address on the target system + debug_info_reader_read_addr_value(reader, v, address_size); } else { - fprintf(stderr,"unknown format:%d", reader->format); - abort(); + debug_info_reader_read_addr_value(reader, v, format); } break; case DW_FORM_ref1: @@ -1186,11 +1296,10 @@ debug_info_reader_read_value(DebugInfoReader *reader, uint64_t form, DebugInfoVa set_uint_value(v, 1); break; case DW_FORM_strx: - set_uint_value(v, uleb128(&reader->p)); + set_cstr_value(v, resolve_strx(reader, uleb128(&reader->p))); break; case DW_FORM_addrx: - /* TODO: read .debug_addr */ - set_uint_value(v, uleb128(&reader->p)); + set_addr_idx_value(v, uleb128(&reader->p)); break; case DW_FORM_ref_sup4: set_uint_value(v, read_uint32(&reader->p)); @@ -1205,8 +1314,7 @@ debug_info_reader_read_value(DebugInfoReader *reader, uint64_t form, DebugInfoVa reader->p += v->size; break; case DW_FORM_line_strp: - set_uint_value(v, read_uint(reader)); - /* *p = reader->file + reader->line->sh_offset + ret; */ + set_cstrp_value(v, reader->obj->debug_line_str.ptr, read_uint(reader)); break; case DW_FORM_ref_sig8: set_uint_value(v, read_uint64(&reader->p)); @@ -1224,43 +1332,51 @@ debug_info_reader_read_value(DebugInfoReader *reader, uint64_t form, DebugInfoVa set_uint_value(v, read_uint64(&reader->p)); break; case DW_FORM_strx1: - set_uint_value(v, read_uint8(&reader->p)); + set_cstr_value(v, resolve_strx(reader, read_uint8(&reader->p))); break; case DW_FORM_strx2: - set_uint_value(v, read_uint16(&reader->p)); + set_cstr_value(v, resolve_strx(reader, read_uint16(&reader->p))); break; case DW_FORM_strx3: - set_uint_value(v, read_uint24(&reader->p)); + set_cstr_value(v, resolve_strx(reader, read_uint24(&reader->p))); break; case DW_FORM_strx4: - set_uint_value(v, read_uint32(&reader->p)); + set_cstr_value(v, resolve_strx(reader, read_uint32(&reader->p))); break; case DW_FORM_addrx1: - set_uint_value(v, read_uint8(&reader->p)); + set_addr_idx_value(v, read_uint8(&reader->p)); break; case DW_FORM_addrx2: - set_uint_value(v, read_uint16(&reader->p)); + set_addr_idx_value(v, read_uint16(&reader->p)); break; case DW_FORM_addrx3: - set_uint_value(v, read_uint24(&reader->p)); + set_addr_idx_value(v, read_uint24(&reader->p)); break; case DW_FORM_addrx4: - set_uint_value(v, read_uint32(&reader->p)); + set_addr_idx_value(v, read_uint32(&reader->p)); + break; + /* we have no support for actually reading the real values of these refs out + * of the .gnu_debugaltlink dwz-compressed debuginfo at the moment, but "read" + * them anyway so that we advance the reader by the right amount. */ + case DW_FORM_GNU_ref_alt: + case DW_FORM_GNU_strp_alt: + read_uint(reader); + set_uint_value(v, 0); break; case 0: goto fail; break; } - return; + return true; fail: - fprintf(stderr, "%d: unsupported form: %#"PRIx64"\n", __LINE__, form); - exit(1); + kprintf("%d: unsupported form: %#"PRIx64"\n", __LINE__, form); + return false; } /* find abbrev in current compilation unit */ static const char * -di_find_abbrev(DebugInfoReader *reader, uint64_t abbrev_number) +di_find_abbrev(DebugInfoReader *reader, uint64_t abbrev_number, FILE *errout) { const char *p; if (abbrev_number < ABBREV_TABLE_SIZE) { @@ -1273,8 +1389,8 @@ di_find_abbrev(DebugInfoReader *reader, uint64_t abbrev_number) di_skip_die_attributes(&p); for (uint64_t n = uleb128(&p); abbrev_number != n; n = uleb128(&p)) { if (n == 0) { - fprintf(stderr,"%d: Abbrev Number %"PRId64" not found\n",__LINE__, abbrev_number); - exit(1); + kprintf("%d: Abbrev Number %"PRId64" not found\n",__LINE__, abbrev_number); + return NULL; } uleb128(&p); /* tag */ p++; /* has_children */ @@ -1285,52 +1401,52 @@ di_find_abbrev(DebugInfoReader *reader, uint64_t abbrev_number) #if 0 static void -hexdump0(const unsigned char *p, size_t n) +hexdump0(const unsigned char *p, size_t n, FILE *errout) { size_t i; - fprintf(stderr, " 0 1 2 3 4 5 6 7 8 9 A B C D E F\n"); + kprintf(" 0 1 2 3 4 5 6 7 8 9 A B C D E F\n"); for (i=0; i < n; i++){ switch (i & 15) { case 0: - fprintf(stderr, "%02zd: %02X ", i/16, p[i]); + kprintf("%02" PRIdSIZE ": %02X ", i/16, p[i]); break; case 15: - fprintf(stderr, "%02X\n", p[i]); + kprintf("%02X\n", p[i]); break; default: - fprintf(stderr, "%02X ", p[i]); + kprintf("%02X ", p[i]); break; } } if ((i & 15) != 15) { - fprintf(stderr, "\n"); + kprintf("\n"); } } -#define hexdump(p,n) hexdump0((const unsigned char *)p, n) +#define hexdump(p,n,e) hexdump0((const unsigned char *)p, n, e) static void -div_inspect(DebugInfoValue *v) +div_inspect(DebugInfoValue *v, FILE *errout) { switch (v->type) { case VAL_uint: - fprintf(stderr,"%d: type:%d size:%zx v:%"PRIx64"\n",__LINE__,v->type,v->size,v->as.uint64); + kprintf("%d: type:%d size:%" PRIxSIZE " v:%"PRIx64"\n",__LINE__,v->type,v->size,v->as.uint64); break; case VAL_int: - fprintf(stderr,"%d: type:%d size:%zx v:%"PRId64"\n",__LINE__,v->type,v->size,(int64_t)v->as.uint64); + kprintf("%d: type:%d size:%" PRIxSIZE " v:%"PRId64"\n",__LINE__,v->type,v->size,(int64_t)v->as.uint64); break; case VAL_cstr: - fprintf(stderr,"%d: type:%d size:%zx v:'%s'\n",__LINE__,v->type,v->size,v->as.ptr); + kprintf("%d: type:%d size:%" PRIxSIZE " v:'%s'\n",__LINE__,v->type,v->size,v->as.ptr); break; case VAL_data: - fprintf(stderr,"%d: type:%d size:%zx v:\n",__LINE__,v->type,v->size); - hexdump(v->as.ptr, 16); + kprintf("%d: type:%d size:%" PRIxSIZE " v:\n",__LINE__,v->type,v->size); + hexdump(v->as.ptr, 16, errout); break; } } #endif static DIE * -di_read_die(DebugInfoReader *reader, DIE *die) +di_read_die(DebugInfoReader *reader, DIE *die, FILE *errout) { uint64_t abbrev_number = uleb128(&reader->p); if (abbrev_number == 0) { @@ -1338,7 +1454,7 @@ di_read_die(DebugInfoReader *reader, DIE *die) return NULL; } - reader->q = di_find_abbrev(reader, abbrev_number); + if (!(reader->q = di_find_abbrev(reader, abbrev_number, errout))) return NULL; die->pos = reader->p - reader->obj->debug_info.ptr - 1; die->tag = (int)uleb128(&reader->q); /* tag */ @@ -1350,27 +1466,109 @@ di_read_die(DebugInfoReader *reader, DIE *die) } static DebugInfoValue * -di_read_record(DebugInfoReader *reader, DebugInfoValue *vp) +di_read_record(DebugInfoReader *reader, DebugInfoValue *vp, FILE *errout) { uint64_t at = uleb128(&reader->q); uint64_t form = uleb128(&reader->q); if (!at || !form) return NULL; vp->at = at; vp->form = form; - debug_info_reader_read_value(reader, form, vp); + if (!debug_info_reader_read_value(reader, form, vp, errout)) return NULL; return vp; } -static void -di_skip_records(DebugInfoReader *reader) +static bool +di_skip_records(DebugInfoReader *reader, FILE *errout) { for (;;) { - DebugInfoValue v = {{}}; + DebugInfoValue v = {{0}}; uint64_t at = uleb128(&reader->q); uint64_t form = uleb128(&reader->q); - if (!at || !form) return; - debug_info_reader_read_value(reader, form, &v); + if (!at || !form) return true; + if (!debug_info_reader_read_value(reader, form, &v, errout)) return false; + } +} + +typedef struct addr_header { + const char *ptr; + uint64_t unit_length; + uint8_t format; + uint8_t address_size; + /* uint8_t segment_selector_size; */ +} addr_header_t; + +static bool +addr_header_init(obj_info_t *obj, addr_header_t *header, FILE *errout) +{ + const char *p = obj->debug_addr.ptr; + + header->ptr = p; + + if (!p) return true; + + header->unit_length = *(uint32_t *)p; + p += sizeof(uint32_t); + + header->format = 4; + if (header->unit_length == 0xffffffff) { + header->unit_length = *(uint64_t *)p; + p += sizeof(uint64_t); + header->format = 8; + } + + p += 2; /* version */ + header->address_size = *p++; + if (header->address_size != 4 && header->address_size != 8) { + kprintf("unknown address_size:%d", header->address_size); + return false; } + p++; /* segment_selector_size */ + return true; +} + +static uint64_t +read_addr(addr_header_t *header, uint64_t addr_base, uint64_t idx) { + if (header->address_size == 4) { + return ((uint32_t*)(header->ptr + addr_base))[idx]; + } + else { + return ((uint64_t*)(header->ptr + addr_base))[idx]; + } +} + +typedef struct rnglists_header { + uint64_t unit_length; + uint8_t format; + uint8_t address_size; + uint32_t offset_entry_count; +} rnglists_header_t; + +static bool +rnglists_header_init(obj_info_t *obj, rnglists_header_t *header, FILE *errout) +{ + const char *p = obj->debug_rnglists.ptr; + + if (!p) return true; + + header->unit_length = *(uint32_t *)p; + p += sizeof(uint32_t); + + header->format = 4; + if (header->unit_length == 0xffffffff) { + header->unit_length = *(uint64_t *)p; + p += sizeof(uint64_t); + header->format = 8; + } + + p += 2; /* version */ + header->address_size = *p++; + if (header->address_size != 4 && header->address_size != 8) { + kprintf("unknown address_size:%d", header->address_size); + return false; + } + p++; /* segment_selector_size */ + header->offset_entry_count = *(uint32_t *)p; + return true; } typedef struct { @@ -1383,50 +1581,54 @@ typedef struct { } ranges_t; static void -ranges_set(ranges_t *ptr, DebugInfoValue *v) +ranges_set(ranges_t *ptr, DebugInfoValue *v, addr_header_t *addr_header, uint64_t addr_base) { + uint64_t n = 0; + if (v->type == VAL_uint) { + n = v->as.uint64; + } + else if (v->type == VAL_addr) { + n = read_addr(addr_header, addr_base, v->as.addr_idx); + } switch (v->at) { case DW_AT_low_pc: - ptr->low_pc = v->as.uint64; + ptr->low_pc = n; ptr->low_pc_set = true; break; case DW_AT_high_pc: if (v->form == DW_FORM_addr) { - ptr->high_pc = v->as.uint64; + ptr->high_pc = n; } else { - ptr->high_pc = ptr->low_pc + v->as.uint64; + ptr->high_pc = ptr->low_pc + n; } ptr->high_pc_set = true; break; case DW_AT_ranges: - ptr->ranges = v->as.uint64; + ptr->ranges = n; ptr->ranges_set = true; break; } } static uint64_t -read_dw_form_addr(DebugInfoReader *reader, const char **ptr) +read_dw_form_addr(DebugInfoReader *reader, const char **ptr, FILE *errout) { const char *p = *ptr; *ptr = p + reader->address_size; if (reader->address_size == 4) { return read_uint32(&p); - } else if (reader->address_size == 8) { - return read_uint64(&p); } else { - fprintf(stderr,"unknown address_size:%d", reader->address_size); - abort(); + return read_uint64(&p); } } static uintptr_t -ranges_include(DebugInfoReader *reader, ranges_t *ptr, uint64_t addr) +ranges_include(DebugInfoReader *reader, ranges_t *ptr, uint64_t addr, rnglists_header_t *rnglists_header, FILE *errout) { if (ptr->high_pc_set) { if (ptr->ranges_set || !ptr->low_pc_set) { - exit(1); + return UINTPTR_MAX; } if (ptr->low_pc <= addr && addr <= ptr->high_pc) { return (uintptr_t)ptr->low_pc; @@ -1437,8 +1639,21 @@ ranges_include(DebugInfoReader *reader, ranges_t *ptr, uint64_t addr) const char *p; uint64_t base = ptr->low_pc_set ? ptr->low_pc : reader->current_low_pc; bool base_valid = true; - if (reader->obj->debug_rnglists.ptr) { - p = reader->obj->debug_rnglists.ptr + ptr->ranges; + if (reader->current_version >= 5) { + if (rnglists_header->offset_entry_count == 0) { + // DW_FORM_sec_offset + p = reader->obj->debug_rnglists.ptr + ptr->ranges + reader->current_rnglists_base; + } + else { + // DW_FORM_rnglistx + const char *offset_array = reader->obj->debug_rnglists.ptr + reader->current_rnglists_base; + if (rnglists_header->format == 4) { + p = offset_array + ((uint32_t *)offset_array)[ptr->ranges]; + } + else { + p = offset_array + ((uint64_t *)offset_array)[ptr->ranges]; + } + } for (;;) { uint8_t rle = read_uint8(&p); uintptr_t from = 0, to = 0; @@ -1462,15 +1677,15 @@ ranges_include(DebugInfoReader *reader, ranges_t *ptr, uint64_t addr) to = (uintptr_t)base + uleb128(&p); break; case DW_RLE_base_address: - base = read_dw_form_addr(reader, &p); + base = read_dw_form_addr(reader, &p, errout); base_valid = true; break; case DW_RLE_start_end: - from = (uintptr_t)read_dw_form_addr(reader, &p); - to = (uintptr_t)read_dw_form_addr(reader, &p); + from = (uintptr_t)read_dw_form_addr(reader, &p, errout); + to = (uintptr_t)read_dw_form_addr(reader, &p, errout); break; case DW_RLE_start_length: - from = (uintptr_t)read_dw_form_addr(reader, &p); + from = (uintptr_t)read_dw_form_addr(reader, &p, errout); to = from + uleb128(&p); break; } @@ -1478,7 +1693,7 @@ ranges_include(DebugInfoReader *reader, ranges_t *ptr, uint64_t addr) return from; } } - return false; + return 0; } p = reader->obj->debug_ranges.ptr + ptr->ranges; for (;;) { @@ -1499,42 +1714,42 @@ ranges_include(DebugInfoReader *reader, ranges_t *ptr, uint64_t addr) return (uintptr_t)ptr->low_pc; } } - return false; + return 0; } #if 0 static void -ranges_inspect(DebugInfoReader *reader, ranges_t *ptr) +ranges_inspect(DebugInfoReader *reader, ranges_t *ptr, FILE *errout) { if (ptr->high_pc_set) { if (ptr->ranges_set || !ptr->low_pc_set) { - fprintf(stderr,"low_pc_set:%d high_pc_set:%d ranges_set:%d\n",ptr->low_pc_set,ptr->high_pc_set,ptr->ranges_set); - exit(1); + kprintf("low_pc_set:%d high_pc_set:%d ranges_set:%d\n",ptr->low_pc_set,ptr->high_pc_set,ptr->ranges_set); + return; } - fprintf(stderr,"low_pc:%"PRIx64" high_pc:%"PRIx64"\n",ptr->low_pc,ptr->high_pc); + kprintf("low_pc:%"PRIx64" high_pc:%"PRIx64"\n",ptr->low_pc,ptr->high_pc); } else if (ptr->ranges_set) { char *p = reader->obj->debug_ranges.ptr + ptr->ranges; - fprintf(stderr,"low_pc:%"PRIx64" ranges:%"PRIx64" %lx ",ptr->low_pc,ptr->ranges, p-reader->obj->mapped); + kprintf("low_pc:%"PRIx64" ranges:%"PRIx64" %lx ",ptr->low_pc,ptr->ranges, p-reader->obj->mapped); for (;;) { uintptr_t from = read_uintptr(&p); uintptr_t to = read_uintptr(&p); if (!from && !to) break; - fprintf(stderr,"%"PRIx64"-%"PRIx64" ",ptr->low_pc+from,ptr->low_pc+to); + kprintf("%"PRIx64"-%"PRIx64" ",ptr->low_pc+from,ptr->low_pc+to); } - fprintf(stderr,"\n"); + kprintf("\n"); } else if (ptr->low_pc_set) { - fprintf(stderr,"low_pc:%"PRIx64"\n",ptr->low_pc); + kprintf("low_pc:%"PRIx64"\n",ptr->low_pc); } else { - fprintf(stderr,"empty\n"); + kprintf("empty\n"); } } #endif static int -di_read_cu(DebugInfoReader *reader) +di_read_cu(DebugInfoReader *reader, FILE *errout) { uint64_t unit_length; uint16_t version; @@ -1548,6 +1763,7 @@ di_read_cu(DebugInfoReader *reader) } reader->cu_end = reader->p + unit_length; version = read_uint16(&reader->p); + reader->current_version = version; if (version > 5) { return -1; } @@ -1560,43 +1776,72 @@ di_read_cu(DebugInfoReader *reader) debug_abbrev_offset = read_uint(reader); reader->address_size = read_uint8(&reader->p); } + if (reader->address_size != 4 && reader->address_size != 8) { + kprintf("unknown address_size:%d", reader->address_size); + return -1; + } reader->q0 = reader->obj->debug_abbrev.ptr + debug_abbrev_offset; reader->level = 0; di_read_debug_abbrev_cu(reader); - if (di_read_debug_line_cu(reader)) return -1; + if (di_read_debug_line_cu(reader, errout)) return -1; -#if defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER_BUILD_DATE) - /* Though DWARF specifies "the applicable base address defaults to the base - address of the compilation unit", but GCC seems to use zero as default */ -#else do { DIE die; - if (!di_read_die(reader, &die)) continue; + if (!di_read_die(reader, &die, errout)) continue; if (die.tag != DW_TAG_compile_unit) { - di_skip_records(reader); + if (!di_skip_records(reader, errout)) return -1; break; } + reader->current_str_offsets_base = 0; + reader->current_addr_base = 0; + reader->current_rnglists_base = 0; + + DebugInfoValue low_pc = {{0}}; /* enumerate abbrev */ for (;;) { - DebugInfoValue v = {{}}; - if (!di_read_record(reader, &v)) break; + DebugInfoValue v = {{0}}; + if (!di_read_record(reader, &v, errout)) break; switch (v.at) { case DW_AT_low_pc: - reader->current_low_pc = v.as.uint64; + // clang may output DW_AT_addr_base after DW_AT_low_pc. + // We need to resolve the DW_FORM_addr* after DW_AT_addr_base is parsed. + low_pc = v; + break; + case DW_AT_str_offsets_base: + reader->current_str_offsets_base = v.as.uint64; + break; + case DW_AT_addr_base: + reader->current_addr_base = v.as.uint64; + break; + case DW_AT_rnglists_base: + reader->current_rnglists_base = v.as.uint64; break; } } + // Resolve the DW_FORM_addr of DW_AT_low_pc + switch (low_pc.type) { + case VAL_uint: + reader->current_low_pc = low_pc.as.uint64; + break; + case VAL_addr: + { + addr_header_t header = {0}; + if (!addr_header_init(reader->obj, &header, errout)) return -1; + reader->current_low_pc = read_addr(&header, reader->current_addr_base, low_pc.as.addr_idx); + } + break; + } } while (0); -#endif + return 0; } static void -read_abstract_origin(DebugInfoReader *reader, uint64_t form, uint64_t abstract_origin, line_info_t *line) +read_abstract_origin(DebugInfoReader *reader, uint64_t form, uint64_t abstract_origin, line_info_t *line, FILE *errout) { const char *p = reader->p; const char *q = reader->q; @@ -1621,12 +1866,12 @@ read_abstract_origin(DebugInfoReader *reader, uint64_t form, uint64_t abstract_o default: goto finish; } - if (!di_read_die(reader, &die)) goto finish; + if (!di_read_die(reader, &die, errout)) goto finish; /* enumerate abbrev */ for (;;) { - DebugInfoValue v = {{}}; - if (!di_read_record(reader, &v)) break; + DebugInfoValue v = {{0}}; + if (!di_read_record(reader, &v, errout)) break; switch (v.at) { case DW_AT_name: line->sname = get_cstr_value(&v); @@ -1640,36 +1885,44 @@ read_abstract_origin(DebugInfoReader *reader, uint64_t form, uint64_t abstract_o reader->level = level; } -static void +static bool debug_info_read(DebugInfoReader *reader, int num_traces, void **traces, - line_info_t *lines, int offset) { + line_info_t *lines, int offset, FILE *errout) +{ + + addr_header_t addr_header = {0}; + if (!addr_header_init(reader->obj, &addr_header, errout)) return false; + + rnglists_header_t rnglists_header = {0}; + if (!rnglists_header_init(reader->obj, &rnglists_header, errout)) return false; + while (reader->p < reader->cu_end) { DIE die; - ranges_t ranges = {}; - line_info_t line = {}; + ranges_t ranges = {0}; + line_info_t line = {0}; - if (!di_read_die(reader, &die)) continue; - /* fprintf(stderr,"%d:%tx: <%d>\n",__LINE__,die.pos,reader->level,die.tag); */ + if (!di_read_die(reader, &die, errout)) continue; + /* kprintf("%d:%tx: <%d>\n",__LINE__,die.pos,reader->level,die.tag); */ if (die.tag != DW_TAG_subprogram && die.tag != DW_TAG_inlined_subroutine) { skip_die: - di_skip_records(reader); + if (!di_skip_records(reader, errout)) return false; continue; } /* enumerate abbrev */ for (;;) { - DebugInfoValue v = {{}}; + DebugInfoValue v = {{0}}; /* ptrdiff_t pos = reader->p - reader->p0; */ - if (!di_read_record(reader, &v)) break; - /* fprintf(stderr,"\n%d:%tx: AT:%lx FORM:%lx\n",__LINE__,pos,v.at,v.form); */ - /* div_inspect(&v); */ + if (!di_read_record(reader, &v, errout)) break; + /* kprintf("\n%d:%tx: AT:%lx FORM:%lx\n",__LINE__,pos,v.at,v.form); */ + /* div_inspect(&v, errout); */ switch (v.at) { case DW_AT_name: line.sname = get_cstr_value(&v); break; case DW_AT_call_file: - fill_filename((int)v.as.uint64, reader->debug_line_directories, reader->debug_line_files, &line, reader->obj); + fill_filename((int)v.as.uint64, reader->debug_line_format, reader->debug_line_version, reader->debug_line_directories, reader->debug_line_files, &line, reader->obj, errout); break; case DW_AT_call_line: line.line = (int)v.as.uint64; @@ -1677,7 +1930,7 @@ debug_info_read(DebugInfoReader *reader, int num_traces, void **traces, case DW_AT_low_pc: case DW_AT_high_pc: case DW_AT_ranges: - ranges_set(&ranges, &v); + ranges_set(&ranges, &v, &addr_header, reader->current_addr_base); break; case DW_AT_declaration: goto skip_die; @@ -1685,18 +1938,19 @@ debug_info_read(DebugInfoReader *reader, int num_traces, void **traces, /* 1 or 3 */ break; /* goto skip_die; */ case DW_AT_abstract_origin: - read_abstract_origin(reader, v.form, v.as.uint64, &line); + read_abstract_origin(reader, v.form, v.as.uint64, &line, errout); break; /* goto skip_die; */ } } - /* ranges_inspect(reader, &ranges); */ - /* fprintf(stderr,"%d:%tx: %x ",__LINE__,diepos,die.tag); */ + /* ranges_inspect(reader, &ranges, errout); */ + /* kprintf("%d:%tx: %x ",__LINE__,diepos,die.tag); */ for (int i=offset; i < num_traces; i++) { uintptr_t addr = (uintptr_t)traces[i]; uintptr_t offset = addr - reader->obj->base_addr + reader->obj->vmaddr; - uintptr_t saddr = ranges_include(reader, &ranges, offset); + uintptr_t saddr = ranges_include(reader, &ranges, offset, &rnglists_header, errout); + if (saddr == UINTPTR_MAX) return false; if (saddr) { - /* fprintf(stderr, "%d:%tx: %d %lx->%lx %x %s: %s/%s %d %s %s %s\n",__LINE__,die.pos, i,addr,offset, die.tag,line.sname,line.dirname,line.filename,line.line,reader->obj->path,line.sname,lines[i].sname); */ + /* kprintf("%d:%tx: %d %lx->%lx %x %s: %s/%s %d %s %s %s\n",__LINE__,die.pos, i,addr,offset, die.tag,line.sname,line.dirname,line.filename,line.line,reader->obj->path,line.sname,lines[i].sname); */ if (lines[i].sname) { line_info_t *lp = malloc(sizeof(line_info_t)); memcpy(lp, &lines[i], sizeof(line_info_t)); @@ -1713,6 +1967,58 @@ debug_info_read(DebugInfoReader *reader, int num_traces, void **traces, } } } + return true; +} + +// This function parses the following attributes of Line Number Program Header in DWARF 5: +// +// * directory_entry_format_count +// * directory_entry_format +// * directories_count +// * directories +// +// or +// +// * file_name_entry_format_count +// * file_name_entry_format +// * file_names_count +// * file_names +// +// It records DW_LNCT_path and DW_LNCT_directory_index at the index "idx". +static const char * +parse_ver5_debug_line_header(const char *p, int idx, uint8_t format, + obj_info_t *obj, const char **out_path, + uint64_t *out_directory_index, FILE *errout) +{ + int i, j; + int entry_format_count = *(uint8_t *)p++; + const char *entry_format = p; + + /* skip the part of entry_format */ + for (i = 0; i < entry_format_count * 2; i++) uleb128(&p); + + int entry_count = (int)uleb128(&p); + + DebugInfoReader reader = {0}; + debug_info_reader_init(&reader, obj); + reader.format = format; + reader.p = p; + for (j = 0; j < entry_count; j++) { + const char *format = entry_format; + for (i = 0; i < entry_format_count; i++) { + DebugInfoValue v = {{0}}; + unsigned long dw_lnct = uleb128(&format); + unsigned long dw_form = uleb128(&format); + if (!debug_info_reader_read_value(&reader, dw_form, &v, errout)) return 0; + if (dw_lnct == 1 /* DW_LNCT_path */ && v.type == VAL_cstr && out_path) + *out_path = v.as.ptr + v.off; + if (dw_lnct == 2 /* DW_LNCT_directory_index */ && v.type == VAL_uint && out_directory_index) + *out_directory_index = v.as.uint64; + } + if (j == idx) return 0; + } + + return reader.p; } #ifdef USE_ELF @@ -1726,14 +2032,14 @@ uncompress_debug_section(ElfW(Shdr) *shdr, char *file, char **ptr) int ret = 0; if (chdr->ch_type != ELFCOMPRESS_ZLIB) { - /* unsupported compression type */ - return 0; + /* unsupported compression type */ + return 0; } *ptr = malloc(destsize); if (!*ptr) return 0; ret = uncompress((Bytef *)*ptr, &destsize, - (const Bytef*)chdr + sizeof(ElfW(Chdr)), + (const Bytef*)chdr + sizeof(ElfW(Chdr)), shdr->sh_size - sizeof(ElfW(Chdr))); if (ret != Z_OK) goto fail; return destsize; @@ -1748,7 +2054,7 @@ fail: /* read file and fill lines */ static uintptr_t fill_lines(int num_traces, void **traces, int check_debuglink, - obj_info_t **objp, line_info_t *lines, int offset) + obj_info_t **objp, line_info_t *lines, int offset, FILE *errout) { int i, j; char *shstr; @@ -1766,40 +2072,40 @@ fill_lines(int num_traces, void **traces, int check_debuglink, fd = open(binary_filename, O_RDONLY); if (fd < 0) { - goto fail; + goto fail; } filesize = lseek(fd, 0, SEEK_END); if (filesize < 0) { - int e = errno; - close(fd); - kprintf("lseek: %s\n", strerror(e)); - goto fail; + int e = errno; + close(fd); + kprintf("lseek: %s\n", strerror(e)); + goto fail; } #if SIZEOF_OFF_T > SIZEOF_SIZE_T if (filesize > (off_t)SIZE_MAX) { - close(fd); - kprintf("Too large file %s\n", binary_filename); - goto fail; + close(fd); + kprintf("Too large file %s\n", binary_filename); + goto fail; } #endif lseek(fd, 0, SEEK_SET); /* async-signal unsafe */ file = (char *)mmap(NULL, (size_t)filesize, PROT_READ, MAP_SHARED, fd, 0); if (file == MAP_FAILED) { - int e = errno; - close(fd); - kprintf("mmap: %s\n", strerror(e)); - goto fail; + int e = errno; + close(fd); + kprintf("mmap: %s\n", strerror(e)); + goto fail; } close(fd); ehdr = (ElfW(Ehdr) *)file; if (memcmp(ehdr->e_ident, "\177ELF", 4) != 0) { - /* - * Huh? Maybe filename was overridden by setproctitle() and - * it match non-elf file. - */ - goto fail; + /* + * Huh? Maybe filename was overridden by setproctitle() and + * it match non-elf file. + */ + goto fail; } obj->mapped = file; obj->mapped_size = (size_t)filesize; @@ -1811,40 +2117,43 @@ fill_lines(int num_traces, void **traces, int check_debuglink, for (i = 0; i < ehdr->e_shnum; i++) { char *section_name = shstr + shdr[i].sh_name; - switch (shdr[i].sh_type) { - case SHT_STRTAB: - if (!strcmp(section_name, ".strtab")) { - strtab_shdr = shdr + i; - } - else if (!strcmp(section_name, ".dynstr")) { - dynstr_shdr = shdr + i; - } - break; - case SHT_SYMTAB: - /* if (!strcmp(section_name, ".symtab")) */ - symtab_shdr = shdr + i; - break; - case SHT_DYNSYM: - /* if (!strcmp(section_name, ".dynsym")) */ - dynsym_shdr = shdr + i; - break; + switch (shdr[i].sh_type) { + case SHT_STRTAB: + if (!strcmp(section_name, ".strtab")) { + strtab_shdr = shdr + i; + } + else if (!strcmp(section_name, ".dynstr")) { + dynstr_shdr = shdr + i; + } + break; + case SHT_SYMTAB: + /* if (!strcmp(section_name, ".symtab")) */ + symtab_shdr = shdr + i; + break; + case SHT_DYNSYM: + /* if (!strcmp(section_name, ".dynsym")) */ + dynsym_shdr = shdr + i; + break; case SHT_NOTE: if (!strcmp(section_name, ".note.gnu.build-id")) { note_gnu_build_id = shdr + i; } break; - case SHT_PROGBITS: - if (!strcmp(section_name, ".gnu_debuglink")) { - gnu_debuglink_shdr = shdr + i; - } + case SHT_PROGBITS: + if (!strcmp(section_name, ".gnu_debuglink")) { + gnu_debuglink_shdr = shdr + i; + } else { const char *debug_section_names[] = { ".debug_abbrev", ".debug_info", ".debug_line", ".debug_ranges", + ".debug_str_offsets", + ".debug_addr", ".debug_rnglists", - ".debug_str" + ".debug_str", + ".debug_line_str" }; for (j=0; j < DWARF_SECTION_COUNT; j++) { @@ -1863,17 +2172,16 @@ fill_lines(int num_traces, void **traces, int check_debuglink, break; } } - break; - } + break; + } } - if (offset == -1) { - /* main executable */ - offset = 0; - if (dynsym_shdr && dynstr_shdr) { - char *strtab = file + dynstr_shdr->sh_offset; - ElfW(Sym) *symtab = (ElfW(Sym) *)(file + dynsym_shdr->sh_offset); - int symtab_count = (int)(dynsym_shdr->sh_size / sizeof(ElfW(Sym))); + if (offset == 0) { + /* main executable */ + if (dynsym_shdr && dynstr_shdr) { + char *strtab = file + dynstr_shdr->sh_offset; + ElfW(Sym) *symtab = (ElfW(Sym) *)(file + dynsym_shdr->sh_offset); + int symtab_count = (int)(dynsym_shdr->sh_size / sizeof(ElfW(Sym))); void *handle = dlopen(NULL, RTLD_NOW|RTLD_LOCAL); if (handle) { for (j = 0; j < symtab_count; j++) { @@ -1890,14 +2198,14 @@ fill_lines(int num_traces, void **traces, int check_debuglink, } dlclose(handle); } - if (ehdr->e_type == ET_EXEC) { - obj->base_addr = 0; - } - else { - /* PIE (position-independent executable) */ - obj->base_addr = dladdr_fbase; - } - } + if (ehdr->e_type == ET_EXEC) { + obj->base_addr = 0; + } + else { + /* PIE (position-independent executable) */ + obj->base_addr = dladdr_fbase; + } + } } if (obj->debug_info.ptr && obj->debug_abbrev.ptr) { @@ -1905,9 +2213,10 @@ fill_lines(int num_traces, void **traces, int check_debuglink, debug_info_reader_init(&reader, obj); i = 0; while (reader.p < reader.pend) { - /* fprintf(stderr, "%d:%tx: CU[%d]\n", __LINE__, reader.p - reader.obj->debug_info.ptr, i++); */ - if (di_read_cu(&reader)) goto use_symtab; - debug_info_read(&reader, num_traces, traces, lines, offset); + /* kprintf("%d:%tx: CU[%d]\n", __LINE__, reader.p - reader.obj->debug_info.ptr, i++); */ + if (di_read_cu(&reader, errout)) goto use_symtab; + if (!debug_info_read(&reader, num_traces, traces, lines, offset, errout)) + goto use_symtab; } } else { @@ -1942,27 +2251,27 @@ use_symtab: } if (!obj->debug_line.ptr) { - /* This file doesn't have .debug_line section, - let's check .gnu_debuglink section instead. */ - if (gnu_debuglink_shdr && check_debuglink) { - follow_debuglink(file + gnu_debuglink_shdr->sh_offset, - num_traces, traces, - objp, lines, offset); - } + /* This file doesn't have .debug_line section, + let's check .gnu_debuglink section instead. */ + if (gnu_debuglink_shdr && check_debuglink) { + follow_debuglink(file + gnu_debuglink_shdr->sh_offset, + num_traces, traces, + objp, lines, offset, errout); + } if (note_gnu_build_id && check_debuglink) { ElfW(Nhdr) *nhdr = (ElfW(Nhdr)*) (file + note_gnu_build_id->sh_offset); const char *build_id = (char *)(nhdr + 1) + nhdr->n_namesz; follow_debuglink_build_id(build_id, nhdr->n_descsz, - num_traces, traces, - objp, lines, offset); + num_traces, traces, + objp, lines, offset, errout); } - goto finish; + goto finish; } if (parse_debug_line(num_traces, traces, obj->debug_line.ptr, obj->debug_line.size, - obj, lines, offset) == -1) + obj, lines, offset, errout) == -1) goto fail; finish: @@ -1974,7 +2283,7 @@ fail: /* read file and fill lines */ static uintptr_t fill_lines(int num_traces, void **traces, int check_debuglink, - obj_info_t **objp, line_info_t *lines, int offset) + obj_info_t **objp, line_info_t *lines, int offset, FILE *errout) { # ifdef __LP64__ # define LP(x) x##_64 @@ -2053,13 +2362,13 @@ fill_lines(int num_traces, void **traces, int check_debuglink, struct fat_header *fat = (struct fat_header *)file; char *q = file + sizeof(*fat); uint32_t nfat_arch = __builtin_bswap32(fat->nfat_arch); - /* fprintf(stderr,"%d: fat:%s %d\n",__LINE__, binary_filename,nfat_arch); */ + /* kprintf("%d: fat:%s %d\n",__LINE__, binary_filename,nfat_arch); */ for (uint32_t i = 0; i < nfat_arch; i++) { struct fat_arch *arch = (struct fat_arch *)q; cpu_type_t cputype = __builtin_bswap32(arch->cputype); cpu_subtype_t cpusubtype = __builtin_bswap32(arch->cpusubtype); uint32_t offset = __builtin_bswap32(arch->offset); - /* fprintf(stderr,"%d: fat %d %x/%x %x/%x\n",__LINE__, i, mhp->cputype,mhp->cpusubtype, cputype,cpusubtype); */ + /* kprintf("%d: fat %d %x/%x %x/%x\n",__LINE__, i, mhp->cputype,mhp->cpusubtype, cputype,cpusubtype); */ if (mhp->cputype == cputype && (cpu_subtype_t)(mhp->cpusubtype & ~CPU_SUBTYPE_MASK) == cpusubtype) { p = file + offset; @@ -2077,13 +2386,14 @@ fill_lines(int num_traces, void **traces, int check_debuglink, goto fail; } else { - kprintf("'%s' is not a " # ifdef __LP64__ - "64" +# define bitsize "64" # else - "32" +# define bitsize "32" # endif + kprintf("'%s' is not a " bitsize "-bit Mach-O file!\n",binary_filename); +# undef bitsize close(fd); goto fail; } @@ -2100,8 +2410,11 @@ found_mach_header: "__debug_info", "__debug_line", "__debug_ranges", + "__debug_str_offsets", + "__debug_addr", "__debug_rnglists", - "__debug_str" + "__debug_str", + "__debug_line_str", }; struct LP(segment_command) *scmd = (struct LP(segment_command) *)lcmd; if (strcmp(scmd->segname, "__TEXT") == 0) { @@ -2115,7 +2428,17 @@ found_mach_header: for (int j=0; j < DWARF_SECTION_COUNT; j++) { struct dwarf_section *s = obj_dwarf_section_at(obj, j); - if (strcmp(sect->sectname, debug_section_names[j]) != 0) + if (strcmp(sect->sectname, debug_section_names[j]) != 0 +#ifdef __APPLE__ + /* macOS clang 16 generates DWARF5, which have Mach-O + * section names that are limited to 16 characters, + * which causes sections with long names to be truncated + * and not match above. + * See: https://wiki.dwarfstd.org/Best_Practices.md#Mach-2d-O + */ + && strncmp(sect->sectname, debug_section_names[j], 16) != 0 +#endif + ) continue; s->ptr = file + sect->offset; @@ -2172,15 +2495,16 @@ found_mach_header: DebugInfoReader reader; debug_info_reader_init(&reader, obj); while (reader.p < reader.pend) { - if (di_read_cu(&reader)) goto fail; - debug_info_read(&reader, num_traces, traces, lines, offset); + if (di_read_cu(&reader, errout)) goto fail; + if (!debug_info_read(&reader, num_traces, traces, lines, offset, errout)) + goto fail; } } if (parse_debug_line(num_traces, traces, obj->debug_line.ptr, obj->debug_line.size, - obj, lines, offset) == -1) + obj, lines, offset, errout) == -1) goto fail; return dladdr_fbase; @@ -2193,7 +2517,7 @@ fail: #if defined(__FreeBSD__) || defined(__DragonFly__) # include <sys/sysctl.h> #endif -/* ssize_t main_exe_path(void) +/* ssize_t main_exe_path(FILE *errout) * * store the path of the main executable to `binary_filename`, * and returns strlen(binary_filename). @@ -2201,7 +2525,7 @@ fail: */ #if defined(__linux__) || defined(__NetBSD__) static ssize_t -main_exe_path(void) +main_exe_path(FILE *errout) { # if defined(__linux__) # define PROC_SELF_EXE "/proc/self/exe" @@ -2215,21 +2539,21 @@ main_exe_path(void) } #elif defined(__FreeBSD__) || defined(__DragonFly__) static ssize_t -main_exe_path(void) +main_exe_path(FILE *errout) { int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; size_t len = PATH_MAX; int err = sysctl(mib, 4, binary_filename, &len, NULL, 0); if (err) { - kprintf("Can't get the path of ruby"); - return -1; + kprintf("Can't get the path of ruby"); + return -1; } len--; /* sysctl sets strlen+1 */ return len; } #elif defined(HAVE_LIBPROC_H) static ssize_t -main_exe_path(void) +main_exe_path(FILE *errout) { int len = proc_pidpath(getpid(), binary_filename, PATH_MAX); if (len == 0) return 0; @@ -2241,7 +2565,7 @@ main_exe_path(void) #endif static void -print_line0(line_info_t *line, void *address) +print_line0(line_info_t *line, void *address, FILE *errout) { uintptr_t addr = (uintptr_t)address; uintptr_t d = addr - line->saddr; @@ -2257,9 +2581,12 @@ print_line0(line_info_t *line, void *address) else if (!line->path) { kprintf("[0x%"PRIxPTR"]\n", addr); } - else if (!line->saddr || !line->sname) { + else if (!line->sname) { kprintf("%s(0x%"PRIxPTR") [0x%"PRIxPTR"]\n", line->path, addr-line->base_addr, addr); } + else if (!line->saddr) { + kprintf("%s(%s) [0x%"PRIxPTR"]\n", line->path, line->sname, addr); + } else if (line->line <= 0) { kprintf("%s(%s+0x%"PRIxPTR") [0x%"PRIxPTR"]\n", line->path, line->sname, d, addr); @@ -2279,16 +2606,16 @@ print_line0(line_info_t *line, void *address) } static void -print_line(line_info_t *line, void *address) +print_line(line_info_t *line, void *address, FILE *errout) { - print_line0(line, address); + print_line0(line, address, errout); if (line->next) { - print_line(line->next, NULL); + print_line(line->next, NULL, errout); } } void -rb_dump_backtrace_with_lines(int num_traces, void **traces) +rb_dump_backtrace_with_lines(int num_traces, void **traces, FILE *errout) { int i; /* async-signal unsafe */ @@ -2296,81 +2623,84 @@ rb_dump_backtrace_with_lines(int num_traces, void **traces) obj_info_t *obj = NULL; /* 2 is NULL + main executable */ void **dladdr_fbases = (void **)calloc(num_traces+2, sizeof(void *)); + #ifdef HAVE_MAIN_EXE_PATH char *main_path = NULL; /* used on printing backtrace */ ssize_t len; - if ((len = main_exe_path()) > 0) { - main_path = (char *)alloca(len + 1); - if (main_path) { - uintptr_t addr; - memcpy(main_path, binary_filename, len+1); - append_obj(&obj); - obj->path = main_path; - addr = fill_lines(num_traces, traces, 1, &obj, lines, -1); - if (addr != (uintptr_t)-1) { - dladdr_fbases[0] = (void *)addr; - } - } + if ((len = main_exe_path(errout)) > 0) { + main_path = (char *)alloca(len + 1); + if (main_path) { + uintptr_t addr; + memcpy(main_path, binary_filename, len+1); + append_obj(&obj); + obj->path = main_path; + addr = fill_lines(num_traces, traces, 1, &obj, lines, 0, errout); + if (addr != (uintptr_t)-1) { + dladdr_fbases[0] = (void *)addr; + } + } } #endif /* fill source lines by reading dwarf */ for (i = 0; i < num_traces; i++) { - Dl_info info; - if (lines[i].line) continue; - if (dladdr(traces[i], &info)) { - const char *path; - void **p; - - /* skip symbols which is in already checked objects */ - /* if the binary is strip-ed, this may effect */ - for (p=dladdr_fbases; *p; p++) { - if (*p == info.dli_fbase) { - lines[i].path = info.dli_fname; - lines[i].sname = info.dli_sname; - goto next_line; - } - } - *p = info.dli_fbase; - - append_obj(&obj); - obj->base_addr = (uintptr_t)info.dli_fbase; - path = info.dli_fname; - obj->path = path; - lines[i].path = path; - lines[i].sname = info.dli_sname; - lines[i].saddr = (uintptr_t)info.dli_saddr; - strlcpy(binary_filename, path, PATH_MAX); - if (fill_lines(num_traces, traces, 1, &obj, lines, i) == (uintptr_t)-1) - break; - } + Dl_info info; + if (lines[i].line) continue; + if (dladdr(traces[i], &info)) { + const char *path; + void **p; + + /* skip symbols which is in already checked objects */ + /* if the binary is strip-ed, this may effect */ + for (p=dladdr_fbases; *p; p++) { + if (*p == info.dli_fbase) { + if (info.dli_fname) lines[i].path = info.dli_fname; + if (info.dli_sname) lines[i].sname = info.dli_sname; + goto next_line; + } + } + *p = info.dli_fbase; + + append_obj(&obj); + obj->base_addr = (uintptr_t)info.dli_fbase; + path = info.dli_fname; + obj->path = path; + if (path) lines[i].path = path; + if (info.dli_sname) { + lines[i].sname = info.dli_sname; + lines[i].saddr = (uintptr_t)info.dli_saddr; + } + strlcpy(binary_filename, path, PATH_MAX); + if (fill_lines(num_traces, traces, 1, &obj, lines, i, errout) == (uintptr_t)-1) + break; + } next_line: - continue; + continue; } /* output */ for (i = 0; i < num_traces; i++) { - print_line(&lines[i], traces[i]); + print_line(&lines[i], traces[i], errout); - /* FreeBSD's backtrace may show _start and so on */ - if (lines[i].sname && strcmp("main", lines[i].sname) == 0) - break; + /* FreeBSD's backtrace may show _start and so on */ + if (lines[i].sname && strcmp("main", lines[i].sname) == 0) + break; } /* free */ while (obj) { - obj_info_t *o = obj; + obj_info_t *o = obj; for (i=0; i < DWARF_SECTION_COUNT; i++) { struct dwarf_section *s = obj_dwarf_section_at(obj, i); if (s->flags & SHF_COMPRESSED) { free(s->ptr); } } - if (obj->mapped_size) { - munmap(obj->mapped, obj->mapped_size); - } - obj = o->next; - free(o); + if (obj->mapped_size) { + munmap(obj->mapped, obj->mapped_size); + } + obj = o->next; + free(o); } for (i = 0; i < num_traces; i++) { line_info_t *line = lines[i].next; @@ -2384,435 +2714,8 @@ next_line: free(dladdr_fbases); } -/* From FreeBSD's lib/libstand/printf.c */ -/*- - * Copyright (c) 1986, 1988, 1991, 1993 - * The Regents of the University of California. All rights reserved. - * (c) UNIX System Laboratories, Inc. - * All or some portions of this file are derived from material licensed - * to the University of California by American Telephone and Telegraph - * Co. or Unix System Laboratories, Inc. and are reproduced herein with - * the permission of UNIX System Laboratories, Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 4. Neither the name of the University nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY - * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - * - * @(#)subr_prf.c 8.3 (Berkeley) 1/21/94 - */ - -#include <stdarg.h> -#define MAXNBUF (sizeof(intmax_t) * CHAR_BIT + 1) -static inline int toupper(int c) { return ('A' <= c && c <= 'Z') ? (c&0x5f) : c; } -#define hex2ascii(hex) (hex2ascii_data[hex]) -static const char hex2ascii_data[] = "0123456789abcdefghijklmnopqrstuvwxyz"; -static inline int imax(int a, int b) { return (a > b ? a : b); } -static int kvprintf(char const *fmt, void (*func)(int), void *arg, int radix, va_list ap); - -static void putce(int c) -{ - char s[1]; - ssize_t ret; - - s[0] = (char)c; - ret = write(2, s, 1); - (void)ret; -} - -static int -kprintf(const char *fmt, ...) -{ - va_list ap; - int retval; - - va_start(ap, fmt); - retval = kvprintf(fmt, putce, NULL, 10, ap); - va_end(ap); - return retval; -} - -/* - * Put a NUL-terminated ASCII number (base <= 36) in a buffer in reverse - * order; return an optional length and a pointer to the last character - * written in the buffer (i.e., the first character of the string). - * The buffer pointed to by `nbuf' must have length >= MAXNBUF. - */ -static char * -ksprintn(char *nbuf, uintmax_t num, int base, int *lenp, int upper) -{ - char *p, c; - - p = nbuf; - *p = '\0'; - do { - c = hex2ascii(num % base); - *++p = upper ? toupper(c) : c; - } while (num /= base); - if (lenp) - *lenp = (int)(p - nbuf); - return (p); -} +#undef kprintf -/* - * Scaled down version of printf(3). - * - * Two additional formats: - * - * The format %b is supported to decode error registers. - * Its usage is: - * - * printf("reg=%b\n", regval, "<base><arg>*"); - * - * where <base> is the output base expressed as a control character, e.g. - * \10 gives octal; \20 gives hex. Each arg is a sequence of characters, - * the first of which gives the bit number to be inspected (origin 1), and - * the next characters (up to a control character, i.e. a character <= 32), - * give the name of the register. Thus: - * - * kvprintf("reg=%b\n", 3, "\10\2BITTWO\1BITONE\n"); - * - * would produce output: - * - * reg=3<BITTWO,BITONE> - * - * XXX: %D -- Hexdump, takes pointer and separator string: - * ("%6D", ptr, ":") -> XX:XX:XX:XX:XX:XX - * ("%*D", len, ptr, " " -> XX XX XX XX ... - */ -static int -kvprintf(char const *fmt, void (*func)(int), void *arg, int radix, va_list ap) -{ -#define PCHAR(c) {int cc=(c); if (func) (*func)(cc); else *d++ = cc; retval++; } - char nbuf[MAXNBUF]; - char *d; - const char *p, *percent, *q; - unsigned char *up; - int ch, n; - uintmax_t num; - int base, lflag, qflag, tmp, width, ladjust, sharpflag, neg, sign, dot; - int cflag, hflag, jflag, tflag, zflag; - int dwidth, upper; - char padc; - int stop = 0, retval = 0; - - num = 0; - if (!func) - d = (char *) arg; - else - d = NULL; - - if (fmt == NULL) - fmt = "(fmt null)\n"; - - if (radix < 2 || radix > 36) - radix = 10; - - for (;;) { - padc = ' '; - width = 0; - while ((ch = (unsigned char)*fmt++) != '%' || stop) { - if (ch == '\0') - return (retval); - PCHAR(ch); - } - percent = fmt - 1; - qflag = 0; lflag = 0; ladjust = 0; sharpflag = 0; neg = 0; - sign = 0; dot = 0; dwidth = 0; upper = 0; - cflag = 0; hflag = 0; jflag = 0; tflag = 0; zflag = 0; -reswitch: switch (ch = (unsigned char)*fmt++) { - case '.': - dot = 1; - goto reswitch; - case '#': - sharpflag = 1; - goto reswitch; - case '+': - sign = 1; - goto reswitch; - case '-': - ladjust = 1; - goto reswitch; - case '%': - PCHAR(ch); - break; - case '*': - if (!dot) { - width = va_arg(ap, int); - if (width < 0) { - ladjust = !ladjust; - width = -width; - } - } else { - dwidth = va_arg(ap, int); - } - goto reswitch; - case '0': - if (!dot) { - padc = '0'; - goto reswitch; - } - case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - for (n = 0;; ++fmt) { - n = n * 10 + ch - '0'; - ch = *fmt; - if (ch < '0' || ch > '9') - break; - } - if (dot) - dwidth = n; - else - width = n; - goto reswitch; - case 'b': - num = (unsigned int)va_arg(ap, int); - p = va_arg(ap, char *); - for (q = ksprintn(nbuf, num, *p++, NULL, 0); *q;) - PCHAR(*q--); - - if (num == 0) - break; - - for (tmp = 0; *p;) { - n = *p++; - if (num & (1 << (n - 1))) { - PCHAR(tmp ? ',' : '<'); - for (; (n = *p) > ' '; ++p) - PCHAR(n); - tmp = 1; - } else - for (; *p > ' '; ++p) - continue; - } - if (tmp) - PCHAR('>'); - break; - case 'c': - PCHAR(va_arg(ap, int)); - break; - case 'D': - up = va_arg(ap, unsigned char *); - p = va_arg(ap, char *); - if (!width) - width = 16; - while(width--) { - PCHAR(hex2ascii(*up >> 4)); - PCHAR(hex2ascii(*up & 0x0f)); - up++; - if (width) - for (q=p;*q;q++) - PCHAR(*q); - } - break; - case 'd': - case 'i': - base = 10; - sign = 1; - goto handle_sign; - case 'h': - if (hflag) { - hflag = 0; - cflag = 1; - } else - hflag = 1; - goto reswitch; - case 'j': - jflag = 1; - goto reswitch; - case 'l': - if (lflag) { - lflag = 0; - qflag = 1; - } else - lflag = 1; - goto reswitch; - case 'n': - if (jflag) - *(va_arg(ap, intmax_t *)) = retval; - else if (qflag) - *(va_arg(ap, int64_t *)) = retval; - else if (lflag) - *(va_arg(ap, long *)) = retval; - else if (zflag) - *(va_arg(ap, size_t *)) = retval; - else if (hflag) - *(va_arg(ap, short *)) = retval; - else if (cflag) - *(va_arg(ap, char *)) = retval; - else - *(va_arg(ap, int *)) = retval; - break; - case 'o': - base = 8; - goto handle_nosign; - case 'p': - base = 16; - sharpflag = (width == 0); - sign = 0; - num = (uintptr_t)va_arg(ap, void *); - goto number; - case 'q': - qflag = 1; - goto reswitch; - case 'r': - base = radix; - if (sign) - goto handle_sign; - goto handle_nosign; - case 's': - p = va_arg(ap, char *); - if (p == NULL) - p = "(null)"; - if (!dot) - n = (int)strlen (p); - else - for (n = 0; n < dwidth && p[n]; n++) - continue; - - width -= n; - - if (!ladjust && width > 0) - while (width--) - PCHAR(padc); - while (n--) - PCHAR(*p++); - if (ladjust && width > 0) - while (width--) - PCHAR(padc); - break; - case 't': - tflag = 1; - goto reswitch; - case 'u': - base = 10; - goto handle_nosign; - case 'X': - upper = 1; - case 'x': - base = 16; - goto handle_nosign; - case 'y': - base = 16; - sign = 1; - goto handle_sign; - case 'z': - zflag = 1; - goto reswitch; -handle_nosign: - sign = 0; - if (jflag) - num = va_arg(ap, uintmax_t); - else if (qflag) - num = va_arg(ap, uint64_t); - else if (tflag) - num = va_arg(ap, ptrdiff_t); - else if (lflag) - num = va_arg(ap, unsigned long); - else if (zflag) - num = va_arg(ap, size_t); - else if (hflag) - num = (unsigned short)va_arg(ap, int); - else if (cflag) - num = (unsigned char)va_arg(ap, int); - else - num = va_arg(ap, unsigned int); - goto number; -handle_sign: - if (jflag) - num = va_arg(ap, intmax_t); - else if (qflag) - num = va_arg(ap, int64_t); - else if (tflag) - num = va_arg(ap, ptrdiff_t); - else if (lflag) - num = va_arg(ap, long); - else if (zflag) - num = va_arg(ap, ssize_t); - else if (hflag) - num = (short)va_arg(ap, int); - else if (cflag) - num = (char)va_arg(ap, int); - else - num = va_arg(ap, int); -number: - if (sign && (intmax_t)num < 0) { - neg = 1; - num = -(intmax_t)num; - } - p = ksprintn(nbuf, num, base, &n, upper); - tmp = 0; - if (sharpflag && num != 0) { - if (base == 8) - tmp++; - else if (base == 16) - tmp += 2; - } - if (neg) - tmp++; - - if (!ladjust && padc == '0') - dwidth = width - tmp; - width -= tmp + imax(dwidth, n); - dwidth -= n; - if (!ladjust) - while (width-- > 0) - PCHAR(' '); - if (neg) - PCHAR('-'); - if (sharpflag && num != 0) { - if (base == 8) { - PCHAR('0'); - } else if (base == 16) { - PCHAR('0'); - PCHAR('x'); - } - } - while (dwidth-- > 0) - PCHAR('0'); - - while (*p) - PCHAR(*p--); - - if (ladjust) - while (width-- > 0) - PCHAR(' '); - - break; - default: - while (percent < fmt) - PCHAR(*percent++); - /* - * Since we ignore an formatting argument it is no - * longer safe to obey the remaining formatting - * arguments as the arguments will no longer match - * the format specs. - */ - stop = 1; - break; - } - } -#undef PCHAR -} #else /* defined(USE_ELF) */ #error not supported #endif diff --git a/addr2line.h b/addr2line.h index f09b665800..ff8e476b92 100644 --- a/addr2line.h +++ b/addr2line.h @@ -12,8 +12,10 @@ #if (defined(USE_ELF) || defined(HAVE_MACH_O_LOADER_H)) +#include <stdio.h> + void -rb_dump_backtrace_with_lines(int num_traces, void **traces); +rb_dump_backtrace_with_lines(int num_traces, void **traces, FILE *errout); #endif /* USE_ELF */ @@ -27,8 +27,10 @@ #include "probes.h" #include "ruby/encoding.h" #include "ruby/st.h" +#include "ruby/thread.h" #include "ruby/util.h" -#include "transient_heap.h" +#include "ruby/ractor.h" +#include "vm_core.h" #include "builtin.h" #if !ARRAY_DEBUG @@ -38,6 +40,31 @@ #include "ruby_assert.h" VALUE rb_cArray; +VALUE rb_cArray_empty_frozen; + +/* Flags of RArray + * + * 0: RARRAY_SHARED_FLAG (equal to ELTS_SHARED) + * The array is shared. The buffer this array points to is owned by + * another array (the shared root). + * 1: RARRAY_EMBED_FLAG + * The array is embedded (its contents follow the header, rather than + * being on a separately allocated buffer). + * 3-9: RARRAY_EMBED_LEN + * The length of the array when RARRAY_EMBED_FLAG is set. + * 12: RARRAY_SHARED_ROOT_FLAG + * The array is a shared root that does reference counting. The buffer + * this array points to is owned by this array but may be pointed to + * by other arrays. + * Note: Frozen arrays may be a shared root without this flag being + * set. Frozen arrays do not have reference counting because + * they cannot be modified. Not updating the reference count + * improves copy-on-write performance. Their reference count is + * assumed to be infinity. + * 14: RARRAY_PTR_IN_USE_FLAG + * The buffer of the array is in use. This is only used during + * debugging. + */ /* for OPTIMIZED_CMP: */ #define id_cmp idCmp @@ -53,66 +80,49 @@ should_be_T_ARRAY(VALUE ary) return RB_TYPE_P(ary, T_ARRAY); } -RBIMPL_ATTR_MAYBE_UNUSED() -static int -should_not_be_shared_and_embedded(VALUE ary) -{ - return !FL_TEST((ary), ELTS_SHARED) || !FL_TEST((ary), RARRAY_EMBED_FLAG); -} - -#define ARY_SHARED_P(ary) \ - (assert(should_be_T_ARRAY((VALUE)(ary))), \ - assert(should_not_be_shared_and_embedded((VALUE)ary)), \ - FL_TEST_RAW((ary),ELTS_SHARED)!=0) - -#define ARY_EMBED_P(ary) \ - (assert(should_be_T_ARRAY((VALUE)(ary))), \ - assert(should_not_be_shared_and_embedded((VALUE)ary)), \ - FL_TEST_RAW((ary), RARRAY_EMBED_FLAG) != 0) - -#define ARY_HEAP_PTR(a) (assert(!ARY_EMBED_P(a)), RARRAY(a)->as.heap.ptr) -#define ARY_HEAP_LEN(a) (assert(!ARY_EMBED_P(a)), RARRAY(a)->as.heap.len) -#define ARY_HEAP_CAPA(a) (assert(!ARY_EMBED_P(a)), assert(!ARY_SHARED_ROOT_P(a)), \ +#define ARY_HEAP_PTR(a) (RUBY_ASSERT(!ARY_EMBED_P(a)), RARRAY(a)->as.heap.ptr) +#define ARY_HEAP_LEN(a) (RUBY_ASSERT(!ARY_EMBED_P(a)), RARRAY(a)->as.heap.len) +#define ARY_HEAP_CAPA(a) (RUBY_ASSERT(!ARY_EMBED_P(a)), RUBY_ASSERT(!ARY_SHARED_ROOT_P(a)), \ RARRAY(a)->as.heap.aux.capa) -#define ARY_EMBED_PTR(a) (assert(ARY_EMBED_P(a)), RARRAY(a)->as.ary) +#define ARY_EMBED_PTR(a) (RUBY_ASSERT(ARY_EMBED_P(a)), RARRAY(a)->as.ary) #define ARY_EMBED_LEN(a) \ - (assert(ARY_EMBED_P(a)), \ + (RUBY_ASSERT(ARY_EMBED_P(a)), \ (long)((RBASIC(a)->flags >> RARRAY_EMBED_LEN_SHIFT) & \ - (RARRAY_EMBED_LEN_MASK >> RARRAY_EMBED_LEN_SHIFT))) -#define ARY_HEAP_SIZE(a) (assert(!ARY_EMBED_P(a)), assert(ARY_OWNS_HEAP_P(a)), ARY_CAPA(a) * sizeof(VALUE)) + (RARRAY_EMBED_LEN_MASK >> RARRAY_EMBED_LEN_SHIFT))) +#define ARY_HEAP_SIZE(a) (RUBY_ASSERT(!ARY_EMBED_P(a)), RUBY_ASSERT(ARY_OWNS_HEAP_P(a)), ARY_CAPA(a) * sizeof(VALUE)) -#define ARY_OWNS_HEAP_P(a) (assert(should_be_T_ARRAY((VALUE)(a))), \ - !FL_TEST_RAW((a), ELTS_SHARED|RARRAY_EMBED_FLAG)) +#define ARY_OWNS_HEAP_P(a) (RUBY_ASSERT(should_be_T_ARRAY((VALUE)(a))), \ + !FL_TEST_RAW((a), RARRAY_SHARED_FLAG|RARRAY_EMBED_FLAG)) #define FL_SET_EMBED(a) do { \ - assert(!ARY_SHARED_P(a)); \ + RUBY_ASSERT(!ARY_SHARED_P(a)); \ FL_SET((a), RARRAY_EMBED_FLAG); \ - RARY_TRANSIENT_UNSET(a); \ ary_verify(a); \ } while (0) #define FL_UNSET_EMBED(ary) FL_UNSET((ary), RARRAY_EMBED_FLAG|RARRAY_EMBED_LEN_MASK) #define FL_SET_SHARED(ary) do { \ - assert(!ARY_EMBED_P(ary)); \ - FL_SET((ary), ELTS_SHARED); \ + RUBY_ASSERT(!ARY_EMBED_P(ary)); \ + FL_SET((ary), RARRAY_SHARED_FLAG); \ } while (0) -#define FL_UNSET_SHARED(ary) FL_UNSET((ary), ELTS_SHARED) +#define FL_UNSET_SHARED(ary) FL_UNSET((ary), RARRAY_SHARED_FLAG) +#define ARY_SET_PTR_FORCE(ary, p) \ + (RARRAY(ary)->as.heap.ptr = (p)) #define ARY_SET_PTR(ary, p) do { \ - assert(!ARY_EMBED_P(ary)); \ - assert(!OBJ_FROZEN(ary)); \ - RARRAY(ary)->as.heap.ptr = (p); \ + RUBY_ASSERT(!ARY_EMBED_P(ary)); \ + RUBY_ASSERT(!OBJ_FROZEN(ary)); \ + ARY_SET_PTR_FORCE(ary, p); \ } while (0) #define ARY_SET_EMBED_LEN(ary, n) do { \ long tmp_n = (n); \ - assert(ARY_EMBED_P(ary)); \ - assert(!OBJ_FROZEN(ary)); \ + RUBY_ASSERT(ARY_EMBED_P(ary)); \ RBASIC(ary)->flags &= ~RARRAY_EMBED_LEN_MASK; \ RBASIC(ary)->flags |= (tmp_n) << RARRAY_EMBED_LEN_SHIFT; \ } while (0) #define ARY_SET_HEAP_LEN(ary, n) do { \ - assert(!ARY_EMBED_P(ary)); \ + RUBY_ASSERT(!ARY_EMBED_P(ary)); \ RARRAY(ary)->as.heap.len = (n); \ } while (0) #define ARY_SET_LEN(ary, n) do { \ @@ -122,15 +132,15 @@ should_not_be_shared_and_embedded(VALUE ary) else { \ ARY_SET_HEAP_LEN((ary), (n)); \ } \ - assert(RARRAY_LEN(ary) == (n)); \ + RUBY_ASSERT(RARRAY_LEN(ary) == (n)); \ } while (0) #define ARY_INCREASE_PTR(ary, n) do { \ - assert(!ARY_EMBED_P(ary)); \ - assert(!OBJ_FROZEN(ary)); \ + RUBY_ASSERT(!ARY_EMBED_P(ary)); \ + RUBY_ASSERT(!OBJ_FROZEN(ary)); \ RARRAY(ary)->as.heap.ptr += (n); \ } while (0) #define ARY_INCREASE_LEN(ary, n) do { \ - assert(!OBJ_FROZEN(ary)); \ + RUBY_ASSERT(!OBJ_FROZEN(ary)); \ if (ARY_EMBED_P(ary)) { \ ARY_SET_EMBED_LEN((ary), RARRAY_LEN(ary)+(n)); \ } \ @@ -139,50 +149,93 @@ should_not_be_shared_and_embedded(VALUE ary) } \ } while (0) -#define ARY_CAPA(ary) (ARY_EMBED_P(ary) ? RARRAY_EMBED_LEN_MAX : \ +#define ARY_CAPA(ary) (ARY_EMBED_P(ary) ? ary_embed_capa(ary) : \ ARY_SHARED_ROOT_P(ary) ? RARRAY_LEN(ary) : ARY_HEAP_CAPA(ary)) +#define ARY_SET_CAPA_FORCE(ary, n) \ + RARRAY(ary)->as.heap.aux.capa = (n); #define ARY_SET_CAPA(ary, n) do { \ - assert(!ARY_EMBED_P(ary)); \ - assert(!ARY_SHARED_P(ary)); \ - assert(!OBJ_FROZEN(ary)); \ - RARRAY(ary)->as.heap.aux.capa = (n); \ + RUBY_ASSERT(!ARY_EMBED_P(ary)); \ + RUBY_ASSERT(!ARY_SHARED_P(ary)); \ + RUBY_ASSERT(!OBJ_FROZEN(ary)); \ + ARY_SET_CAPA_FORCE(ary, n); \ } while (0) -#define ARY_SHARED_ROOT(ary) (assert(ARY_SHARED_P(ary)), RARRAY(ary)->as.heap.aux.shared_root) -#define ARY_SET_SHARED(ary, value) do { \ - const VALUE _ary_ = (ary); \ - const VALUE _value_ = (value); \ - assert(!ARY_EMBED_P(_ary_)); \ - assert(ARY_SHARED_P(_ary_)); \ - assert(ARY_SHARED_ROOT_P(_value_)); \ - RB_OBJ_WRITE(_ary_, &RARRAY(_ary_)->as.heap.aux.shared_root, _value_); \ -} while (0) -#define RARRAY_SHARED_ROOT_FLAG FL_USER5 -#define ARY_SHARED_ROOT_P(ary) (assert(should_be_T_ARRAY((VALUE)(ary))), \ - FL_TEST_RAW((ary), RARRAY_SHARED_ROOT_FLAG)) -#define ARY_SHARED_ROOT_REFCNT(ary) \ - (assert(ARY_SHARED_ROOT_P(ary)), RARRAY(ary)->as.heap.aux.capa) -#define ARY_SHARED_ROOT_OCCUPIED(ary) (ARY_SHARED_ROOT_REFCNT(ary) == 1) +#define ARY_SHARED_ROOT_OCCUPIED(ary) (!OBJ_FROZEN(ary) && ARY_SHARED_ROOT_REFCNT(ary) == 1) #define ARY_SET_SHARED_ROOT_REFCNT(ary, value) do { \ - assert(ARY_SHARED_ROOT_P(ary)); \ + RUBY_ASSERT(ARY_SHARED_ROOT_P(ary)); \ + RUBY_ASSERT(!OBJ_FROZEN(ary)); \ + RUBY_ASSERT((value) >= 0); \ RARRAY(ary)->as.heap.aux.capa = (value); \ } while (0) #define FL_SET_SHARED_ROOT(ary) do { \ - assert(!ARY_EMBED_P(ary)); \ - assert(!RARRAY_TRANSIENT_P(ary)); \ + RUBY_ASSERT(!OBJ_FROZEN(ary)); \ + RUBY_ASSERT(!ARY_EMBED_P(ary)); \ FL_SET((ary), RARRAY_SHARED_ROOT_FLAG); \ } while (0) static inline void ARY_SET(VALUE a, long i, VALUE v) { - assert(!ARY_SHARED_P(a)); - assert(!OBJ_FROZEN(a)); + RUBY_ASSERT(!ARY_SHARED_P(a)); + RUBY_ASSERT(!OBJ_FROZEN(a)); RARRAY_ASET(a, i, v); } #undef RARRAY_ASET +static long +ary_embed_capa(VALUE ary) +{ + size_t size = rb_gc_obj_slot_size(ary) - offsetof(struct RArray, as.ary); + RUBY_ASSERT(size % sizeof(VALUE) == 0); + return size / sizeof(VALUE); +} + +static size_t +ary_embed_size(long capa) +{ + size_t size = offsetof(struct RArray, as.ary) + (sizeof(VALUE) * capa); + if (size < sizeof(struct RArray)) size = sizeof(struct RArray); + return size; +} + +static bool +ary_embeddable_p(long capa) +{ + return rb_gc_size_allocatable_p(ary_embed_size(capa)); +} + +bool +rb_ary_embeddable_p(VALUE ary) +{ + /* An array cannot be turned embeddable when the array is: + * - Shared root: other objects may point to the buffer of this array + * so we cannot make it embedded. + * - Frozen: this array may also be a shared root without the shared root + * flag. + * - Shared: we don't want to re-embed an array that points to a shared + * root (to save memory). + */ + return !(ARY_SHARED_ROOT_P(ary) || OBJ_FROZEN(ary) || ARY_SHARED_P(ary)); +} + +size_t +rb_ary_size_as_embedded(VALUE ary) +{ + size_t real_size; + + if (ARY_EMBED_P(ary)) { + real_size = ary_embed_size(ARY_EMBED_LEN(ary)); + } + else if (rb_ary_embeddable_p(ary)) { + real_size = ary_embed_size(ARY_HEAP_CAPA(ary)); + } + else { + real_size = sizeof(struct RArray); + } + return real_size; +} + #if ARRAY_DEBUG #define ary_verify(ary) ary_verify_(ary, __FILE__, __LINE__) @@ -190,25 +243,23 @@ ARY_SET(VALUE a, long i, VALUE v) static VALUE ary_verify_(VALUE ary, const char *file, int line) { - assert(RB_TYPE_P(ary, T_ARRAY)); + RUBY_ASSERT(RB_TYPE_P(ary, T_ARRAY)); - if (FL_TEST(ary, ELTS_SHARED)) { - VALUE root = RARRAY(ary)->as.heap.aux.shared_root; + if (ARY_SHARED_P(ary)) { + VALUE root = ARY_SHARED_ROOT(ary); const VALUE *ptr = ARY_HEAP_PTR(ary); - const VALUE *root_ptr = RARRAY_CONST_PTR_TRANSIENT(root); + const VALUE *root_ptr = RARRAY_CONST_PTR(root); long len = ARY_HEAP_LEN(ary), root_len = RARRAY_LEN(root); - assert(FL_TEST(root, RARRAY_SHARED_ROOT_FLAG)); - assert(root_ptr <= ptr && ptr + len <= root_ptr + root_len); + RUBY_ASSERT(ARY_SHARED_ROOT_P(root) || OBJ_FROZEN(root)); + RUBY_ASSERT(root_ptr <= ptr && ptr + len <= root_ptr + root_len); ary_verify(root); } else if (ARY_EMBED_P(ary)) { - assert(!RARRAY_TRANSIENT_P(ary)); - assert(!ARY_SHARED_P(ary)); - assert(RARRAY_LEN(ary) <= RARRAY_EMBED_LEN_MAX); + RUBY_ASSERT(!ARY_SHARED_P(ary)); + RUBY_ASSERT(RARRAY_LEN(ary) <= ary_embed_capa(ary)); } else { -#if 1 - const VALUE *ptr = RARRAY_CONST_PTR_TRANSIENT(ary); + const VALUE *ptr = RARRAY_CONST_PTR(ary); long i, len = RARRAY_LEN(ary); volatile VALUE v; if (len > 1) len = 1; /* check only HEAD */ @@ -216,25 +267,10 @@ ary_verify_(VALUE ary, const char *file, int line) v = ptr[i]; /* access check */ } v = v; -#endif - } - -#if USE_TRANSIENT_HEAP - if (RARRAY_TRANSIENT_P(ary)) { - assert(rb_transient_heap_managed_ptr_p(RARRAY_CONST_PTR_TRANSIENT(ary))); } -#endif - - rb_transient_heap_verify(); return ary; } - -void -rb_ary_verify(VALUE ary) -{ - ary_verify(ary); -} #else #define ary_verify(ary) ((void)0) #endif @@ -245,7 +281,7 @@ rb_ary_ptr_use_start(VALUE ary) #if ARRAY_DEBUG FL_SET_RAW(ary, RARRAY_PTR_IN_USE_FLAG); #endif - return (VALUE *)RARRAY_CONST_PTR_TRANSIENT(ary); + return (VALUE *)RARRAY_CONST_PTR(ary); } void @@ -260,15 +296,15 @@ void rb_mem_clear(VALUE *mem, long size) { while (size--) { - *mem++ = Qnil; + *mem++ = Qnil; } } static void ary_mem_clear(VALUE ary, long beg, long size) { - RARRAY_PTR_USE_TRANSIENT(ary, ptr, { - rb_mem_clear(ptr + beg, size); + RARRAY_PTR_USE(ary, ptr, { + rb_mem_clear(ptr + beg, size); }); } @@ -276,33 +312,33 @@ static inline void memfill(register VALUE *mem, register long size, register VALUE val) { while (size--) { - *mem++ = val; + *mem++ = val; } } static void ary_memfill(VALUE ary, long beg, long size, VALUE val) { - RARRAY_PTR_USE_TRANSIENT(ary, ptr, { - memfill(ptr + beg, size, val); - RB_OBJ_WRITTEN(ary, Qundef, val); + RARRAY_PTR_USE(ary, ptr, { + memfill(ptr + beg, size, val); + RB_OBJ_WRITTEN(ary, Qundef, val); }); } static void ary_memcpy0(VALUE ary, long beg, long argc, const VALUE *argv, VALUE buff_owner_ary) { - assert(!ARY_SHARED_P(buff_owner_ary)); + RUBY_ASSERT(!ARY_SHARED_P(buff_owner_ary)); if (argc > (int)(128/sizeof(VALUE)) /* is magic number (cache line size) */) { rb_gc_writebarrier_remember(buff_owner_ary); - RARRAY_PTR_USE_TRANSIENT(ary, ptr, { + RARRAY_PTR_USE(ary, ptr, { MEMCPY(ptr+beg, argv, VALUE, argc); }); } else { int i; - RARRAY_PTR_USE_TRANSIENT(ary, ptr, { + RARRAY_PTR_USE(ary, ptr, { for (i=0; i<argc; i++) { RB_OBJ_WRITE(buff_owner_ary, &ptr[i+beg], argv[i]); } @@ -317,140 +353,62 @@ ary_memcpy(VALUE ary, long beg, long argc, const VALUE *argv) } static VALUE * -ary_heap_alloc(VALUE ary, size_t capa) +ary_heap_alloc_buffer(size_t capa) { - VALUE *ptr = rb_transient_heap_alloc(ary, sizeof(VALUE) * capa); - - if (ptr != NULL) { - RARY_TRANSIENT_SET(ary); - } - else { - RARY_TRANSIENT_UNSET(ary); - ptr = ALLOC_N(VALUE, capa); - } - - return ptr; + return ALLOC_N(VALUE, capa); } static void ary_heap_free_ptr(VALUE ary, const VALUE *ptr, long size) { - if (RARRAY_TRANSIENT_P(ary)) { - /* ignore it */ - } - else { - ruby_sized_xfree((void *)ptr, size); - } + ruby_sized_xfree((void *)ptr, size); } static void ary_heap_free(VALUE ary) { - if (RARRAY_TRANSIENT_P(ary)) { - RARY_TRANSIENT_UNSET(ary); - } - else { - ary_heap_free_ptr(ary, ARY_HEAP_PTR(ary), ARY_HEAP_SIZE(ary)); - } + ary_heap_free_ptr(ary, ARY_HEAP_PTR(ary), ARY_HEAP_SIZE(ary)); } static size_t ary_heap_realloc(VALUE ary, size_t new_capa) { - size_t alloc_capa = new_capa; - size_t old_capa = ARY_HEAP_CAPA(ary); - - if (RARRAY_TRANSIENT_P(ary)) { - if (new_capa <= old_capa) { - /* do nothing */ - alloc_capa = old_capa; - } - else { - VALUE *new_ptr = rb_transient_heap_alloc(ary, sizeof(VALUE) * new_capa); - - if (new_ptr == NULL) { - new_ptr = ALLOC_N(VALUE, new_capa); - RARY_TRANSIENT_UNSET(ary); - } - - MEMCPY(new_ptr, ARY_HEAP_PTR(ary), VALUE, old_capa); - ARY_SET_PTR(ary, new_ptr); - } - } - else { - SIZED_REALLOC_N(RARRAY(ary)->as.heap.ptr, VALUE, new_capa, old_capa); - } + RUBY_ASSERT(!OBJ_FROZEN(ary)); + SIZED_REALLOC_N(RARRAY(ary)->as.heap.ptr, VALUE, new_capa, ARY_HEAP_CAPA(ary)); ary_verify(ary); - return alloc_capa; + return new_capa; } -#if USE_TRANSIENT_HEAP -static inline void -rb_ary_transient_heap_evacuate_(VALUE ary, int transient, int promote) +void +rb_ary_make_embedded(VALUE ary) { - if (transient) { - VALUE *new_ptr; - const VALUE *old_ptr = ARY_HEAP_PTR(ary); - long capa = ARY_HEAP_CAPA(ary); - long len = ARY_HEAP_LEN(ary); - - if (ARY_SHARED_ROOT_P(ary)) { - capa = len; - } + RUBY_ASSERT(rb_ary_embeddable_p(ary)); + if (!ARY_EMBED_P(ary)) { + const VALUE *buf = ARY_HEAP_PTR(ary); + long len = ARY_HEAP_LEN(ary); - assert(ARY_OWNS_HEAP_P(ary)); - assert(RARRAY_TRANSIENT_P(ary)); - assert(!ARY_PTR_USING_P(ary)); + FL_SET_EMBED(ary); + ARY_SET_EMBED_LEN(ary, len); - if (promote) { - new_ptr = ALLOC_N(VALUE, capa); - RARY_TRANSIENT_UNSET(ary); - } - else { - new_ptr = ary_heap_alloc(ary, capa); - } + MEMCPY((void *)ARY_EMBED_PTR(ary), (void *)buf, VALUE, len); - MEMCPY(new_ptr, old_ptr, VALUE, capa); - /* do not use ARY_SET_PTR() because they assert !frozen */ - RARRAY(ary)->as.heap.ptr = new_ptr; + ary_heap_free_ptr(ary, buf, len * sizeof(VALUE)); } - - ary_verify(ary); -} - -void -rb_ary_transient_heap_evacuate(VALUE ary, int promote) -{ - rb_ary_transient_heap_evacuate_(ary, RARRAY_TRANSIENT_P(ary), promote); -} - -void -rb_ary_detransient(VALUE ary) -{ - assert(RARRAY_TRANSIENT_P(ary)); - rb_ary_transient_heap_evacuate_(ary, TRUE, TRUE); } -#else -void -rb_ary_detransient(VALUE ary) -{ - /* do nothing */ -} -#endif static void ary_resize_capa(VALUE ary, long capacity) { - assert(RARRAY_LEN(ary) <= capacity); - assert(!OBJ_FROZEN(ary)); - assert(!ARY_SHARED_P(ary)); + RUBY_ASSERT(RARRAY_LEN(ary) <= capacity); + RUBY_ASSERT(!OBJ_FROZEN(ary)); + RUBY_ASSERT(!ARY_SHARED_P(ary)); - if (capacity > RARRAY_EMBED_LEN_MAX) { + if (capacity > ary_embed_capa(ary)) { size_t new_capa = capacity; if (ARY_EMBED_P(ary)) { long len = ARY_EMBED_LEN(ary); - VALUE *ptr = ary_heap_alloc(ary, capacity); + VALUE *ptr = ary_heap_alloc_buffer(capacity); MEMCPY(ptr, ARY_EMBED_PTR(ary), VALUE, len); FL_UNSET_EMBED(ary); @@ -485,9 +443,12 @@ ary_shrink_capa(VALUE ary) { long capacity = ARY_HEAP_LEN(ary); long old_capa = ARY_HEAP_CAPA(ary); - assert(!ARY_SHARED_P(ary)); - assert(old_capa >= capacity); - if (old_capa > capacity) ary_heap_realloc(ary, capacity); + RUBY_ASSERT(!ARY_SHARED_P(ary)); + RUBY_ASSERT(old_capa >= capacity); + if (old_capa > capacity) { + size_t new_capa = ary_heap_realloc(ary, capacity); + ARY_SET_CAPA(ary, new_capa); + } ary_verify(ary); } @@ -498,10 +459,10 @@ ary_double_capa(VALUE ary, long min) long new_capa = ARY_CAPA(ary) / 2; if (new_capa < ARY_DEFAULT_SIZE) { - new_capa = ARY_DEFAULT_SIZE; + new_capa = ARY_DEFAULT_SIZE; } if (new_capa >= ARY_MAX_SIZE - min) { - new_capa = (ARY_MAX_SIZE - min) / 2; + new_capa = (ARY_MAX_SIZE - min) / 2; } new_capa += min; ary_resize_capa(ary, new_capa); @@ -512,35 +473,40 @@ ary_double_capa(VALUE ary, long min) static void rb_ary_decrement_share(VALUE shared_root) { - if (shared_root) { - long num = ARY_SHARED_ROOT_REFCNT(shared_root) - 1; - if (num > 0) { - ARY_SET_SHARED_ROOT_REFCNT(shared_root, num); - } + if (!OBJ_FROZEN(shared_root)) { + long num = ARY_SHARED_ROOT_REFCNT(shared_root); + ARY_SET_SHARED_ROOT_REFCNT(shared_root, num - 1); } } static void rb_ary_unshare(VALUE ary) { - VALUE shared_root = RARRAY(ary)->as.heap.aux.shared_root; + VALUE shared_root = ARY_SHARED_ROOT(ary); rb_ary_decrement_share(shared_root); FL_UNSET_SHARED(ary); } -static inline void -rb_ary_unshare_safe(VALUE ary) +static void +rb_ary_reset(VALUE ary) { - if (ARY_SHARED_P(ary) && !ARY_EMBED_P(ary)) { - rb_ary_unshare(ary); + if (ARY_OWNS_HEAP_P(ary)) { + ary_heap_free(ary); + } + else if (ARY_SHARED_P(ary)) { + rb_ary_unshare(ary); } + + FL_SET_EMBED(ary); + ARY_SET_EMBED_LEN(ary, 0); } static VALUE rb_ary_increment_share(VALUE shared_root) { - long num = ARY_SHARED_ROOT_REFCNT(shared_root); - if (num >= 0) { + if (!OBJ_FROZEN(shared_root)) { + long num = ARY_SHARED_ROOT_REFCNT(shared_root); + RUBY_ASSERT(num >= 0); ARY_SET_SHARED_ROOT_REFCNT(shared_root, num + 1); } return shared_root; @@ -549,15 +515,22 @@ rb_ary_increment_share(VALUE shared_root) static void rb_ary_set_shared(VALUE ary, VALUE shared_root) { + RUBY_ASSERT(!ARY_EMBED_P(ary)); + RUBY_ASSERT(!OBJ_FROZEN(ary)); + RUBY_ASSERT(ARY_SHARED_ROOT_P(shared_root) || OBJ_FROZEN(shared_root)); + rb_ary_increment_share(shared_root); FL_SET_SHARED(ary); + RB_OBJ_WRITE(ary, &RARRAY(ary)->as.heap.aux.shared_root, shared_root); + RB_DEBUG_COUNTER_INC(obj_ary_shared_create); - ARY_SET_SHARED(ary, shared_root); } static inline void rb_ary_modify_check(VALUE ary) { + RUBY_ASSERT(ruby_thread_has_gvl_p()); + rb_check_frozen(ary); ary_verify(ary); } @@ -571,7 +544,7 @@ rb_ary_cancel_sharing(VALUE ary) ary_verify(shared_root); - if (len <= RARRAY_EMBED_LEN_MAX) { + if (len <= ary_embed_capa(ary)) { const VALUE *ptr = ARY_HEAP_PTR(ary); FL_UNSET_SHARED(ary); FL_SET_EMBED(ary); @@ -580,22 +553,22 @@ rb_ary_cancel_sharing(VALUE ary) ARY_SET_EMBED_LEN(ary, len); } else if (ARY_SHARED_ROOT_OCCUPIED(shared_root) && len > ((shared_len = RARRAY_LEN(shared_root))>>1)) { - long shift = RARRAY_CONST_PTR_TRANSIENT(ary) - RARRAY_CONST_PTR_TRANSIENT(shared_root); + long shift = RARRAY_CONST_PTR(ary) - RARRAY_CONST_PTR(shared_root); FL_UNSET_SHARED(ary); - ARY_SET_PTR(ary, RARRAY_CONST_PTR_TRANSIENT(shared_root)); + ARY_SET_PTR(ary, RARRAY_CONST_PTR(shared_root)); ARY_SET_CAPA(ary, shared_len); - RARRAY_PTR_USE_TRANSIENT(ary, ptr, { + RARRAY_PTR_USE(ary, ptr, { MEMMOVE(ptr, ptr+shift, VALUE, len); }); FL_SET_EMBED(shared_root); rb_ary_decrement_share(shared_root); } else { - VALUE *ptr = ary_heap_alloc(ary, len); + VALUE *ptr = ary_heap_alloc_buffer(len); MEMCPY(ptr, ARY_HEAP_PTR(ary), VALUE, len); rb_ary_unshare(ary); - ARY_SET_CAPA(ary, len); - ARY_SET_PTR(ary, ptr); + ARY_SET_CAPA_FORCE(ary, len); + ARY_SET_PTR_FORCE(ary, ptr); } rb_gc_writebarrier_remember(ary); @@ -618,40 +591,40 @@ ary_ensure_room_for_push(VALUE ary, long add_len) long capa; if (old_len > ARY_MAX_SIZE - add_len) { - rb_raise(rb_eIndexError, "index %ld too big", new_len); + rb_raise(rb_eIndexError, "index %ld too big", new_len); } if (ARY_SHARED_P(ary)) { - if (new_len > RARRAY_EMBED_LEN_MAX) { + if (new_len > ary_embed_capa(ary)) { VALUE shared_root = ARY_SHARED_ROOT(ary); if (ARY_SHARED_ROOT_OCCUPIED(shared_root)) { - if (ARY_HEAP_PTR(ary) - RARRAY_CONST_PTR_TRANSIENT(shared_root) + new_len <= RARRAY_LEN(shared_root)) { - rb_ary_modify_check(ary); + if (ARY_HEAP_PTR(ary) - RARRAY_CONST_PTR(shared_root) + new_len <= RARRAY_LEN(shared_root)) { + rb_ary_modify_check(ary); ary_verify(ary); ary_verify(shared_root); return shared_root; - } - else { - /* if array is shared, then it is likely it participate in push/shift pattern */ - rb_ary_modify(ary); - capa = ARY_CAPA(ary); - if (new_len > capa - (capa >> 6)) { - ary_double_capa(ary, new_len); - } + } + else { + /* if array is shared, then it is likely it participate in push/shift pattern */ + rb_ary_modify(ary); + capa = ARY_CAPA(ary); + if (new_len > capa - (capa >> 6)) { + ary_double_capa(ary, new_len); + } ary_verify(ary); - return ary; - } - } - } + return ary; + } + } + } ary_verify(ary); rb_ary_modify(ary); } else { - rb_ary_modify_check(ary); + rb_ary_modify_check(ary); } capa = ARY_CAPA(ary); if (new_len > capa) { - ary_double_capa(ary, new_len); + ary_double_capa(ary, new_len); } ary_verify(ary); @@ -660,20 +633,32 @@ ary_ensure_room_for_push(VALUE ary, long add_len) /* * call-seq: - * array.freeze -> self + * freeze -> self + * + * Freezes +self+ (if not already frozen); returns +self+: * - * Freezes +self+; returns +self+: * a = [] * a.frozen? # => false * a.freeze * a.frozen? # => true * - * An attempt to modify a frozen \Array raises FrozenError. + * No further changes may be made to +self+; + * raises FrozenError if a change is attempted. + * + * Related: Kernel#frozen?. */ VALUE rb_ary_freeze(VALUE ary) { + RUBY_ASSERT(RB_TYPE_P(ary, T_ARRAY)); + + if (OBJ_FROZEN(ary)) return ary; + + if (!ARY_EMBED_P(ary) && !ARY_SHARED_P(ary) && !ARY_SHARED_ROOT_P(ary)) { + ary_shrink_capa(ary); + } + return rb_obj_freeze(ary); } @@ -688,18 +673,22 @@ VALUE rb_ary_shared_with_p(VALUE ary1, VALUE ary2) { if (!ARY_EMBED_P(ary1) && ARY_SHARED_P(ary1) && - !ARY_EMBED_P(ary2) && ARY_SHARED_P(ary2) && - RARRAY(ary1)->as.heap.aux.shared_root == RARRAY(ary2)->as.heap.aux.shared_root && - RARRAY(ary1)->as.heap.len == RARRAY(ary2)->as.heap.len) { - return Qtrue; + !ARY_EMBED_P(ary2) && ARY_SHARED_P(ary2) && + ARY_SHARED_ROOT(ary1) == ARY_SHARED_ROOT(ary2) && + ARY_HEAP_LEN(ary1) == ARY_HEAP_LEN(ary2)) { + return Qtrue; } return Qfalse; } static VALUE -ary_alloc(VALUE klass) +ary_alloc_embed(VALUE klass, long capa) { - NEWOBJ_OF(ary, struct RArray, klass, T_ARRAY | RARRAY_EMBED_FLAG | (RGENGC_WB_PROTECTED_ARRAY ? FL_WB_PROTECTED : 0)); + size_t size = ary_embed_size(capa); + RUBY_ASSERT(rb_gc_size_allocatable_p(size)); + NEWOBJ_OF(ary, struct RArray, klass, + T_ARRAY | RARRAY_EMBED_FLAG | (RGENGC_WB_PROTECTED_ARRAY ? FL_WB_PROTECTED : 0), + size, 0); /* Created array is: * FL_SET_EMBED((VALUE)ary); * ARY_SET_EMBED_LEN((VALUE)ary, 0); @@ -708,32 +697,51 @@ ary_alloc(VALUE klass) } static VALUE +ary_alloc_heap(VALUE klass) +{ + NEWOBJ_OF(ary, struct RArray, klass, + T_ARRAY | (RGENGC_WB_PROTECTED_ARRAY ? FL_WB_PROTECTED : 0), + sizeof(struct RArray), 0); + + ary->as.heap.len = 0; + ary->as.heap.aux.capa = 0; + ary->as.heap.ptr = NULL; + + return (VALUE)ary; +} + +static VALUE empty_ary_alloc(VALUE klass) { RUBY_DTRACE_CREATE_HOOK(ARRAY, 0); - return ary_alloc(klass); + return ary_alloc_embed(klass, 0); } static VALUE ary_new(VALUE klass, long capa) { - VALUE ary,*ptr; + RUBY_ASSERT(ruby_thread_has_gvl_p()); + + VALUE ary; if (capa < 0) { - rb_raise(rb_eArgError, "negative array size (or size too big)"); + rb_raise(rb_eArgError, "negative array size (or size too big)"); } if (capa > ARY_MAX_SIZE) { - rb_raise(rb_eArgError, "array size too big"); + rb_raise(rb_eArgError, "array size too big"); } RUBY_DTRACE_CREATE_HOOK(ARRAY, capa); - ary = ary_alloc(klass); - if (capa > RARRAY_EMBED_LEN_MAX) { - ptr = ary_heap_alloc(ary, capa); - FL_UNSET_EMBED(ary); - ARY_SET_PTR(ary, ptr); + if (ary_embeddable_p(capa)) { + ary = ary_alloc_embed(klass, capa); + } + else { + ary = ary_alloc_heap(klass); ARY_SET_CAPA(ary, capa); + RUBY_ASSERT(!ARY_EMBED_P(ary)); + + ARY_SET_PTR(ary, ary_heap_alloc_buffer(capa)); ARY_SET_HEAP_LEN(ary, 0); } @@ -749,7 +757,7 @@ rb_ary_new_capa(long capa) VALUE rb_ary_new(void) { - return rb_ary_new2(RARRAY_EMBED_LEN_MAX); + return rb_ary_new_capa(0); } VALUE @@ -763,7 +771,7 @@ VALUE va_start(ar, n); for (i=0; i<n; i++) { - ARY_SET(ary, i, va_arg(ar, VALUE)); + ARY_SET(ary, i, va_arg(ar, VALUE)); } va_end(ar); @@ -771,15 +779,15 @@ VALUE return ary; } -MJIT_FUNC_EXPORTED VALUE +VALUE rb_ary_tmp_new_from_values(VALUE klass, long n, const VALUE *elts) { VALUE ary; ary = ary_new(klass, n); if (n > 0 && elts) { - ary_memcpy(ary, 0, n, elts); - ARY_SET_LEN(ary, n); + ary_memcpy(ary, 0, n, elts); + ARY_SET_LEN(ary, n); } return ary; @@ -792,9 +800,13 @@ rb_ary_new_from_values(long n, const VALUE *elts) } static VALUE -ec_ary_alloc(rb_execution_context_t *ec, VALUE klass) +ec_ary_alloc_embed(rb_execution_context_t *ec, VALUE klass, long capa) { - RB_EC_NEWOBJ_OF(ec, ary, struct RArray, klass, T_ARRAY | RARRAY_EMBED_FLAG | (RGENGC_WB_PROTECTED_ARRAY ? FL_WB_PROTECTED : 0)); + size_t size = ary_embed_size(capa); + RUBY_ASSERT(rb_gc_size_allocatable_p(size)); + NEWOBJ_OF(ary, struct RArray, klass, + T_ARRAY | RARRAY_EMBED_FLAG | (RGENGC_WB_PROTECTED_ARRAY ? FL_WB_PROTECTED : 0), + size, ec); /* Created array is: * FL_SET_EMBED((VALUE)ary); * ARY_SET_EMBED_LEN((VALUE)ary, 0); @@ -803,26 +815,42 @@ ec_ary_alloc(rb_execution_context_t *ec, VALUE klass) } static VALUE +ec_ary_alloc_heap(rb_execution_context_t *ec, VALUE klass) +{ + NEWOBJ_OF(ary, struct RArray, klass, + T_ARRAY | (RGENGC_WB_PROTECTED_ARRAY ? FL_WB_PROTECTED : 0), + sizeof(struct RArray), ec); + + ary->as.heap.len = 0; + ary->as.heap.aux.capa = 0; + ary->as.heap.ptr = NULL; + + return (VALUE)ary; +} + +static VALUE ec_ary_new(rb_execution_context_t *ec, VALUE klass, long capa) { - VALUE ary,*ptr; + VALUE ary; if (capa < 0) { - rb_raise(rb_eArgError, "negative array size (or size too big)"); + rb_raise(rb_eArgError, "negative array size (or size too big)"); } if (capa > ARY_MAX_SIZE) { - rb_raise(rb_eArgError, "array size too big"); + rb_raise(rb_eArgError, "array size too big"); } RUBY_DTRACE_CREATE_HOOK(ARRAY, capa); - ary = ec_ary_alloc(ec, klass); - - if (capa > RARRAY_EMBED_LEN_MAX) { - ptr = ary_heap_alloc(ary, capa); - FL_UNSET_EMBED(ary); - ARY_SET_PTR(ary, ptr); + if (ary_embeddable_p(capa)) { + ary = ec_ary_alloc_embed(ec, klass, capa); + } + else { + ary = ec_ary_alloc_heap(ec, klass); ARY_SET_CAPA(ary, capa); + RUBY_ASSERT(!ARY_EMBED_P(ary)); + + ARY_SET_PTR(ary, ary_heap_alloc_buffer(capa)); ARY_SET_HEAP_LEN(ary, 0); } @@ -836,28 +864,26 @@ rb_ec_ary_new_from_values(rb_execution_context_t *ec, long n, const VALUE *elts) ary = ec_ary_new(ec, rb_cArray, n); if (n > 0 && elts) { - ary_memcpy(ary, 0, n, elts); - ARY_SET_LEN(ary, n); + ary_memcpy(ary, 0, n, elts); + ARY_SET_LEN(ary, n); } return ary; } VALUE -rb_ary_tmp_new(long capa) +rb_ary_hidden_new(long capa) { VALUE ary = ary_new(0, capa); - rb_ary_transient_heap_evacuate(ary, TRUE); return ary; } VALUE -rb_ary_tmp_new_fill(long capa) +rb_ary_hidden_new_fill(long capa) { - VALUE ary = ary_new(0, capa); + VALUE ary = rb_ary_hidden_new(capa); ary_memfill(ary, 0, capa, Qnil); ARY_SET_LEN(ary, capa); - rb_ary_transient_heap_evacuate(ary, TRUE); return ary; } @@ -871,13 +897,8 @@ rb_ary_free(VALUE ary) RB_DEBUG_COUNTER_INC(obj_ary_extracapa); } - if (RARRAY_TRANSIENT_P(ary)) { - RB_DEBUG_COUNTER_INC(obj_ary_transient); - } - else { - RB_DEBUG_COUNTER_INC(obj_ary_ptr); - ary_heap_free(ary); - } + RB_DEBUG_COUNTER_INC(obj_ary_ptr); + ary_heap_free(ary); } else { RB_DEBUG_COUNTER_INC(obj_ary_embed); @@ -891,68 +912,86 @@ rb_ary_free(VALUE ary) } } -RUBY_FUNC_EXPORTED size_t +static VALUE fake_ary_flags; + +static VALUE +init_fake_ary_flags(void) +{ + struct RArray fake_ary = {0}; + fake_ary.basic.flags = T_ARRAY; + VALUE ary = (VALUE)&fake_ary; + rb_ary_freeze(ary); + return fake_ary.basic.flags; +} + +VALUE +rb_setup_fake_ary(struct RArray *fake_ary, const VALUE *list, long len) +{ + fake_ary->basic.flags = fake_ary_flags; + RBASIC_CLEAR_CLASS((VALUE)fake_ary); + + // bypass frozen checks + fake_ary->as.heap.ptr = list; + fake_ary->as.heap.len = len; + fake_ary->as.heap.aux.capa = len; + return (VALUE)fake_ary; +} + +size_t rb_ary_memsize(VALUE ary) { if (ARY_OWNS_HEAP_P(ary)) { - return ARY_CAPA(ary) * sizeof(VALUE); + return ARY_CAPA(ary) * sizeof(VALUE); } else { - return 0; + return 0; } } -static inline void -ary_discard(VALUE ary) -{ - rb_ary_free(ary); - RBASIC(ary)->flags |= RARRAY_EMBED_FLAG; - RBASIC(ary)->flags &= ~(RARRAY_EMBED_LEN_MASK | RARRAY_TRANSIENT_FLAG); -} - static VALUE ary_make_shared(VALUE ary) { - assert(!ARY_EMBED_P(ary)); ary_verify(ary); if (ARY_SHARED_P(ary)) { return ARY_SHARED_ROOT(ary); } else if (ARY_SHARED_ROOT_P(ary)) { - return ary; + return ary; } else if (OBJ_FROZEN(ary)) { - rb_ary_transient_heap_evacuate(ary, TRUE); - ary_shrink_capa(ary); - FL_SET_SHARED_ROOT(ary); - ARY_SET_SHARED_ROOT_REFCNT(ary, 1); - return ary; + return ary; } else { - long capa = ARY_CAPA(ary), len = RARRAY_LEN(ary); - const VALUE *ptr; - NEWOBJ_OF(shared, struct RArray, 0, T_ARRAY | (RGENGC_WB_PROTECTED_ARRAY ? FL_WB_PROTECTED : 0)); - VALUE vshared = (VALUE)shared; - - rb_ary_transient_heap_evacuate(ary, TRUE); - ptr = ARY_HEAP_PTR(ary); - - FL_UNSET_EMBED(vshared); - ARY_SET_LEN(vshared, capa); - ARY_SET_PTR(vshared, ptr); - ary_mem_clear(vshared, len, capa - len); - FL_SET_SHARED_ROOT(vshared); - ARY_SET_SHARED_ROOT_REFCNT(vshared, 1); - FL_SET_SHARED(ary); - RB_DEBUG_COUNTER_INC(obj_ary_shared_create); - ARY_SET_SHARED(ary, vshared); - OBJ_FREEZE(vshared); - - ary_verify(vshared); + long capa = ARY_CAPA(ary); + long len = RARRAY_LEN(ary); + + /* Shared roots cannot be embedded because the reference count + * (refcnt) is stored in as.heap.aux.capa. */ + VALUE shared = ary_alloc_heap(0); + FL_SET_SHARED_ROOT(shared); + + if (ARY_EMBED_P(ary)) { + VALUE *ptr = ary_heap_alloc_buffer(capa); + ARY_SET_PTR(shared, ptr); + ary_memcpy(shared, 0, len, RARRAY_CONST_PTR(ary)); + + FL_UNSET_EMBED(ary); + ARY_SET_HEAP_LEN(ary, len); + ARY_SET_PTR(ary, ptr); + } + else { + ARY_SET_PTR(shared, RARRAY_CONST_PTR(ary)); + } + + ARY_SET_LEN(shared, capa); + ary_mem_clear(shared, len, capa - len); + rb_ary_set_shared(ary, shared); + + ary_verify(shared); ary_verify(ary); - return vshared; + return shared; } } @@ -961,9 +1000,11 @@ ary_make_substitution(VALUE ary) { long len = RARRAY_LEN(ary); - if (len <= RARRAY_EMBED_LEN_MAX) { - VALUE subst = rb_ary_new2(len); - ary_memcpy(subst, 0, len, RARRAY_CONST_PTR_TRANSIENT(ary)); + if (ary_embeddable_p(len)) { + VALUE subst = rb_ary_new_capa(len); + RUBY_ASSERT(ARY_EMBED_P(subst)); + + ary_memcpy(subst, 0, len, RARRAY_CONST_PTR(ary)); ARY_SET_EMBED_LEN(subst, len); return subst; } @@ -991,7 +1032,7 @@ rb_check_array_type(VALUE ary) return rb_check_convert_type_with_id(ary, T_ARRAY, "Array", idTo_ary); } -MJIT_FUNC_EXPORTED VALUE +VALUE rb_check_to_array(VALUE ary) { return rb_check_convert_type_with_id(ary, T_ARRAY, "Array", idTo_a); @@ -1007,14 +1048,18 @@ rb_to_array(VALUE ary) * call-seq: * Array.try_convert(object) -> object, new_array, or nil * - * If +object+ is an \Array object, returns +object+. + * Attempts to return an array, based on the given +object+. + * + * If +object+ is an array, returns +object+. * - * Otherwise if +object+ responds to <tt>:to_ary</tt>, - * calls <tt>object.to_ary</tt> and returns the result. + * Otherwise if +object+ responds to <tt>:to_ary</tt>. + * calls <tt>object.to_ary</tt>: + * if the return value is an array or +nil+, returns that value; + * if not, raises TypeError. * - * Returns +nil+ if +object+ does not respond to <tt>:to_ary</tt> + * Otherwise returns +nil+. * - * Raises an exception unless <tt>object.to_ary</tt> returns an \Array object. + * Related: see {Methods for Creating an Array}[rdoc-ref:Array@Methods+for+Creating+an+Array]. */ static VALUE @@ -1023,48 +1068,79 @@ rb_ary_s_try_convert(VALUE dummy, VALUE ary) return rb_check_array_type(ary); } +/* :nodoc: */ +static VALUE +rb_ary_s_new(int argc, VALUE *argv, VALUE klass) +{ + VALUE ary; + + if (klass == rb_cArray) { + long size = 0; + if (argc > 0 && FIXNUM_P(argv[0])) { + size = FIX2LONG(argv[0]); + if (size < 0) size = 0; + } + + ary = ary_new(klass, size); + + rb_obj_call_init_kw(ary, argc, argv, RB_PASS_CALLED_KEYWORDS); + } + else { + ary = rb_class_new_instance_pass_kw(argc, argv, klass); + } + + return ary; +} + /* * call-seq: * Array.new -> new_empty_array * Array.new(array) -> new_array - * Array.new(size) -> new_array - * Array.new(size, default_value) -> new_array - * Array.new(size) {|index| ... } -> new_array + * Array.new(size, default_value = nil) -> new_array + * Array.new(size = 0) {|index| ... } -> new_array * - * Returns a new \Array. + * Returns a new array. * - * With no block and no arguments, returns a new empty \Array object. + * With no block and no argument given, returns a new empty array: * - * With no block and a single \Array argument +array+, - * returns a new \Array formed from +array+: - * a = Array.new([:foo, 'bar', 2]) - * a.class # => Array - * a # => [:foo, "bar", 2] + * Array.new # => [] + * + * With no block and array argument given, returns a new array with the same elements: + * + * Array.new([:foo, 'bar', 2]) # => [:foo, "bar", 2] + * + * With no block and integer argument given, returns a new array containing + * that many instances of the given +default_value+: * - * With no block and a single \Integer argument +size+, - * returns a new \Array of the given size - * whose elements are all +nil+: - * a = Array.new(3) - * a # => [nil, nil, nil] - * - * With no block and arguments +size+ and +default_value+, - * returns an \Array of the given size; - * each element is that same +default_value+: - * a = Array.new(3, 'x') - * a # => ['x', 'x', 'x'] - * - * With a block and argument +size+, - * returns an \Array of the given size; - * the block is called with each successive integer +index+; - * the element for that +index+ is the return value from the block: - * a = Array.new(3) {|index| "Element #{index}" } - * a # => ["Element 0", "Element 1", "Element 2"] - * - * Raises ArgumentError if +size+ is negative. - * - * With a block and no argument, - * or a single argument +0+, - * ignores the block and returns a new empty \Array. + * Array.new(0) # => [] + * Array.new(3) # => [nil, nil, nil] + * Array.new(2, 3) # => [3, 3] + * + * With a block given, returns an array of the given +size+; + * calls the block with each +index+ in the range <tt>(0...size)</tt>; + * the element at that +index+ in the returned array is the blocks return value: + * + * Array.new(3) {|index| "Element #{index}" } # => ["Element 0", "Element 1", "Element 2"] + * + * A common pitfall for new Rubyists is providing an expression as +default_value+: + * + * array = Array.new(2, {}) + * array # => [{}, {}] + * array[0][:a] = 1 + * array # => [{a: 1}, {a: 1}], as array[0] and array[1] are same object + * + * If you want the elements of the array to be distinct, you should pass a block: + * + * array = Array.new(2) { {} } + * array # => [{}, {}] + * array[0][:a] = 1 + * array # => [{a: 1}, {}], as array[0] and array[1] are different objects + * + * Raises TypeError if the first argument is not either an array + * or an {integer-convertible object}[rdoc-ref:implicit_conversion.rdoc@Integer-Convertible+Objects]). + * Raises ArgumentError if the first argument is a negative integer. + * + * Related: see {Methods for Creating an Array}[rdoc-ref:Array@Methods+for+Creating+an+Array]. */ static VALUE @@ -1075,61 +1151,60 @@ rb_ary_initialize(int argc, VALUE *argv, VALUE ary) rb_ary_modify(ary); if (argc == 0) { - if (ARY_OWNS_HEAP_P(ary) && ARY_HEAP_PTR(ary) != NULL) { - ary_heap_free(ary); - } - rb_ary_unshare_safe(ary); - FL_SET_EMBED(ary); - ARY_SET_EMBED_LEN(ary, 0); - if (rb_block_given_p()) { - rb_warning("given block not used"); - } - return ary; + rb_ary_reset(ary); + RUBY_ASSERT(ARY_EMBED_P(ary)); + RUBY_ASSERT(ARY_EMBED_LEN(ary) == 0); + if (rb_block_given_p()) { + rb_warning("given block not used"); + } + return ary; } rb_scan_args(argc, argv, "02", &size, &val); if (argc == 1 && !FIXNUM_P(size)) { - val = rb_check_array_type(size); - if (!NIL_P(val)) { - rb_ary_replace(ary, val); - return ary; - } + val = rb_check_array_type(size); + if (!NIL_P(val)) { + rb_ary_replace(ary, val); + return ary; + } } len = NUM2LONG(size); /* NUM2LONG() may call size.to_int, ary can be frozen, modified, etc */ if (len < 0) { - rb_raise(rb_eArgError, "negative array size"); + rb_raise(rb_eArgError, "negative array size"); } if (len > ARY_MAX_SIZE) { - rb_raise(rb_eArgError, "array size too big"); + rb_raise(rb_eArgError, "array size too big"); } /* recheck after argument conversion */ rb_ary_modify(ary); ary_resize_capa(ary, len); if (rb_block_given_p()) { - long i; + long i; - if (argc == 2) { - rb_warn("block supersedes default value argument"); - } - for (i=0; i<len; i++) { - rb_ary_store(ary, i, rb_yield(LONG2NUM(i))); - ARY_SET_LEN(ary, i + 1); - } + if (argc == 2) { + rb_warn("block supersedes default value argument"); + } + for (i=0; i<len; i++) { + rb_ary_store(ary, i, rb_yield(LONG2NUM(i))); + ARY_SET_LEN(ary, i + 1); + } } else { - ary_memfill(ary, 0, len, val); - ARY_SET_LEN(ary, len); + ary_memfill(ary, 0, len, val); + ARY_SET_LEN(ary, len); } return ary; } /* - * Returns a new array populated with the given objects. + * Returns a new array, populated with the given objects: + * + * Array[1, 'a', /^A/] # => [1, "a", /^A/] + * Array[] # => [] + * Array.[](1, 'a', /^A/) # => [1, "a", /^A/] * - * Array.[]( 1, 'a', /^A/) # => [1, "a", /^A/] - * Array[ 1, 'a', /^A/ ] # => [1, "a", /^A/] - * [ 1, 'a', /^A/ ] # => [1, "a", /^A/] + * Related: see {Methods for Creating an Array}[rdoc-ref:Array@Methods+for+Creating+an+Array]. */ static VALUE @@ -1150,26 +1225,26 @@ rb_ary_store(VALUE ary, long idx, VALUE val) long len = RARRAY_LEN(ary); if (idx < 0) { - idx += len; - if (idx < 0) { - rb_raise(rb_eIndexError, "index %ld too small for array; minimum: %ld", - idx - len, -len); - } + idx += len; + if (idx < 0) { + rb_raise(rb_eIndexError, "index %ld too small for array; minimum: %ld", + idx - len, -len); + } } else if (idx >= ARY_MAX_SIZE) { - rb_raise(rb_eIndexError, "index %ld too big", idx); + rb_raise(rb_eIndexError, "index %ld too big", idx); } rb_ary_modify(ary); if (idx >= ARY_CAPA(ary)) { - ary_double_capa(ary, idx); + ary_double_capa(ary, idx); } if (idx > len) { - ary_mem_clear(ary, len, idx - len + 1); + ary_mem_clear(ary, len, idx - len + 1); } if (idx >= len) { - ARY_SET_LEN(ary, idx + 1); + ARY_SET_LEN(ary, idx + 1); } ARY_SET(ary, idx, val); } @@ -1177,22 +1252,26 @@ rb_ary_store(VALUE ary, long idx, VALUE val) static VALUE ary_make_partial(VALUE ary, VALUE klass, long offset, long len) { - assert(offset >= 0); - assert(len >= 0); - assert(offset+len <= RARRAY_LEN(ary)); + RUBY_ASSERT(offset >= 0); + RUBY_ASSERT(len >= 0); + RUBY_ASSERT(offset+len <= RARRAY_LEN(ary)); - if (len <= RARRAY_EMBED_LEN_MAX) { - VALUE result = ary_alloc(klass); - ary_memcpy(result, 0, len, RARRAY_CONST_PTR_TRANSIENT(ary) + offset); + VALUE result = ary_alloc_heap(klass); + size_t embed_capa = ary_embed_capa(result); + if ((size_t)len <= embed_capa) { + FL_SET_EMBED(result); + ary_memcpy(result, 0, len, RARRAY_CONST_PTR(ary) + offset); ARY_SET_EMBED_LEN(result, len); - return result; } else { - VALUE shared, result = ary_alloc(klass); + VALUE shared = ary_make_shared(ary); + + /* The ary_make_shared call may allocate, which can trigger a GC + * compaction. This can cause the array to be embedded because it has + * a length of 0. */ FL_UNSET_EMBED(result); - shared = ary_make_shared(ary); - ARY_SET_PTR(result, RARRAY_CONST_PTR_TRANSIENT(ary)); + ARY_SET_PTR(result, RARRAY_CONST_PTR(ary)); ARY_SET_LEN(result, RARRAY_LEN(ary)); rb_ary_set_shared(result, shared); @@ -1200,38 +1279,46 @@ ary_make_partial(VALUE ary, VALUE klass, long offset, long len) ARY_SET_LEN(result, len); ary_verify(shared); - ary_verify(result); - return result; } + + ary_verify(result); + return result; } static VALUE ary_make_partial_step(VALUE ary, VALUE klass, long offset, long len, long step) { - assert(offset >= 0); - assert(len >= 0); - assert(offset+len <= RARRAY_LEN(ary)); - assert(step != 0); + RUBY_ASSERT(offset >= 0); + RUBY_ASSERT(len >= 0); + RUBY_ASSERT(offset+len <= RARRAY_LEN(ary)); + RUBY_ASSERT(step != 0); - const VALUE *values = RARRAY_CONST_PTR_TRANSIENT(ary); const long orig_len = len; - if ((step > 0 && step >= len) || (step < 0 && (step < -len))) { + if (step > 0 && step >= len) { VALUE result = ary_new(klass, 1); VALUE *ptr = (VALUE *)ARY_EMBED_PTR(result); + const VALUE *values = RARRAY_CONST_PTR(ary); + RB_OBJ_WRITE(result, ptr, values[offset]); ARY_SET_EMBED_LEN(result, 1); return result; } + else if (step < 0 && step < -len) { + step = -len; + } long ustep = (step < 0) ? -step : step; - len = (len + ustep - 1) / ustep; + len = roomof(len, ustep); long i; long j = offset + ((step > 0) ? 0 : (orig_len - 1)); + VALUE result = ary_new(klass, len); - if (len <= RARRAY_EMBED_LEN_MAX) { + if (ARY_EMBED_P(result)) { VALUE *ptr = (VALUE *)ARY_EMBED_PTR(result); + const VALUE *values = RARRAY_CONST_PTR(ary); + for (i = 0; i < len; ++i) { RB_OBJ_WRITE(result, ptr+i, values[j]); j += step; @@ -1239,7 +1326,9 @@ ary_make_partial_step(VALUE ary, VALUE klass, long offset, long len, long step) ARY_SET_EMBED_LEN(result, len); } else { - RARRAY_PTR_USE_TRANSIENT(result, ptr, { + const VALUE *values = RARRAY_CONST_PTR(ary); + + RARRAY_PTR_USE(result, ptr, { for (i = 0; i < len; ++i) { RB_OBJ_WRITE(result, ptr+i, values[j]); j += step; @@ -1264,44 +1353,47 @@ enum ary_take_pos_flags }; static VALUE -ary_take_first_or_last(int argc, const VALUE *argv, VALUE ary, enum ary_take_pos_flags last) +ary_take_first_or_last_n(VALUE ary, long n, enum ary_take_pos_flags last) { - long n; - long len; + long len = RARRAY_LEN(ary); long offset = 0; - argc = rb_check_arity(argc, 0, 1); - /* the case optional argument is omitted should be handled in - * callers of this function. if another arity case is added, - * this arity check needs to rewrite. */ - RUBY_ASSERT_ALWAYS(argc == 1); - - n = NUM2LONG(argv[0]); - len = RARRAY_LEN(ary); if (n > len) { - n = len; + n = len; } else if (n < 0) { - rb_raise(rb_eArgError, "negative array size"); + rb_raise(rb_eArgError, "negative array size"); } if (last) { - offset = len - n; + offset = len - n; } return ary_make_partial(ary, rb_cArray, offset, n); } +static VALUE +ary_take_first_or_last(int argc, const VALUE *argv, VALUE ary, enum ary_take_pos_flags last) +{ + argc = rb_check_arity(argc, 0, 1); + /* the case optional argument is omitted should be handled in + * callers of this function. if another arity case is added, + * this arity check needs to rewrite. */ + RUBY_ASSERT_ALWAYS(argc == 1); + return ary_take_first_or_last_n(ary, NUM2LONG(argv[0]), last); +} + /* * call-seq: - * array << object -> self + * self << object -> self * - * Appends +object+ to +self+; returns +self+: - * a = [:foo, 'bar', 2] - * a << :baz # => [:foo, "bar", 2, :baz] + * Appends +object+ as the last element in +self+; returns +self+: * - * Appends +object+ as one element, even if it is another \Array: - * a = [:foo, 'bar', 2] - * a1 = a << [3, 4] - * a1 # => [:foo, "bar", 2, [3, 4]] + * [:foo, 'bar', 2] << :baz # => [:foo, "bar", 2, :baz] + * + * Appends +object+ as a single element, even if it is another array: + * + * [:foo, 'bar', 2] << [3, 4] # => [:foo, "bar", 2, [3, 4]] + * + * Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ VALUE @@ -1309,8 +1401,8 @@ rb_ary_push(VALUE ary, VALUE item) { long idx = RARRAY_LEN((ary_verify(ary), ary)); VALUE target_ary = ary_ensure_room_for_push(ary, 1); - RARRAY_PTR_USE_TRANSIENT(ary, ptr, { - RB_OBJ_WRITE(target_ary, &ptr[idx], item); + RARRAY_PTR_USE(ary, ptr, { + RB_OBJ_WRITE(target_ary, &ptr[idx], item); }); ARY_SET_LEN(ary, idx + 1); ary_verify(ary); @@ -1329,22 +1421,20 @@ rb_ary_cat(VALUE ary, const VALUE *argv, long len) /* * call-seq: - * array.push(*objects) -> self + * push(*objects) -> self + * append(*objects) -> self * - * Appends trailing elements. + * Appends each argument in +objects+ to +self+; returns +self+: * - * Appends each argument in +objects+ to +self+; returns +self+: - * a = [:foo, 'bar', 2] - * a.push(:baz, :bat) # => [:foo, "bar", 2, :baz, :bat] + * a = [:foo, 'bar', 2] # => [:foo, "bar", 2] + * a.push(:baz, :bat) # => [:foo, "bar", 2, :baz, :bat] * - * Appends each argument as one element, even if it is another \Array: - * a = [:foo, 'bar', 2] - * a1 = a.push([:baz, :bat], [:bam, :bad]) - * a1 # => [:foo, "bar", 2, [:baz, :bat], [:bam, :bad]] + * Appends each argument as a single element, even if it is another array: * - * Array#append is an alias for \Array#push. + * a = [:foo, 'bar', 2] # => [:foo, "bar", 2] + a.push([:baz, :bat], [:bam, :bad]) # => [:foo, "bar", 2, [:baz, :bat], [:bam, :bad]] * - * Related: #pop, #shift, #unshift. + * Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ static VALUE @@ -1361,43 +1451,47 @@ rb_ary_pop(VALUE ary) n = RARRAY_LEN(ary); if (n == 0) return Qnil; if (ARY_OWNS_HEAP_P(ary) && - n * 3 < ARY_CAPA(ary) && - ARY_CAPA(ary) > ARY_DEFAULT_SIZE) + n * 3 < ARY_CAPA(ary) && + ARY_CAPA(ary) > ARY_DEFAULT_SIZE) { - ary_resize_capa(ary, n * 2); + ary_resize_capa(ary, n * 2); } - --n; - ARY_SET_LEN(ary, n); + + VALUE obj = RARRAY_AREF(ary, n - 1); + + ARY_SET_LEN(ary, n - 1); ary_verify(ary); - return RARRAY_AREF(ary, n); + return obj; } /* * call-seq: - * array.pop -> object or nil - * array.pop(n) -> new_array + * pop -> object or nil + * pop(count) -> new_array + * + * Removes and returns trailing elements of +self+. * - * Removes and returns trailing elements. + * With no argument given, removes and returns the last element, if available; + * otherwise returns +nil+: * - * When no argument is given and +self+ is not empty, - * removes and returns the last element: * a = [:foo, 'bar', 2] - * a.pop # => 2 - * a # => [:foo, "bar"] + * a.pop # => 2 + * a # => [:foo, "bar"] + * [].pop # => nil * - * Returns +nil+ if the array is empty. + * With non-negative integer argument +count+ given, + * returns a new array containing the trailing +count+ elements of +self+, as available: * - * When a non-negative \Integer argument +n+ is given and is in range, - * removes and returns the last +n+ elements in a new \Array: * a = [:foo, 'bar', 2] * a.pop(2) # => ["bar", 2] + * a # => [:foo] * - * If +n+ is positive and out of range, - * removes and returns all elements: * a = [:foo, 'bar', 2] * a.pop(50) # => [:foo, "bar", 2] + * a # => [] * - * Related: #push, #shift, #unshift. + * Related: Array#push; + * see also {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ static VALUE @@ -1406,7 +1500,7 @@ rb_ary_pop_m(int argc, VALUE *argv, VALUE ary) VALUE result; if (argc == 0) { - return rb_ary_pop(ary); + return rb_ary_pop(ary); } rb_ary_modify_check(ary); @@ -1422,62 +1516,54 @@ rb_ary_shift(VALUE ary) VALUE top; long len = RARRAY_LEN(ary); - rb_ary_modify_check(ary); - if (len == 0) return Qnil; - top = RARRAY_AREF(ary, 0); - if (!ARY_SHARED_P(ary)) { - if (len < ARY_DEFAULT_SIZE) { - RARRAY_PTR_USE_TRANSIENT(ary, ptr, { - MEMMOVE(ptr, ptr+1, VALUE, len-1); - }); /* WB: no new reference */ - ARY_INCREASE_LEN(ary, -1); - ary_verify(ary); - return top; - } - assert(!ARY_EMBED_P(ary)); /* ARY_EMBED_LEN_MAX < ARY_DEFAULT_SIZE */ - - ARY_SET(ary, 0, Qnil); - ary_make_shared(ary); - } - else if (ARY_SHARED_ROOT_OCCUPIED(ARY_SHARED_ROOT(ary))) { - RARRAY_PTR_USE_TRANSIENT(ary, ptr, ptr[0] = Qnil); + if (len == 0) { + rb_ary_modify_check(ary); + return Qnil; } - ARY_INCREASE_PTR(ary, 1); /* shift ptr */ - ARY_INCREASE_LEN(ary, -1); - ary_verify(ary); + top = RARRAY_AREF(ary, 0); + + rb_ary_behead(ary, 1); return top; } /* * call-seq: - * array.shift -> object or nil - * array.shift(n) -> new_array + * shift -> object or nil + * shift(count) -> new_array or nil * - * Removes and returns leading elements. + * Removes and returns leading elements from +self+. * - * When no argument is given, removes and returns the first element: - * a = [:foo, 'bar', 2] - * a.shift # => :foo - * a # => ['bar', 2] + * With no argument, removes and returns one element, if available, + * or +nil+ otherwise: * - * Returns +nil+ if +self+ is empty. + * a = [0, 1, 2, 3] + * a.shift # => 0 + * a # => [1, 2, 3] + * [].shift # => nil * - * When positive \Integer argument +n+ is given, removes the first +n+ elements; - * returns those elements in a new \Array: - * a = [:foo, 'bar', 2] - * a.shift(2) # => [:foo, 'bar'] - * a # => [2] + * With non-negative numeric argument +count+ given, + * removes and returns the first +count+ elements: * - * If +n+ is as large as or larger than <tt>self.length</tt>, - * removes all elements; returns those elements in a new \Array: - * a = [:foo, 'bar', 2] - * a.shift(3) # => [:foo, 'bar', 2] + * a = [0, 1, 2, 3] + * a.shift(2) # => [0, 1] + * a # => [2, 3] + * a.shift(1.1) # => [2] + * a # => [3] + * a.shift(0) # => [] + * a # => [3] + * + * If +count+ is large, + * removes and returns all elements: + * + * a = [0, 1, 2, 3] + * a.shift(50) # => [0, 1, 2, 3] + * a # => [] * - * If +n+ is zero, returns a new empty \Array; +self+ is unmodified. + * If +self+ is empty, returns a new empty array. * - * Related: #push, #pop, #unshift. + * Related: see {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ static VALUE @@ -1487,7 +1573,7 @@ rb_ary_shift_m(int argc, VALUE *argv, VALUE ary) long n; if (argc == 0) { - return rb_ary_shift(ary); + return rb_ary_shift(ary); } rb_ary_modify_check(ary); @@ -1498,50 +1584,39 @@ rb_ary_shift_m(int argc, VALUE *argv, VALUE ary) return result; } -static VALUE -behead_shared(VALUE ary, long n) +VALUE +rb_ary_behead(VALUE ary, long n) { - assert(ARY_SHARED_P(ary)); + if (n <= 0) { + return ary; + } + rb_ary_modify_check(ary); - if (ARY_SHARED_ROOT_OCCUPIED(ARY_SHARED_ROOT(ary))) { + + if (!ARY_SHARED_P(ary)) { + if (ARY_EMBED_P(ary) || RARRAY_LEN(ary) < ARY_DEFAULT_SIZE) { + RARRAY_PTR_USE(ary, ptr, { + MEMMOVE(ptr, ptr + n, VALUE, RARRAY_LEN(ary) - n); + }); /* WB: no new reference */ + ARY_INCREASE_LEN(ary, -n); + ary_verify(ary); + return ary; + } + ary_mem_clear(ary, 0, n); + ary_make_shared(ary); } + else if (ARY_SHARED_ROOT_OCCUPIED(ARY_SHARED_ROOT(ary))) { + ary_mem_clear(ary, 0, n); + } + ARY_INCREASE_PTR(ary, n); ARY_INCREASE_LEN(ary, -n); ary_verify(ary); - return ary; -} -static VALUE -behead_transient(VALUE ary, long n) -{ - rb_ary_modify_check(ary); - RARRAY_PTR_USE_TRANSIENT(ary, ptr, { - MEMMOVE(ptr, ptr+n, VALUE, RARRAY_LEN(ary)-n); - }); /* WB: no new reference */ - ARY_INCREASE_LEN(ary, -n); - ary_verify(ary); return ary; } -MJIT_FUNC_EXPORTED VALUE -rb_ary_behead(VALUE ary, long n) -{ - if (n <= 0) { - return ary; - } - else if (ARY_SHARED_P(ary)) { - return behead_shared(ary, n); - } - else if (RARRAY_LEN(ary) >= ARY_DEFAULT_SIZE) { - ary_make_shared(ary); - return behead_shared(ary, n); - } - else { - return behead_transient(ary, n); - } -} - static VALUE make_room_for_unshift(VALUE ary, const VALUE *head, VALUE *sharedp, int argc, long capa, long len) { @@ -1553,7 +1628,7 @@ make_room_for_unshift(VALUE ary, const VALUE *head, VALUE *sharedp, int argc, lo head = sharedp + argc + room; } ARY_SET_PTR(ary, head - argc); - assert(ARY_SHARED_ROOT_OCCUPIED(ARY_SHARED_ROOT(ary))); + RUBY_ASSERT(ARY_SHARED_ROOT_OCCUPIED(ARY_SHARED_ROOT(ary))); ary_verify(ary); return ARY_SHARED_ROOT(ary); @@ -1570,28 +1645,28 @@ ary_modify_for_unshift(VALUE ary, int argc) rb_ary_modify(ary); capa = ARY_CAPA(ary); if (capa - (capa >> 6) <= new_len) { - ary_double_capa(ary, new_len); + ary_double_capa(ary, new_len); } /* use shared array for big "queues" */ - if (new_len > ARY_DEFAULT_SIZE * 4) { + if (new_len > ARY_DEFAULT_SIZE * 4 && !ARY_EMBED_P(ary)) { ary_verify(ary); /* make a room for unshifted items */ - capa = ARY_CAPA(ary); - ary_make_shared(ary); + capa = ARY_CAPA(ary); + ary_make_shared(ary); - head = sharedp = RARRAY_CONST_PTR_TRANSIENT(ary); + head = sharedp = RARRAY_CONST_PTR(ary); return make_room_for_unshift(ary, head, (void *)sharedp, argc, capa, len); } else { - /* sliding items */ - RARRAY_PTR_USE_TRANSIENT(ary, ptr, { - MEMMOVE(ptr + argc, ptr, VALUE, len); - }); + /* sliding items */ + RARRAY_PTR_USE(ary, ptr, { + MEMMOVE(ptr + argc, ptr, VALUE, len); + }); ary_verify(ary); - return ary; + return ary; } } @@ -1618,8 +1693,8 @@ ary_ensure_room_for_unshift(VALUE ary, int argc) return ary_modify_for_unshift(ary, argc); } else { - const VALUE * head = RARRAY_CONST_PTR_TRANSIENT(ary); - void *sharedp = (void *)RARRAY_CONST_PTR_TRANSIENT(shared_root); + const VALUE * head = RARRAY_CONST_PTR(ary); + void *sharedp = (void *)RARRAY_CONST_PTR(shared_root); rb_ary_modify_check(ary); return make_room_for_unshift(ary, head, sharedp, argc, capa, len); @@ -1629,26 +1704,27 @@ ary_ensure_room_for_unshift(VALUE ary, int argc) /* * call-seq: - * array.unshift(*objects) -> self + * unshift(*objects) -> self + * prepend(*objects) -> self * * Prepends the given +objects+ to +self+: + * * a = [:foo, 'bar', 2] * a.unshift(:bam, :bat) # => [:bam, :bat, :foo, "bar", 2] * - * Array#prepend is an alias for Array#unshift. - * - * Related: #push, #pop, #shift. + * Related: Array#shift; + * see also {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ -static VALUE +VALUE rb_ary_unshift_m(int argc, VALUE *argv, VALUE ary) { long len = RARRAY_LEN(ary); VALUE target_ary; if (argc == 0) { - rb_ary_modify_check(ary); - return ary; + rb_ary_modify_check(ary); + return ary; } target_ary = ary_ensure_room_for_unshift(ary, argc); @@ -1660,7 +1736,7 @@ rb_ary_unshift_m(int argc, VALUE *argv, VALUE ary) VALUE rb_ary_unshift(VALUE ary, VALUE item) { - return rb_ary_unshift_m(1,&item,ary); + return rb_ary_unshift_m(1, &item, ary); } /* faster version - use this if you don't need to treat negative offset */ @@ -1670,7 +1746,7 @@ rb_ary_elt(VALUE ary, long offset) long len = RARRAY_LEN(ary); if (len == 0) return Qnil; if (offset < 0 || len <= offset) { - return Qnil; + return Qnil; } return RARRAY_AREF(ary, offset); } @@ -1691,7 +1767,7 @@ rb_ary_subseq_step(VALUE ary, long beg, long len, long step) if (beg < 0 || len < 0) return Qnil; if (alen < len || alen < beg + len) { - len = alen - beg; + len = alen - beg; } klass = rb_cArray; if (len == 0) return ary_new(klass, 0); @@ -1713,83 +1789,106 @@ static VALUE rb_ary_aref2(VALUE ary, VALUE b, VALUE e); /* * call-seq: - * array[index] -> object or nil - * array[start, length] -> object or nil - * array[range] -> object or nil - * array[aseq] -> object or nil - * array.slice(index) -> object or nil - * array.slice(start, length) -> object or nil - * array.slice(range) -> object or nil - * array.slice(aseq) -> object or nil + * self[offset] -> object or nil + * self[offset, size] -> object or nil + * self[range] -> object or nil + * self[aseq] -> object or nil * * Returns elements from +self+; does not modify +self+. * - * When a single \Integer argument +index+ is given, returns the element at offset +index+: + * In brief: + * + * a = [:foo, 'bar', 2] + * + * # Single argument offset: returns one element. + * a[0] # => :foo # Zero-based index. + * a[-1] # => 2 # Negative index counts backwards from end. + * + * # Arguments offset and size: returns an array. + * a[1, 2] # => ["bar", 2] + * a[-2, 2] # => ["bar", 2] # Negative offset counts backwards from end. + * + * # Single argument range: returns an array. + * a[0..1] # => [:foo, "bar"] + * a[0..-2] # => [:foo, "bar"] # Negative range-begin counts backwards from end. + * a[-2..2] # => ["bar", 2] # Negative range-end counts backwards from end. + * + * When a single integer argument +offset+ is given, returns the element at offset +offset+: + * * a = [:foo, 'bar', 2] * a[0] # => :foo * a[2] # => 2 * a # => [:foo, "bar", 2] * - * If +index+ is negative, counts relative to the end of +self+: + * If +offset+ is negative, counts backwards from the end of +self+: + * * a = [:foo, 'bar', 2] * a[-1] # => 2 * a[-2] # => "bar" * * If +index+ is out of range, returns +nil+. * - * When two \Integer arguments +start+ and +length+ are given, - * returns a new \Array of size +length+ containing successive elements beginning at offset +start+: + * When two Integer arguments +offset+ and +size+ are given, + * returns a new array of size +size+ containing successive elements beginning at offset +offset+: + * * a = [:foo, 'bar', 2] * a[0, 2] # => [:foo, "bar"] * a[1, 2] # => ["bar", 2] * - * If <tt>start + length</tt> is greater than <tt>self.length</tt>, - * returns all elements from offset +start+ to the end: + * If <tt>offset + size</tt> is greater than <tt>self.size</tt>, + * returns all elements from offset +offset+ to the end: + * * a = [:foo, 'bar', 2] * a[0, 4] # => [:foo, "bar", 2] * a[1, 3] # => ["bar", 2] * a[2, 2] # => [2] * - * If <tt>start == self.size</tt> and <tt>length >= 0</tt>, - * returns a new empty \Array. + * If <tt>offset == self.size</tt> and <tt>size >= 0</tt>, + * returns a new empty array. * - * If +length+ is negative, returns +nil+. + * If +size+ is negative, returns +nil+. + * + * When a single Range argument +range+ is given, + * treats <tt>range.min</tt> as +offset+ above + * and <tt>range.size</tt> as +size+ above: * - * When a single \Range argument +range+ is given, - * treats <tt>range.min</tt> as +start+ above - * and <tt>range.size</tt> as +length+ above: * a = [:foo, 'bar', 2] * a[0..1] # => [:foo, "bar"] * a[1..2] # => ["bar", 2] * - * Special case: If <tt>range.start == a.size</tt>, returns a new empty \Array. + * Special case: If <tt>range.start == a.size</tt>, returns a new empty array. * * If <tt>range.end</tt> is negative, calculates the end index from the end: + * * a = [:foo, 'bar', 2] * a[0..-1] # => [:foo, "bar", 2] * a[0..-2] # => [:foo, "bar"] * a[0..-3] # => [:foo] * * If <tt>range.start</tt> is negative, calculates the start index from the end: + * * a = [:foo, 'bar', 2] * a[-1..2] # => [2] * a[-2..2] # => ["bar", 2] * a[-3..2] # => [:foo, "bar", 2] * * If <tt>range.start</tt> is larger than the array size, returns +nil+. + * * a = [:foo, 'bar', 2] * a[4..1] # => nil * a[4..0] # => nil * a[4..-1] # => nil * * When a single Enumerator::ArithmeticSequence argument +aseq+ is given, - * returns an Array of elements corresponding to the indexes produced by + * returns an array of elements corresponding to the indexes produced by * the sequence. + * * a = ['--', 'data1', '--', 'data2', '--', 'data3'] * a[(1..).step(2)] # => ["data1", "data2", "data3"] * * Unlike slicing with range, if the start or the end of the arithmetic sequence * is larger than array size, throws RangeError. + * * a = ['--', 'data1', '--', 'data2', '--', 'data3'] * a[(1..11).step(2)] * # RangeError (((1..11).step(2)) out of range) @@ -1798,11 +1897,12 @@ static VALUE rb_ary_aref2(VALUE ary, VALUE b, VALUE e); * * If given a single argument, and its type is not one of the listed, tries to * convert it to Integer, and raises if it is impossible: + * * a = [:foo, 'bar', 2] * # Raises TypeError (no implicit conversion of Symbol into Integer): * a[:foo] * - * Array#slice is an alias for Array#[]. + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ VALUE @@ -1810,7 +1910,7 @@ rb_ary_aref(int argc, const VALUE *argv, VALUE ary) { rb_check_arity(argc, 1, 2); if (argc == 2) { - return rb_ary_aref2(ary, argv[0], argv[1]); + return rb_ary_aref2(ary, argv[0], argv[1]); } return rb_ary_aref1(ary, argv[0]); } @@ -1821,19 +1921,19 @@ rb_ary_aref2(VALUE ary, VALUE b, VALUE e) long beg = NUM2LONG(b); long len = NUM2LONG(e); if (beg < 0) { - beg += RARRAY_LEN(ary); + beg += RARRAY_LEN(ary); } return rb_ary_subseq(ary, beg, len); } -MJIT_FUNC_EXPORTED VALUE +VALUE rb_ary_aref1(VALUE ary, VALUE arg) { long beg, len, step; /* special case - speeding up */ if (FIXNUM_P(arg)) { - return rb_ary_entry(ary, FIX2LONG(arg)); + return rb_ary_entry(ary, FIX2LONG(arg)); } /* check if idx is Range or ArithmeticSequence */ switch (rb_arithmetic_sequence_beg_len_step(arg, &beg, &len, &step, RARRAY_LEN(ary), 0)) { @@ -1850,12 +1950,26 @@ rb_ary_aref1(VALUE ary, VALUE arg) /* * call-seq: - * array.at(index) -> object + * at(index) -> object or nil + * + * Returns the element of +self+ specified by the given +index+ + * or +nil+ if there is no such element; + * +index+ must be an + * {integer-convertible object}[rdoc-ref:implicit_conversion.rdoc@Integer-Convertible+Objects]. + * + * For non-negative +index+, returns the element of +self+ at offset +index+: * - * Returns the element at \Integer offset +index+; does not modify +self+. * a = [:foo, 'bar', 2] - * a.at(0) # => :foo - * a.at(2) # => 2 + * a.at(0) # => :foo + * a.at(2) # => 2 + * a.at(2.0) # => 2 + * + * For negative +index+, counts backwards from the end of +self+: + * + * a.at(-2) # => "bar" + * + * Related: Array#[]; + * see also {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ VALUE @@ -1864,113 +1978,72 @@ rb_ary_at(VALUE ary, VALUE pos) return rb_ary_entry(ary, NUM2LONG(pos)); } -/* - * call-seq: - * array.first -> object or nil - * array.first(n) -> new_array - * - * Returns elements from +self+; does not modify +self+. - * - * When no argument is given, returns the first element: - * a = [:foo, 'bar', 2] - * a.first # => :foo - * a # => [:foo, "bar", 2] - * - * If +self+ is empty, returns +nil+. - * - * When non-negative \Integer argument +n+ is given, - * returns the first +n+ elements in a new \Array: - * a = [:foo, 'bar', 2] - * a.first(2) # => [:foo, "bar"] - * - * If <tt>n >= array.size</tt>, returns all elements: - * a = [:foo, 'bar', 2] - * a.first(50) # => [:foo, "bar", 2] - * - * If <tt>n == 0</tt> returns an new empty \Array: - * a = [:foo, 'bar', 2] - * a.first(0) # [] - * - * Related: #last. - */ +#if 0 static VALUE rb_ary_first(int argc, VALUE *argv, VALUE ary) { if (argc == 0) { - if (RARRAY_LEN(ary) == 0) return Qnil; - return RARRAY_AREF(ary, 0); + if (RARRAY_LEN(ary) == 0) return Qnil; + return RARRAY_AREF(ary, 0); } else { - return ary_take_first_or_last(argc, argv, ary, ARY_TAKE_FIRST); + return ary_take_first_or_last(argc, argv, ary, ARY_TAKE_FIRST); } } +#endif + +static VALUE +ary_first(VALUE self) +{ + return (RARRAY_LEN(self) == 0) ? Qnil : RARRAY_AREF(self, 0); +} -/* - * call-seq: - * array.last -> object or nil - * array.last(n) -> new_array - * - * Returns elements from +self+; +self+ is not modified. - * - * When no argument is given, returns the last element: - * a = [:foo, 'bar', 2] - * a.last # => 2 - * a # => [:foo, "bar", 2] - * - * If +self+ is empty, returns +nil+. - * - * When non-negative \Innteger argument +n+ is given, - * returns the last +n+ elements in a new \Array: - * a = [:foo, 'bar', 2] - * a.last(2) # => ["bar", 2] - * - * If <tt>n >= array.size</tt>, returns all elements: - * a = [:foo, 'bar', 2] - * a.last(50) # => [:foo, "bar", 2] - * - * If <tt>n == 0</tt>, returns an new empty \Array: - * a = [:foo, 'bar', 2] - * a.last(0) # [] - * - * Related: #first. - */ +static VALUE +ary_last(VALUE self) +{ + long len = RARRAY_LEN(self); + return (len == 0) ? Qnil : RARRAY_AREF(self, len-1); +} VALUE -rb_ary_last(int argc, const VALUE *argv, VALUE ary) +rb_ary_last(int argc, const VALUE *argv, VALUE ary) // used by parse.y { if (argc == 0) { - long len = RARRAY_LEN(ary); - if (len == 0) return Qnil; - return RARRAY_AREF(ary, len-1); + return ary_last(ary); } else { - return ary_take_first_or_last(argc, argv, ary, ARY_TAKE_LAST); + return ary_take_first_or_last(argc, argv, ary, ARY_TAKE_LAST); } } /* * call-seq: - * array.fetch(index) -> element - * array.fetch(index, default_value) -> element - * array.fetch(index) {|index| ... } -> element + * fetch(index) -> element + * fetch(index, default_value) -> element or default_value + * fetch(index) {|index| ... } -> element or block_return_value * - * Returns the element at offset +index+. + * Returns the element of +self+ at offset +index+ if +index+ is in range; +index+ must be an + * {integer-convertible object}[rdoc-ref:implicit_conversion.rdoc@Integer-Convertible+Objects]. * - * With the single \Integer argument +index+, + * With the single argument +index+ and no block, * returns the element at offset +index+: + * * a = [:foo, 'bar', 2] - * a.fetch(1) # => "bar" + * a.fetch(1) # => "bar" + * a.fetch(1.1) # => "bar" * * If +index+ is negative, counts from the end of the array: + * * a = [:foo, 'bar', 2] * a.fetch(-1) # => 2 * a.fetch(-2) # => "bar" * - * With arguments +index+ and +default_value+, - * returns the element at offset +index+ if index is in range, - * otherwise returns +default_value+: + * With arguments +index+ and +default_value+ (which may be any object) and no block, + * returns +default_value+ if +index+ is out-of-range: + * * a = [:foo, 'bar', 2] - * a.fetch(1, nil) # => "bar" + * a.fetch(1, nil) # => "bar" + * a.fetch(3, :foo) # => :foo * * With argument +index+ and a block, * returns the element at offset +index+ if index is in range @@ -1979,6 +2052,8 @@ rb_ary_last(int argc, const VALUE *argv, VALUE ary) * a = [:foo, 'bar', 2] * a.fetch(1) {|index| raise 'Cannot happen' } # => "bar" * a.fetch(50) {|index| "Value for #{index}" } # => "Value for 50" + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -1991,57 +2066,145 @@ rb_ary_fetch(int argc, VALUE *argv, VALUE ary) rb_scan_args(argc, argv, "11", &pos, &ifnone); block_given = rb_block_given_p(); if (block_given && argc == 2) { - rb_warn("block supersedes default value argument"); + rb_warn("block supersedes default value argument"); } idx = NUM2LONG(pos); if (idx < 0) { - idx += RARRAY_LEN(ary); + idx += RARRAY_LEN(ary); } if (idx < 0 || RARRAY_LEN(ary) <= idx) { - if (block_given) return rb_yield(pos); - if (argc == 1) { - rb_raise(rb_eIndexError, "index %ld outside of array bounds: %ld...%ld", - idx - (idx < 0 ? RARRAY_LEN(ary) : 0), -RARRAY_LEN(ary), RARRAY_LEN(ary)); - } - return ifnone; + if (block_given) return rb_yield(pos); + if (argc == 1) { + rb_raise(rb_eIndexError, "index %ld outside of array bounds: %ld...%ld", + idx - (idx < 0 ? RARRAY_LEN(ary) : 0), -RARRAY_LEN(ary), RARRAY_LEN(ary)); + } + return ifnone; } return RARRAY_AREF(ary, idx); } /* + * call-seq: + * find(if_none_proc = nil) {|element| ... } -> object or nil + * find(if_none_proc = nil) -> enumerator + * + * Returns the first element for which the block returns a truthy value. + * + * With a block given, calls the block with successive elements of the array; + * returns the first element for which the block returns a truthy value: + * + * [1, 3, 5].find {|element| element > 2} # => 3 + * + * If no such element is found, calls +if_none_proc+ and returns its return value. + * + * [1, 3, 5].find(proc {-1}) {|element| element > 12} # => -1 + * + * With no block given, returns an Enumerator. + * + */ + +static VALUE +rb_ary_find(int argc, VALUE *argv, VALUE ary) +{ + VALUE if_none; + long idx; + + RETURN_ENUMERATOR(ary, argc, argv); + if_none = rb_check_arity(argc, 0, 1) ? argv[0] : Qnil; + + for (idx = 0; idx < RARRAY_LEN(ary); idx++) { + VALUE elem = RARRAY_AREF(ary, idx); + if (RTEST(rb_yield(elem))) { + return elem; + } + } + + if (!NIL_P(if_none)) { + return rb_funcallv(if_none, idCall, 0, 0); + } + return Qnil; +} + +/* + * call-seq: + * rfind(if_none_proc = nil) {|element| ... } -> object or nil + * rfind(if_none_proc = nil) -> enumerator + * + * Returns the last element for which the block returns a truthy value. + * + * With a block given, calls the block with successive elements of the array in + * reverse order; returns the first element for which the block returns a truthy + * value: + * + * [1, 2, 3, 4, 5, 6].rfind {|element| element < 5} # => 4 + * + * If no such element is found, calls +if_none_proc+ and returns its return value. + * + * [1, 2, 3, 4].rfind(proc {0}) {|element| element < -2} # => 0 + * + * With no block given, returns an Enumerator. + * + */ + +static VALUE +rb_ary_rfind(int argc, VALUE *argv, VALUE ary) +{ + VALUE if_none; + long len, idx; + + RETURN_ENUMERATOR(ary, argc, argv); + if_none = rb_check_arity(argc, 0, 1) ? argv[0] : Qnil; + + idx = RARRAY_LEN(ary); + while (idx--) { + VALUE elem = RARRAY_AREF(ary, idx); + if (RTEST(rb_yield(elem))) { + return elem; + } + + len = RARRAY_LEN(ary); + idx = (idx >= len) ? len : idx; + } + + if (!NIL_P(if_none)) { + return rb_funcallv(if_none, idCall, 0, 0); + } + return Qnil; +} + +/* * call-seq: - * array.index(object) -> integer or nil - * array.index {|element| ... } -> integer or nil - * array.index -> new_enumerator + * find_index(object) -> integer or nil + * find_index {|element| ... } -> integer or nil + * find_index -> new_enumerator + * index(object) -> integer or nil + * index {|element| ... } -> integer or nil + * index -> new_enumerator * - * Returns the index of a specified element. + * Returns the zero-based integer index of a specified element, or +nil+. * - * When argument +object+ is given but no block, + * With only argument +object+ given, * returns the index of the first element +element+ * for which <tt>object == element</tt>: + * * a = [:foo, 'bar', 2, 'bar'] * a.index('bar') # => 1 * * Returns +nil+ if no such element found. * - * When both argument +object+ and a block are given, + * With only a block given, * calls the block with each successive element; * returns the index of the first element for which the block returns a truthy value: + * * a = [:foo, 'bar', 2, 'bar'] * a.index {|element| element == 'bar' } # => 1 * * Returns +nil+ if the block never returns a truthy value. * - * When neither an argument nor a block is given, returns a new Enumerator: - * a = [:foo, 'bar', 2] - * e = a.index - * e # => #<Enumerator: [:foo, "bar", 2]:index> - * e.each {|element| element == 'bar' } # => 1 - * - * Array#find_index is an alias for Array#index. + * With neither an argument nor a block given, returns a new Enumerator. * - * Related: #rindex. + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ static VALUE @@ -2051,56 +2214,53 @@ rb_ary_index(int argc, VALUE *argv, VALUE ary) long i; if (argc == 0) { - RETURN_ENUMERATOR(ary, 0, 0); - for (i=0; i<RARRAY_LEN(ary); i++) { - if (RTEST(rb_yield(RARRAY_AREF(ary, i)))) { - return LONG2NUM(i); - } - } - return Qnil; + RETURN_ENUMERATOR(ary, 0, 0); + for (i=0; i<RARRAY_LEN(ary); i++) { + if (RTEST(rb_yield(RARRAY_AREF(ary, i)))) { + return LONG2NUM(i); + } + } + return Qnil; } rb_check_arity(argc, 0, 1); val = argv[0]; if (rb_block_given_p()) - rb_warn("given block not used"); + rb_warn("given block not used"); for (i=0; i<RARRAY_LEN(ary); i++) { - VALUE e = RARRAY_AREF(ary, i); - if (rb_equal(e, val)) { - return LONG2NUM(i); - } + VALUE e = RARRAY_AREF(ary, i); + if (rb_equal(e, val)) { + return LONG2NUM(i); + } } return Qnil; } /* * call-seq: - * array.rindex(object) -> integer or nil - * array.rindex {|element| ... } -> integer or nil - * array.rindex -> new_enumerator + * rindex(object) -> integer or nil + * rindex {|element| ... } -> integer or nil + * rindex -> new_enumerator * * Returns the index of the last element for which <tt>object == element</tt>. * - * When argument +object+ is given but no block, returns the index of the last such element found: + * With argument +object+ given, returns the index of the last such element found: + * * a = [:foo, 'bar', 2, 'bar'] * a.rindex('bar') # => 3 * * Returns +nil+ if no such object found. * - * When a block is given but no argument, calls the block with each successive element; + * With a block given, calls the block with each successive element; * returns the index of the last element for which the block returns a truthy value: + * * a = [:foo, 'bar', 2, 'bar'] * a.rindex {|element| element == 'bar' } # => 3 * * Returns +nil+ if the block never returns a truthy value. * - * When neither an argument nor a block is given, returns a new \Enumerator: + * When neither an argument nor a block is given, returns a new Enumerator. * - * a = [:foo, 'bar', 2, 'bar'] - * e = a.rindex - * e # => #<Enumerator: [:foo, "bar", 2, "bar"]:rindex> - * e.each {|element| element == 'bar' } # => 3 - * - * Related: #index. + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ static VALUE @@ -2110,25 +2270,25 @@ rb_ary_rindex(int argc, VALUE *argv, VALUE ary) long i = RARRAY_LEN(ary), len; if (argc == 0) { - RETURN_ENUMERATOR(ary, 0, 0); - while (i--) { - if (RTEST(rb_yield(RARRAY_AREF(ary, i)))) - return LONG2NUM(i); - if (i > (len = RARRAY_LEN(ary))) { - i = len; - } - } - return Qnil; + RETURN_ENUMERATOR(ary, 0, 0); + while (i--) { + if (RTEST(rb_yield(RARRAY_AREF(ary, i)))) + return LONG2NUM(i); + if (i > (len = RARRAY_LEN(ary))) { + i = len; + } + } + return Qnil; } rb_check_arity(argc, 0, 1); val = argv[0]; if (rb_block_given_p()) - rb_warn("given block not used"); + rb_warn("given block not used"); while (i--) { - VALUE e = RARRAY_AREF(ary, i); - if (rb_equal(e, val)) { - return LONG2NUM(i); - } + VALUE e = RARRAY_AREF(ary, i); + if (rb_equal(e, val)) { + return LONG2NUM(i); + } if (i > RARRAY_LEN(ary)) { break; } @@ -2154,64 +2314,69 @@ rb_ary_splice(VALUE ary, long beg, long len, const VALUE *rptr, long rlen) if (len < 0) rb_raise(rb_eIndexError, "negative length (%ld)", len); olen = RARRAY_LEN(ary); if (beg < 0) { - beg += olen; - if (beg < 0) { - rb_raise(rb_eIndexError, "index %ld too small for array; minimum: %ld", - beg - olen, -olen); - } + beg += olen; + if (beg < 0) { + rb_raise(rb_eIndexError, "index %ld too small for array; minimum: %ld", + beg - olen, -olen); + } } if (olen < len || olen < beg + len) { - len = olen - beg; + len = olen - beg; } { - const VALUE *optr = RARRAY_CONST_PTR_TRANSIENT(ary); - rofs = (rptr >= optr && rptr < optr + olen) ? rptr - optr : -1; + const VALUE *optr = RARRAY_CONST_PTR(ary); + rofs = (rptr >= optr && rptr < optr + olen) ? rptr - optr : -1; } if (beg >= olen) { - VALUE target_ary; - if (beg > ARY_MAX_SIZE - rlen) { - rb_raise(rb_eIndexError, "index %ld too big", beg); - } - target_ary = ary_ensure_room_for_push(ary, rlen-len); /* len is 0 or negative */ - len = beg + rlen; - ary_mem_clear(ary, olen, beg - olen); - if (rlen > 0) { - if (rofs != -1) rptr = RARRAY_CONST_PTR_TRANSIENT(ary) + rofs; - ary_memcpy0(ary, beg, rlen, rptr, target_ary); - } - ARY_SET_LEN(ary, len); + VALUE target_ary; + if (beg > ARY_MAX_SIZE - rlen) { + rb_raise(rb_eIndexError, "index %ld too big", beg); + } + target_ary = ary_ensure_room_for_push(ary, rlen-len); /* len is 0 or negative */ + len = beg + rlen; + ary_mem_clear(ary, olen, beg - olen); + if (rlen > 0) { + if (rofs != -1) rptr = RARRAY_CONST_PTR(ary) + rofs; + ary_memcpy0(ary, beg, rlen, rptr, target_ary); + } + ARY_SET_LEN(ary, len); } else { - long alen; - - if (olen - len > ARY_MAX_SIZE - rlen) { - rb_raise(rb_eIndexError, "index %ld too big", olen + rlen - len); - } - rb_ary_modify(ary); - alen = olen + rlen - len; - if (alen >= ARY_CAPA(ary)) { - ary_double_capa(ary, alen); - } - - if (len != rlen) { - RARRAY_PTR_USE_TRANSIENT(ary, ptr, + long alen; + + if (olen - len > ARY_MAX_SIZE - rlen) { + rb_raise(rb_eIndexError, "index %ld too big", olen + rlen - len); + } + rb_ary_modify(ary); + alen = olen + rlen - len; + if (alen >= ARY_CAPA(ary)) { + ary_double_capa(ary, alen); + } + + if (len != rlen) { + RARRAY_PTR_USE(ary, ptr, MEMMOVE(ptr + beg + rlen, ptr + beg + len, VALUE, olen - (beg + len))); - ARY_SET_LEN(ary, alen); - } - if (rlen > 0) { - if (rofs != -1) rptr = RARRAY_CONST_PTR_TRANSIENT(ary) + rofs; - /* give up wb-protected ary */ - RB_OBJ_WB_UNPROTECT_FOR(ARRAY, ary); + ARY_SET_LEN(ary, alen); + } + if (rlen > 0) { + if (rofs == -1) { + rb_gc_writebarrier_remember(ary); + } + else { + /* In this case, we're copying from a region in this array, so + * we don't need to fire the write barrier. */ + rptr = RARRAY_CONST_PTR(ary) + rofs; + } /* do not use RARRAY_PTR() because it can causes GC. * ary can contain T_NONE object because it is not cleared. */ - RARRAY_PTR_USE_TRANSIENT(ary, ptr, + RARRAY_PTR_USE(ary, ptr, MEMMOVE(ptr + beg, rptr, VALUE, rlen)); - } + } } } @@ -2222,10 +2387,10 @@ rb_ary_set_len(VALUE ary, long len) rb_ary_modify_check(ary); if (ARY_SHARED_P(ary)) { - rb_raise(rb_eRuntimeError, "can't set length of shared "); + rb_raise(rb_eRuntimeError, "can't set length of shared "); } if (len > (capa = (long)ARY_CAPA(ary))) { - rb_bug("probable buffer overflow: %ld for %ld", len, capa); + rb_bug("probable buffer overflow: %ld for %ld", len, capa); } ARY_SET_LEN(ary, len); } @@ -2239,31 +2404,36 @@ rb_ary_resize(VALUE ary, long len) olen = RARRAY_LEN(ary); if (len == olen) return ary; if (len > ARY_MAX_SIZE) { - rb_raise(rb_eIndexError, "index %ld too big", len); + rb_raise(rb_eIndexError, "index %ld too big", len); } if (len > olen) { - if (len >= ARY_CAPA(ary)) { - ary_double_capa(ary, len); - } - ary_mem_clear(ary, olen, len - olen); - ARY_SET_LEN(ary, len); + if (len > ARY_CAPA(ary)) { + ary_double_capa(ary, len); + } + ary_mem_clear(ary, olen, len - olen); + ARY_SET_LEN(ary, len); } else if (ARY_EMBED_P(ary)) { ARY_SET_EMBED_LEN(ary, len); } - else if (len <= RARRAY_EMBED_LEN_MAX) { - VALUE tmp[RARRAY_EMBED_LEN_MAX]; - MEMCPY(tmp, ARY_HEAP_PTR(ary), VALUE, len); - ary_discard(ary); - MEMCPY((VALUE *)ARY_EMBED_PTR(ary), tmp, VALUE, len); /* WB: no new reference */ + else if (len <= ary_embed_capa(ary)) { + const VALUE *ptr = ARY_HEAP_PTR(ary); + long ptr_capa = ARY_HEAP_SIZE(ary); + bool is_malloc_ptr = !ARY_SHARED_P(ary); + + FL_SET_EMBED(ary); + + MEMCPY((VALUE *)ARY_EMBED_PTR(ary), ptr, VALUE, len); /* WB: no new reference */ ARY_SET_EMBED_LEN(ary, len); + + if (is_malloc_ptr) ruby_sized_xfree((void *)ptr, ptr_capa); } else { - if (olen > len + ARY_DEFAULT_SIZE) { + if (olen > len + ARY_DEFAULT_SIZE) { size_t new_capa = ary_heap_realloc(ary, len); ARY_SET_CAPA(ary, new_capa); - } - ARY_SET_HEAP_LEN(ary, len); + } + ARY_SET_HEAP_LEN(ary, len); } ary_verify(ary); return ary; @@ -2280,44 +2450,77 @@ static VALUE ary_aset_by_rb_ary_splice(VALUE ary, long beg, long len, VALUE val) { VALUE rpl = rb_ary_to_ary(val); - rb_ary_splice(ary, beg, len, RARRAY_CONST_PTR_TRANSIENT(rpl), RARRAY_LEN(rpl)); + rb_ary_splice(ary, beg, len, RARRAY_CONST_PTR(rpl), RARRAY_LEN(rpl)); RB_GC_GUARD(rpl); return val; } /* * call-seq: - * array[index] = object -> object - * array[start, length] = object -> object - * array[range] = object -> object - * - * Assigns elements in +self+; returns the given +object+. - * - * When \Integer argument +index+ is given, assigns +object+ to an element in +self+. + * self[index] = object -> object + * self[start, length] = object -> object + * self[range] = object -> object + * + * Assigns elements in +self+, based on the given +object+; returns +object+. + * + * In brief: + * + * a_orig = [:foo, 'bar', 2] + * + * # With argument index. + * a = a_orig.dup + * a[0] = 'foo' # => "foo" + * a # => ["foo", "bar", 2] + * a = a_orig.dup + * a[7] = 'foo' # => "foo" + * a # => [:foo, "bar", 2, nil, nil, nil, nil, "foo"] + * + * # With arguments start and length. + * a = a_orig.dup + * a[0, 2] = 'foo' # => "foo" + * a # => ["foo", 2] + * a = a_orig.dup + * a[6, 50] = 'foo' # => "foo" + * a # => [:foo, "bar", 2, nil, nil, nil, "foo"] + * + * # With argument range. + * a = a_orig.dup + * a[0..1] = 'foo' # => "foo" + * a # => ["foo", 2] + * a = a_orig.dup + * a[6..50] = 'foo' # => "foo" + * a # => [:foo, "bar", 2, nil, nil, nil, "foo"] + * + * When Integer argument +index+ is given, assigns +object+ to an element in +self+. * * If +index+ is non-negative, assigns +object+ the element at offset +index+: + * * a = [:foo, 'bar', 2] * a[0] = 'foo' # => "foo" * a # => ["foo", "bar", 2] * * If +index+ is greater than <tt>self.length</tt>, extends the array: + * * a = [:foo, 'bar', 2] * a[7] = 'foo' # => "foo" * a # => [:foo, "bar", 2, nil, nil, nil, nil, "foo"] * * If +index+ is negative, counts backwards from the end of the array: + * * a = [:foo, 'bar', 2] * a[-1] = 'two' # => "two" * a # => [:foo, "bar", "two"] * - * When \Integer arguments +start+ and +length+ are given and +object+ is not an \Array, + * When Integer arguments +start+ and +length+ are given and +object+ is not an array, * removes <tt>length - 1</tt> elements beginning at offset +start+, * and assigns +object+ at offset +start+: + * * a = [:foo, 'bar', 2] * a[0, 2] = 'foo' # => "foo" * a # => ["foo", 2] * * If +start+ is negative, counts backwards from the end of the array: + * * a = [:foo, 'bar', 2] * a[-2, 2] = 'foo' # => "foo" * a # => [:foo, "foo"] @@ -2325,47 +2528,56 @@ ary_aset_by_rb_ary_splice(VALUE ary, long beg, long len, VALUE val) * If +start+ is non-negative and outside the array (<tt> >= self.size</tt>), * extends the array with +nil+, assigns +object+ at offset +start+, * and ignores +length+: + * * a = [:foo, 'bar', 2] * a[6, 50] = 'foo' # => "foo" * a # => [:foo, "bar", 2, nil, nil, nil, "foo"] * * If +length+ is zero, shifts elements at and following offset +start+ * and assigns +object+ at offset +start+: + * * a = [:foo, 'bar', 2] * a[1, 0] = 'foo' # => "foo" * a # => [:foo, "foo", "bar", 2] * * If +length+ is too large for the existing array, does not extend the array: + * * a = [:foo, 'bar', 2] * a[1, 5] = 'foo' # => "foo" * a # => [:foo, "foo"] * - * When \Range argument +range+ is given and +object+ is an \Array, + * When Range argument +range+ is given and +object+ is not an array, * removes <tt>length - 1</tt> elements beginning at offset +start+, * and assigns +object+ at offset +start+: + * * a = [:foo, 'bar', 2] * a[0..1] = 'foo' # => "foo" * a # => ["foo", 2] * * if <tt>range.begin</tt> is negative, counts backwards from the end of the array: + * * a = [:foo, 'bar', 2] * a[-2..2] = 'foo' # => "foo" * a # => [:foo, "foo"] * * If the array length is less than <tt>range.begin</tt>, - * assigns +object+ at offset <tt>range.begin</tt>, and ignores +length+: + * extends the array with +nil+, assigns +object+ at offset <tt>range.begin</tt>, + * and ignores +length+: + * * a = [:foo, 'bar', 2] * a[6..50] = 'foo' # => "foo" * a # => [:foo, "bar", 2, nil, nil, nil, "foo"] * * If <tt>range.end</tt> is zero, shifts elements at and following offset +start+ * and assigns +object+ at offset +start+: + * * a = [:foo, 'bar', 2] * a[1..0] = 'foo' # => "foo" * a # => [:foo, "foo", "bar", 2] * * If <tt>range.end</tt> is negative, assigns +object+ at offset +start+, * retains <tt>range.end.abs -1</tt> elements past that, and removes those beyond: + * * a = [:foo, 'bar', 2] * a[1..-1] = 'foo' # => "foo" * a # => [:foo, "foo"] @@ -2379,9 +2591,12 @@ ary_aset_by_rb_ary_splice(VALUE ary, long beg, long len, VALUE val) * * If <tt>range.end</tt> is too large for the existing array, * replaces array elements, but does not extend the array with +nil+ values: + * * a = [:foo, 'bar', 2] * a[1..5] = 'foo' # => "foo" * a # => [:foo, "foo"] + * + * Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ static VALUE @@ -2392,16 +2607,16 @@ rb_ary_aset(int argc, VALUE *argv, VALUE ary) rb_check_arity(argc, 2, 3); rb_ary_modify_check(ary); if (argc == 3) { - beg = NUM2LONG(argv[0]); - len = NUM2LONG(argv[1]); + beg = NUM2LONG(argv[0]); + len = NUM2LONG(argv[1]); return ary_aset_by_rb_ary_splice(ary, beg, len, argv[2]); } if (FIXNUM_P(argv[0])) { - offset = FIX2LONG(argv[0]); + offset = FIX2LONG(argv[0]); return ary_aset_by_rb_ary_store(ary, offset, argv[1]); } if (rb_range_beg_len(argv[0], &beg, &len, RARRAY_LEN(ary), 1)) { - /* check if idx is Range */ + /* check if idx is Range */ return ary_aset_by_rb_ary_splice(ary, beg, len, argv[1]); } @@ -2411,33 +2626,38 @@ rb_ary_aset(int argc, VALUE *argv, VALUE ary) /* * call-seq: - * array.insert(index, *objects) -> self + * insert(index, *objects) -> self * - * Inserts given +objects+ before or after the element at \Integer index +offset+; + * Inserts the given +objects+ as elements of +self+; * returns +self+. * - * When +index+ is non-negative, inserts all given +objects+ - * before the element at offset +index+: - * a = [:foo, 'bar', 2] - * a.insert(1, :bat, :bam) # => [:foo, :bat, :bam, "bar", 2] + * When +index+ is non-negative, inserts +objects+ + * _before_ the element at offset +index+: + * + * a = ['a', 'b', 'c'] # => ["a", "b", "c"] + * a.insert(1, :x, :y, :z) # => ["a", :x, :y, :z, "b", "c"] * * Extends the array if +index+ is beyond the array (<tt>index >= self.size</tt>): - * a = [:foo, 'bar', 2] - * a.insert(5, :bat, :bam) - * a # => [:foo, "bar", 2, nil, nil, :bat, :bam] * - * Does nothing if no objects given: - * a = [:foo, 'bar', 2] - * a.insert(1) - * a.insert(50) - * a.insert(-50) - * a # => [:foo, "bar", 2] + * a = ['a', 'b', 'c'] # => ["a", "b", "c"] + * a.insert(5, :x, :y, :z) # => ["a", "b", "c", nil, nil, :x, :y, :z] * - * When +index+ is negative, inserts all given +objects+ - * _after_ the element at offset <tt>index+self.size</tt>: - * a = [:foo, 'bar', 2] - * a.insert(-2, :bat, :bam) - * a # => [:foo, "bar", :bat, :bam, 2] + * When +index+ is negative, inserts +objects+ + * _after_ the element at offset <tt>index + self.size</tt>: + * + * a = ['a', 'b', 'c'] # => ["a", "b", "c"] + * a.insert(-2, :x, :y, :z) # => ["a", "b", :x, :y, :z, "c"] + * + * With no +objects+ given, does nothing: + * + * a = ['a', 'b', 'c'] # => ["a", "b", "c"] + * a.insert(1) # => ["a", "b", "c"] + * a.insert(50) # => ["a", "b", "c"] + * a.insert(-50) # => ["a", "b", "c"] + * + * Raises IndexError if +objects+ are given and +index+ is negative and out of range. + * + * Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ static VALUE @@ -2450,15 +2670,15 @@ rb_ary_insert(int argc, VALUE *argv, VALUE ary) pos = NUM2LONG(argv[0]); if (argc == 1) return ary; if (pos == -1) { - pos = RARRAY_LEN(ary); + pos = RARRAY_LEN(ary); } else if (pos < 0) { - long minpos = -RARRAY_LEN(ary) - 1; - if (pos < minpos) { - rb_raise(rb_eIndexError, "index %ld too small for array; minimum: %ld", - pos, minpos); - } - pos++; + long minpos = -RARRAY_LEN(ary) - 1; + if (pos < minpos) { + rb_raise(rb_eIndexError, "index %ld too small for array; minimum: %ld", + pos, minpos); + } + pos++; } rb_ary_splice(ary, pos, 0, argv + 1, argc - 1); return ary; @@ -2473,43 +2693,51 @@ ary_enum_length(VALUE ary, VALUE args, VALUE eobj) return rb_ary_length(ary); } +// Primitive to avoid a race condition in Array#each. +// Return `true` and write `value` and `index` if the element exists. +static VALUE +ary_fetch_next(VALUE self, VALUE *index, VALUE *value) +{ + long i = NUM2LONG(*index); + if (i >= RARRAY_LEN(self)) { + return Qfalse; + } + *value = RARRAY_AREF(self, i); + *index = LONG2NUM(i + 1); + return Qtrue; +} + /* * call-seq: - * array.each {|element| ... } -> self - * array.each -> Enumerator - * - * Iterates over array elements. + * each {|element| ... } -> self + * each -> new_enumerator * - * When a block given, passes each successive array element to the block; + * With a block given, iterates over the elements of +self+, + * passing each element to the block; * returns +self+: + * * a = [:foo, 'bar', 2] * a.each {|element| puts "#{element.class} #{element}" } * * Output: + * * Symbol foo * String bar * Integer 2 * * Allows the array to be modified during iteration: + * * a = [:foo, 'bar', 2] * a.each {|element| puts element; a.clear if element.to_s.start_with?('b') } * * Output: + * * foo * bar * - * When no block given, returns a new \Enumerator: - * a = [:foo, 'bar', 2] - * e = a.each - * e # => #<Enumerator: [:foo, "bar", 2]:each> - * a1 = e.each {|element| puts "#{element.class} #{element}" } - * - * Output: - * Symbol foo - * String bar - * Integer 2 + * With no block given, returns a new Enumerator. * - * Related: #each_index, #reverse_each. + * Related: see {Methods for Iterating}[rdoc-ref:Array@Methods+for+Iterating]. */ VALUE @@ -2519,48 +2747,43 @@ rb_ary_each(VALUE ary) ary_verify(ary); RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length); for (i=0; i<RARRAY_LEN(ary); i++) { - rb_yield(RARRAY_AREF(ary, i)); + rb_yield(RARRAY_AREF(ary, i)); } return ary; } /* * call-seq: - * array.each_index {|index| ... } -> self - * array.each_index -> Enumerator + * each_index {|index| ... } -> self + * each_index -> new_enumerator * - * Iterates over array indexes. - * - * When a block given, passes each successive array index to the block; + * With a block given, iterates over the elements of +self+, + * passing each <i>array index</i> to the block; * returns +self+: + * * a = [:foo, 'bar', 2] * a.each_index {|index| puts "#{index} #{a[index]}" } * * Output: + * * 0 foo * 1 bar * 2 2 * * Allows the array to be modified during iteration: + * * a = [:foo, 'bar', 2] * a.each_index {|index| puts index; a.clear if index > 0 } + * a # => [] * * Output: + * * 0 * 1 * - * When no block given, returns a new \Enumerator: - * a = [:foo, 'bar', 2] - * e = a.each_index - * e # => #<Enumerator: [:foo, "bar", 2]:each_index> - * a1 = e.each {|index| puts "#{index} #{a[index]}"} - * - * Output: - * 0 foo - * 1 bar - * 2 2 + * With no block given, returns a new Enumerator. * - * Related: #each, #reverse_each. + * Related: see {Methods for Iterating}[rdoc-ref:Array@Methods+for+Iterating]. */ static VALUE @@ -2570,47 +2793,33 @@ rb_ary_each_index(VALUE ary) RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length); for (i=0; i<RARRAY_LEN(ary); i++) { - rb_yield(LONG2NUM(i)); + rb_yield(LONG2NUM(i)); } return ary; } /* * call-seq: - * array.reverse_each {|element| ... } -> self - * array.reverse_each -> Enumerator + * reverse_each {|element| ... } -> self + * reverse_each -> Enumerator * - * Iterates backwards over array elements. - * - * When a block given, passes, in reverse order, each element to the block; + * When a block given, iterates backwards over the elements of +self+, + * passing, in reverse order, each element to the block; * returns +self+: - * a = [:foo, 'bar', 2] - * a.reverse_each {|element| puts "#{element.class} #{element}" } * - * Output: - * Integer 2 - * String bar - * Symbol foo + * a = [] + * [0, 1, 2].reverse_each {|element| a.push(element) } + * a # => [2, 1, 0] * * Allows the array to be modified during iteration: - * a = [:foo, 'bar', 2] - * a.reverse_each {|element| puts element; a.clear if element.to_s.start_with?('b') } * - * Output: - * 2 - * bar + * a = ['a', 'b', 'c'] + * a.reverse_each {|element| a.clear if element.start_with?('b') } + * a # => [] * - * When no block given, returns a new \Enumerator: - * a = [:foo, 'bar', 2] - * e = a.reverse_each - * e # => #<Enumerator: [:foo, "bar", 2]:reverse_each> - * a1 = e.each {|element| puts "#{element.class} #{element}" } - * Output: - * Integer 2 - * String bar - * Symbol foo + * When no block given, returns a new Enumerator. * - * Related: #each, #each_index. + * Related: see {Methods for Iterating}[rdoc-ref:Array@Methods+for+Iterating]. */ static VALUE @@ -2621,21 +2830,27 @@ rb_ary_reverse_each(VALUE ary) RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length); len = RARRAY_LEN(ary); while (len--) { - long nlen; - rb_yield(RARRAY_AREF(ary, len)); - nlen = RARRAY_LEN(ary); - if (nlen < len) { - len = nlen; - } + long nlen; + rb_yield(RARRAY_AREF(ary, len)); + nlen = RARRAY_LEN(ary); + if (nlen < len) { + len = nlen; + } } return ary; } /* * call-seq: - * array.length -> an_integer + * length -> integer + * size -> integer + * + * Returns the count of elements in +self+: + * + * [0, 1, 2].length # => 3 + * [].length # => 0 * - * Returns the count of elements in +self+. + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ static VALUE @@ -2647,10 +2862,12 @@ rb_ary_length(VALUE ary) /* * call-seq: - * array.empty? -> true or false + * empty? -> true or false * * Returns +true+ if the count of elements in +self+ is zero, * +false+ otherwise. + * + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ static VALUE @@ -2664,7 +2881,7 @@ rb_ary_dup(VALUE ary) { long len = RARRAY_LEN(ary); VALUE dup = rb_ary_new2(len); - ary_memcpy(dup, 0, len, RARRAY_CONST_PTR_TRANSIENT(ary)); + ary_memcpy(dup, 0, len, RARRAY_CONST_PTR(ary)); ARY_SET_LEN(dup, len); ary_verify(ary); @@ -2692,10 +2909,10 @@ recursive_join(VALUE obj, VALUE argp, int recur) int *first = (int *)arg[3]; if (recur) { - rb_raise(rb_eArgError, "recursive array join"); + rb_raise(rb_eArgError, "recursive array join"); } else { - ary_join_1(obj, ary, sep, 0, result, first); + ary_join_1(obj, ary, sep, 0, result, first); } return Qnil; } @@ -2708,11 +2925,11 @@ ary_join_0(VALUE ary, VALUE sep, long max, VALUE result) if (max > 0) rb_enc_copy(result, RARRAY_AREF(ary, 0)); for (i=0; i<max; i++) { - val = RARRAY_AREF(ary, i); + val = RARRAY_AREF(ary, i); if (!RB_TYPE_P(val, T_STRING)) break; - if (i > 0 && !NIL_P(sep)) - rb_str_buf_append(result, sep); - rb_str_buf_append(result, val); + if (i > 0 && !NIL_P(sep)) + rb_str_buf_append(result, sep); + rb_str_buf_append(result, val); } return i; } @@ -2751,16 +2968,16 @@ ary_join_1(VALUE obj, VALUE ary, VALUE sep, long i, VALUE result, int *first) VALUE val, tmp; for (; i<RARRAY_LEN(ary); i++) { - if (i > 0 && !NIL_P(sep)) - rb_str_buf_append(result, sep); + if (i > 0 && !NIL_P(sep)) + rb_str_buf_append(result, sep); - val = RARRAY_AREF(ary, i); - if (RB_TYPE_P(val, T_STRING)) { + val = RARRAY_AREF(ary, i); + if (RB_TYPE_P(val, T_STRING)) { ary_join_1_str(result, val, first); - } - else if (RB_TYPE_P(val, T_ARRAY)) { + } + else if (RB_TYPE_P(val, T_ARRAY)) { ary_join_1_ary(val, ary, sep, result, val, first); - } + } else if (!NIL_P(tmp = rb_check_string_type(val))) { ary_join_1_str(result, tmp, first); } @@ -2769,7 +2986,7 @@ ary_join_1(VALUE obj, VALUE ary, VALUE sep, long i, VALUE result, int *first) } else { ary_join_1_str(result, rb_obj_as_string(val), first); - } + } } } @@ -2782,26 +2999,31 @@ rb_ary_join(VALUE ary, VALUE sep) if (RARRAY_LEN(ary) == 0) return rb_usascii_str_new(0, 0); if (!NIL_P(sep)) { - StringValue(sep); - len += RSTRING_LEN(sep) * (RARRAY_LEN(ary) - 1); - } - for (i=0; i<RARRAY_LEN(ary); i++) { - val = RARRAY_AREF(ary, i); - tmp = rb_check_string_type(val); - - if (NIL_P(tmp) || tmp != val) { - int first; - long n = RARRAY_LEN(ary); - if (i > n) i = n; - result = rb_str_buf_new(len + (n-i)*10); - rb_enc_associate(result, rb_usascii_encoding()); - i = ary_join_0(ary, sep, i, result); - first = i == 0; - ary_join_1(ary, ary, sep, i, result, &first); - return result; - } - - len += RSTRING_LEN(tmp); + StringValue(sep); + len += RSTRING_LEN(sep) * (RARRAY_LEN(ary) - 1); + } + long len_memo = RARRAY_LEN(ary); + for (i=0; i < len_memo; i++) { + val = RARRAY_AREF(ary, i); + if (RB_UNLIKELY(!RB_TYPE_P(val, T_STRING))) { + tmp = rb_check_string_type(val); + if (NIL_P(tmp) || tmp != val) { + int first; + long n = RARRAY_LEN(ary); + if (i > n) i = n; + result = rb_str_buf_new(len + (n-i)*10); + rb_enc_associate(result, rb_usascii_encoding()); + i = ary_join_0(ary, sep, i, result); + first = i == 0; + ary_join_1(ary, ary, sep, i, result, &first); + return result; + } + len += RSTRING_LEN(tmp); + len_memo = RARRAY_LEN(ary); + } + else { + len += RSTRING_LEN(val); + } } result = rb_str_new(0, len); @@ -2814,26 +3036,32 @@ rb_ary_join(VALUE ary, VALUE sep) /* * call-seq: - * array.join ->new_string - * array.join(separator = $,) -> new_string + * join(separator = $,) -> new_string + * + * Returns the new string formed by joining the converted elements of +self+; + * for each element +element+: + * + * - Converts recursively using <tt>element.join(separator)</tt> + * if +element+ is a <tt>kind_of?(Array)</tt>. + * - Otherwise, converts using <tt>element.to_s</tt>. * - * Returns the new \String formed by joining the array elements after conversion. - * For each element +element+ - * - Uses <tt>element.to_s</tt> if +element+ is not a <tt>kind_of?(Array)</tt>. - * - Uses recursive <tt>element.join(separator)</tt> if +element+ is a <tt>kind_of?(Array)</tt>. + * With no argument given, joins using the output field separator, <tt>$,</tt>: * - * With no argument, joins using the output field separator, <tt>$,</tt>: * a = [:foo, 'bar', 2] * $, # => nil * a.join # => "foobar2" * - * With \string argument +separator+, joins using that separator: + * With string argument +separator+ given, joins using that separator: + * * a = [:foo, 'bar', 2] * a.join("\n") # => "foo\nbar\n2" * - * Joins recursively for nested Arrays: + * Joins recursively for nested arrays: + * * a = [:foo, [:bar, [:baz, :bat]]] * a.join # => "foobarbazbat" + * + * Related: see {Methods for Converting}[rdoc-ref:Array@Methods+for+Converting]. */ static VALUE rb_ary_join_m(int argc, VALUE *argv, VALUE ary) @@ -2859,10 +3087,10 @@ inspect_ary(VALUE ary, VALUE dummy, int recur) if (recur) return rb_usascii_str_new_cstr("[...]"); str = rb_str_buf_new2("["); for (i=0; i<RARRAY_LEN(ary); i++) { - s = rb_inspect(RARRAY_AREF(ary, i)); - if (i > 0) rb_str_buf_cat2(str, ", "); - else rb_enc_copy(str, s); - rb_str_buf_append(str, s); + s = rb_inspect(RARRAY_AREF(ary, i)); + if (i > 0) rb_str_buf_cat2(str, ", "); + else rb_enc_copy(str, s); + rb_str_buf_append(str, s); } rb_str_buf_cat2(str, "]"); return str; @@ -2870,14 +3098,16 @@ inspect_ary(VALUE ary, VALUE dummy, int recur) /* * call-seq: - * array.inspect -> new_string + * inspect -> new_string + * to_s -> new_string * - * Returns the new \String formed by calling method <tt>#inspect</tt> + * Returns the new string formed by calling method <tt>#inspect</tt> * on each array element: + * * a = [:foo, 'bar', 2] * a.inspect # => "[:foo, \"bar\", 2]" * - * Array#to_s is an alias for Array#inspect. + * Related: see {Methods for Converting}[rdoc-ref:Array@Methods+for+Converting]. */ static VALUE @@ -2897,51 +3127,53 @@ rb_ary_to_s(VALUE ary) * call-seq: * to_a -> self or new_array * - * When +self+ is an instance of \Array, returns +self+: - * a = [:foo, 'bar', 2] - * a.to_a # => [:foo, "bar", 2] + * When +self+ is an instance of \Array, returns +self+. + * + * Otherwise, returns a new array containing the elements of +self+: * - * Otherwise, returns a new \Array containing the elements of +self+: * class MyArray < Array; end - * a = MyArray.new(['foo', 'bar', 'two']) - * a.instance_of?(Array) # => false - * a.kind_of?(Array) # => true - * a1 = a.to_a - * a1 # => ["foo", "bar", "two"] - * a1.class # => Array # Not MyArray + * my_a = MyArray.new(['foo', 'bar', 'two']) + * a = my_a.to_a + * a # => ["foo", "bar", "two"] + * a.class # => Array # Not MyArray. + * + * Related: see {Methods for Converting}[rdoc-ref:Array@Methods+for+Converting]. */ static VALUE rb_ary_to_a(VALUE ary) { if (rb_obj_class(ary) != rb_cArray) { - VALUE dup = rb_ary_new2(RARRAY_LEN(ary)); - rb_ary_replace(dup, ary); - return dup; + VALUE dup = rb_ary_new2(RARRAY_LEN(ary)); + rb_ary_replace(dup, ary); + return dup; } return ary; } /* * call-seq: - * array.to_h -> new_hash - * array.to_h {|item| ... } -> new_hash + * to_h -> new_hash + * to_h {|element| ... } -> new_hash * - * Returns a new \Hash formed from +self+. + * Returns a new hash formed from +self+. * - * When a block is given, calls the block with each array element; - * the block must return a 2-element \Array whose two elements - * form a key-value pair in the returned \Hash: - * a = ['foo', :bar, 1, [2, 3], {baz: 4}] - * h = a.to_h {|item| [item, item] } - * h # => {"foo"=>"foo", :bar=>:bar, 1=>1, [2, 3]=>[2, 3], {:baz=>4}=>{:baz=>4}} + * With no block given, each element of +self+ must be a 2-element sub-array; + * forms each sub-array into a key-value pair in the new hash: * - * When no block is given, +self+ must be an \Array of 2-element sub-arrays, - * each sub-array is formed into a key-value pair in the new \Hash: - * [].to_h # => {} * a = [['foo', 'zero'], ['bar', 'one'], ['baz', 'two']] - * h = a.to_h - * h # => {"foo"=>"zero", "bar"=>"one", "baz"=>"two"} + * a.to_h # => {"foo"=>"zero", "bar"=>"one", "baz"=>"two"} + * [].to_h # => {} + * + * With a block given, the block must return a 2-element array; + * calls the block with each element of +self+; + * forms each returned array into a key-value pair in the returned hash: + * + * a = ['foo', :bar, 1, [2, 3], {baz: 4}] + * a.to_h {|element| [element, element.class] } + * # => {"foo"=>String, :bar=>Symbol, 1=>Integer, [2, 3]=>Array, {:baz=>4}=>Hash} + * + * Related: see {Methods for Converting}[rdoc-ref:Array@Methods+for+Converting]. */ static VALUE @@ -2952,25 +3184,25 @@ rb_ary_to_h(VALUE ary) int block_given = rb_block_given_p(); for (i=0; i<RARRAY_LEN(ary); i++) { - const VALUE e = rb_ary_elt(ary, i); - const VALUE elt = block_given ? rb_yield_force_blockarg(e) : e; - const VALUE key_value_pair = rb_check_array_type(elt); - if (NIL_P(key_value_pair)) { - rb_raise(rb_eTypeError, "wrong element type %"PRIsVALUE" at %ld (expected array)", - rb_obj_class(elt), i); - } - if (RARRAY_LEN(key_value_pair) != 2) { - rb_raise(rb_eArgError, "wrong array length at %ld (expected 2, was %ld)", - i, RARRAY_LEN(key_value_pair)); - } - rb_hash_aset(hash, RARRAY_AREF(key_value_pair, 0), RARRAY_AREF(key_value_pair, 1)); + const VALUE e = rb_ary_elt(ary, i); + const VALUE elt = block_given ? rb_yield_force_blockarg(e) : e; + const VALUE key_value_pair = rb_check_array_type(elt); + if (NIL_P(key_value_pair)) { + rb_raise(rb_eTypeError, "wrong element type %"PRIsVALUE" at %ld (expected array)", + rb_obj_class(elt), i); + } + if (RARRAY_LEN(key_value_pair) != 2) { + rb_raise(rb_eArgError, "wrong array length at %ld (expected 2, was %ld)", + i, RARRAY_LEN(key_value_pair)); + } + rb_hash_aset(hash, RARRAY_AREF(key_value_pair, 0), RARRAY_AREF(key_value_pair, 1)); } return hash; } /* * call-seq: - * array.to_ary -> self + * to_ary -> self * * Returns +self+. */ @@ -2985,9 +3217,9 @@ static void ary_reverse(VALUE *p1, VALUE *p2) { while (p1 < p2) { - VALUE tmp = *p1; - *p1++ = *p2; - *p2-- = tmp; + VALUE tmp = *p1; + *p1++ = *p2; + *p2-- = tmp; } } @@ -2999,21 +3231,26 @@ rb_ary_reverse(VALUE ary) rb_ary_modify(ary); if (len > 1) { - RARRAY_PTR_USE_TRANSIENT(ary, p1, { + RARRAY_PTR_USE(ary, p1, { p2 = p1 + len - 1; /* points last item */ ary_reverse(p1, p2); - }); /* WB: no new reference */ + }); /* WB: no new reference */ } return ary; } /* * call-seq: - * array.reverse! -> self + * reverse! -> self + * + * Reverses the order of the elements of +self+; + * returns +self+: * - * Reverses +self+ in place: - * a = ['foo', 'bar', 'two'] - * a.reverse! # => ["two", "bar", "foo"] + * a = [0, 1, 2] + * a.reverse! # => [2, 1, 0] + * a # => [2, 1, 0] + * + * Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ static VALUE @@ -3024,12 +3261,13 @@ rb_ary_reverse_bang(VALUE ary) /* * call-seq: - * array.reverse -> new_array + * reverse -> new_array + * + * Returns a new array containing the elements of +self+ in reverse order: * - * Returns a new \Array with the elements of +self+ in reverse order. - * a = ['foo', 'bar', 'two'] - * a1 = a.reverse - * a1 # => ["two", "bar", "foo"] + * [0, 1, 2].reverse # => [2, 1, 0] + * + * Related: see {Methods for Combining}[rdoc-ref:Array@Methods+for+Combining]. */ static VALUE @@ -3039,9 +3277,9 @@ rb_ary_reverse_m(VALUE ary) VALUE dup = rb_ary_new2(len); if (len > 0) { - const VALUE *p1 = RARRAY_CONST_PTR_TRANSIENT(ary); - VALUE *p2 = (VALUE *)RARRAY_CONST_PTR_TRANSIENT(dup) + len - 1; - do *p2-- = *p1++; while (--len > 0); + const VALUE *p1 = RARRAY_CONST_PTR(ary); + VALUE *p2 = (VALUE *)RARRAY_CONST_PTR(dup) + len - 1; + do *p2-- = *p1++; while (--len > 0); } ARY_SET_LEN(dup, RARRAY_LEN(ary)); return dup; @@ -3082,7 +3320,7 @@ rb_ary_rotate(VALUE ary, long cnt) if (cnt != 0) { long len = RARRAY_LEN(ary); if (len > 1 && (cnt = rotate_count(cnt, len)) > 0) { - RARRAY_PTR_USE_TRANSIENT(ary, ptr, ary_rotate_ptr(ptr, len, cnt)); + RARRAY_PTR_USE(ary, ptr, ary_rotate_ptr(ptr, len, cnt)); return ary; } } @@ -3091,41 +3329,34 @@ rb_ary_rotate(VALUE ary, long cnt) /* * call-seq: - * array.rotate! -> self - * array.rotate!(count) -> self + * rotate!(count = 1) -> self * * Rotates +self+ in place by moving elements from one end to the other; returns +self+. * - * When no argument given, rotates the first element to the last position: - * a = [:foo, 'bar', 2, 'bar'] - * a.rotate! # => ["bar", 2, "bar", :foo] - * - * When given a non-negative \Integer +count+, + * With non-negative numeric +count+, * rotates +count+ elements from the beginning to the end: - * a = [:foo, 'bar', 2] - * a.rotate!(2) - * a # => [2, :foo, "bar"] + * + * [0, 1, 2, 3].rotate!(2) # => [2, 3, 0, 1] + [0, 1, 2, 3].rotate!(2.1) # => [2, 3, 0, 1] * * If +count+ is large, uses <tt>count % array.size</tt> as the count: - * a = [:foo, 'bar', 2] - * a.rotate!(20) - * a # => [2, :foo, "bar"] * - * If +count+ is zero, returns +self+ unmodified: - * a = [:foo, 'bar', 2] - * a.rotate!(0) - * a # => [:foo, "bar", 2] + * [0, 1, 2, 3].rotate!(21) # => [1, 2, 3, 0] + * + * If +count+ is zero, rotates no elements: + * + * [0, 1, 2, 3].rotate!(0) # => [0, 1, 2, 3] * - * When given a negative Integer +count+, rotates in the opposite direction, + * With a negative numeric +count+, rotates in the opposite direction, * from end to beginning: - * a = [:foo, 'bar', 2] - * a.rotate!(-2) - * a # => ["bar", 2, :foo] + * + * [0, 1, 2, 3].rotate!(-1) # => [3, 0, 1, 2] * * If +count+ is small (far from zero), uses <tt>count % array.size</tt> as the count: - * a = [:foo, 'bar', 2] - * a.rotate!(-5) - * a # => ["bar", 2, :foo] + * + * [0, 1, 2, 3].rotate!(-21) # => [3, 0, 1, 2] + * + * Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ static VALUE @@ -3138,44 +3369,35 @@ rb_ary_rotate_bang(int argc, VALUE *argv, VALUE ary) /* * call-seq: - * array.rotate -> new_array - * array.rotate(count) -> new_array + * rotate(count = 1) -> new_array * - * Returns a new \Array formed from +self+ with elements + * Returns a new array formed from +self+ with elements * rotated from one end to the other. * - * When no argument given, returns a new \Array that is like +self+, - * except that the first element has been rotated to the last position: - * a = [:foo, 'bar', 2, 'bar'] - * a1 = a.rotate - * a1 # => ["bar", 2, "bar", :foo] + * With non-negative numeric +count+, + * rotates elements from the beginning to the end: * - * When given a non-negative \Integer +count+, - * returns a new \Array with +count+ elements rotated from the beginning to the end: - * a = [:foo, 'bar', 2] - * a1 = a.rotate(2) - * a1 # => [2, :foo, "bar"] + * [0, 1, 2, 3].rotate(2) # => [2, 3, 0, 1] + * [0, 1, 2, 3].rotate(2.1) # => [2, 3, 0, 1] * * If +count+ is large, uses <tt>count % array.size</tt> as the count: - * a = [:foo, 'bar', 2] - * a1 = a.rotate(20) - * a1 # => [2, :foo, "bar"] * - * If +count+ is zero, returns a copy of +self+, unmodified: - * a = [:foo, 'bar', 2] - * a1 = a.rotate(0) - * a1 # => [:foo, "bar", 2] + * [0, 1, 2, 3].rotate(22) # => [2, 3, 0, 1] * - * When given a negative \Integer +count+, rotates in the opposite direction, - * from end to beginning: - * a = [:foo, 'bar', 2] - * a1 = a.rotate(-2) - * a1 # => ["bar", 2, :foo] + * With a +count+ of zero, rotates no elements: + * + * [0, 1, 2, 3].rotate(0) # => [0, 1, 2, 3] + * + * With negative numeric +count+, rotates in the opposite direction, + * from the end to the beginning: + * + * [0, 1, 2, 3].rotate(-1) # => [3, 0, 1, 2] * * If +count+ is small (far from zero), uses <tt>count % array.size</tt> as the count: - * a = [:foo, 'bar', 2] - * a1 = a.rotate(-5) - * a1 # => ["bar", 2, :foo] + * + * [0, 1, 2, 3].rotate(-21) # => [3, 0, 1, 2] + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -3189,11 +3411,11 @@ rb_ary_rotate_m(int argc, VALUE *argv, VALUE ary) len = RARRAY_LEN(ary); rotated = rb_ary_new2(len); if (len > 0) { - cnt = rotate_count(cnt, len); - ptr = RARRAY_CONST_PTR_TRANSIENT(ary); - len -= cnt; - ary_memcpy(rotated, 0, len, ptr + cnt); - ary_memcpy(rotated, len, cnt, ptr); + cnt = rotate_count(cnt, len); + ptr = RARRAY_CONST_PTR(ary); + len -= cnt; + ary_memcpy(rotated, 0, len, ptr + cnt); + ary_memcpy(rotated, len, cnt, ptr); } ARY_SET_LEN(rotated, RARRAY_LEN(ary)); return rotated; @@ -3202,14 +3424,13 @@ rb_ary_rotate_m(int argc, VALUE *argv, VALUE ary) struct ary_sort_data { VALUE ary; VALUE receiver; - struct cmp_opt_data cmp_opt; }; static VALUE sort_reentered(VALUE ary) { if (RBASIC(ary)->klass) { - rb_raise(rb_eRuntimeError, "sort reentered"); + rb_raise(rb_eRuntimeError, "sort reentered"); } return Qnil; } @@ -3248,16 +3469,16 @@ sort_2(const void *ap, const void *bp, void *dummy) VALUE a = *(const VALUE *)ap, b = *(const VALUE *)bp; int n; - if (FIXNUM_P(a) && FIXNUM_P(b) && CMP_OPTIMIZABLE(data->cmp_opt, Integer)) { - if ((long)a > (long)b) return 1; - if ((long)a < (long)b) return -1; - return 0; + if (FIXNUM_P(a) && FIXNUM_P(b) && CMP_OPTIMIZABLE(INTEGER)) { + if ((long)a > (long)b) return 1; + if ((long)a < (long)b) return -1; + return 0; } - if (STRING_P(a) && STRING_P(b) && CMP_OPTIMIZABLE(data->cmp_opt, String)) { - return rb_str_cmp(a, b); + if (STRING_P(a) && STRING_P(b) && CMP_OPTIMIZABLE(STRING)) { + return rb_str_cmp(a, b); } - if (RB_FLOAT_TYPE_P(a) && CMP_OPTIMIZABLE(data->cmp_opt, Float)) { - return rb_float_cmp(a, b); + if (RB_FLOAT_TYPE_P(a) && CMP_OPTIMIZABLE(FLOAT)) { + return rb_float_cmp(a, b); } retval = rb_funcallv(a, id_cmp, 1, &b); @@ -3269,65 +3490,40 @@ sort_2(const void *ap, const void *bp, void *dummy) /* * call-seq: - * array.sort! -> self - * array.sort! {|a, b| ... } -> self - * - * Returns +self+ with its elements sorted in place. + * sort! -> self + * sort! {|a, b| ... } -> self * - * With no block, compares elements using operator <tt><=></tt> - * (see Comparable): - * a = 'abcde'.split('').shuffle - * a # => ["e", "b", "d", "a", "c"] - * a.sort! - * a # => ["a", "b", "c", "d", "e"] + * Like Array#sort, but returns +self+ with its elements sorted in place. * - * With a block, calls the block with each element pair; - * for each element pair +a+ and +b+, the block should return an integer: - * - Negative when +b+ is to follow +a+. - * - Zero when +a+ and +b+ are equivalent. - * - Positive when +a+ is to follow +b+. - * - * Example: - * a = 'abcde'.split('').shuffle - * a # => ["e", "b", "d", "a", "c"] - * a.sort! {|a, b| a <=> b } - * a # => ["a", "b", "c", "d", "e"] - * a.sort! {|a, b| b <=> a } - * a # => ["e", "d", "c", "b", "a"] - * - * When the block returns zero, the order for +a+ and +b+ is indeterminate, - * and may be unstable: - * a = 'abcde'.split('').shuffle - * a # => ["e", "b", "d", "a", "c"] - * a.sort! {|a, b| 0 } - * a # => ["d", "e", "c", "a", "b"] + * Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ VALUE rb_ary_sort_bang(VALUE ary) { rb_ary_modify(ary); - assert(!ARY_SHARED_P(ary)); + RUBY_ASSERT(!ARY_SHARED_P(ary)); if (RARRAY_LEN(ary) > 1) { - VALUE tmp = ary_make_substitution(ary); /* only ary refers tmp */ - struct ary_sort_data data; - long len = RARRAY_LEN(ary); - RBASIC_CLEAR_CLASS(tmp); - data.ary = tmp; + VALUE tmp = ary_make_substitution(ary); /* only ary refers tmp */ + struct ary_sort_data data; + long len = RARRAY_LEN(ary); + RBASIC_CLEAR_CLASS(tmp); + data.ary = tmp; data.receiver = ary; - data.cmp_opt.opt_methods = 0; - data.cmp_opt.opt_inited = 0; - RARRAY_PTR_USE(tmp, ptr, { + RARRAY_PTR_USE(tmp, ptr, { ruby_qsort(ptr, len, sizeof(VALUE), rb_block_given_p()?sort_1:sort_2, &data); - }); /* WB: no new reference */ - rb_ary_modify(ary); + }); /* WB: no new reference */ + rb_ary_modify(ary); if (ARY_EMBED_P(tmp)) { if (ARY_SHARED_P(ary)) { /* ary might be destructively operated in the given block */ rb_ary_unshare(ary); - FL_SET_EMBED(ary); + FL_SET_EMBED(ary); + } + if (ARY_EMBED_LEN(tmp) > ARY_CAPA(ary)) { + ary_resize_capa(ary, ARY_EMBED_LEN(tmp)); } - ary_memcpy(ary, 0, ARY_EMBED_LEN(tmp), ARY_EMBED_PTR(tmp)); + ary_memcpy(ary, 0, ARY_EMBED_LEN(tmp), ARY_EMBED_PTR(tmp)); ARY_SET_LEN(ary, ARY_EMBED_LEN(tmp)); } else { @@ -3336,7 +3532,7 @@ rb_ary_sort_bang(VALUE ary) ARY_SET_CAPA(ary, RARRAY_LEN(tmp)); } else { - assert(!ARY_SHARED_P(tmp)); + RUBY_ASSERT(!ARY_SHARED_P(tmp)); if (ARY_EMBED_P(ary)) { FL_UNSET_EMBED(ary); } @@ -3352,10 +3548,9 @@ rb_ary_sort_bang(VALUE ary) ARY_SET_CAPA(ary, ARY_HEAP_LEN(tmp)); } /* tmp was lost ownership for the ptr */ - FL_UNSET(tmp, FL_FREEZE); FL_SET_EMBED(tmp); ARY_SET_EMBED_LEN(tmp, 0); - FL_SET(tmp, FL_FREEZE); + OBJ_FREEZE(tmp); } /* tmp will be GC'ed. */ RBASIC_SET_CLASS_RAW(tmp, rb_cArray); /* rb_cArray must be marked */ @@ -3366,40 +3561,36 @@ rb_ary_sort_bang(VALUE ary) /* * call-seq: - * array.sort -> new_array - * array.sort {|a, b| ... } -> new_array + * sort -> new_array + * sort {|a, b| ... } -> new_array * - * Returns a new \Array whose elements are those from +self+, sorted. + * Returns a new array containing the elements of +self+, sorted. * - * With no block, compares elements using operator <tt><=></tt> - * (see Comparable): - * a = 'abcde'.split('').shuffle - * a # => ["e", "b", "d", "a", "c"] - * a1 = a.sort - * a1 # => ["a", "b", "c", "d", "e"] + * With no block given, compares elements using operator <tt>#<=></tt> + * (see Object#<=>): + * + * [0, 2, 3, 1].sort # => [0, 1, 2, 3] + * + * With a block given, calls the block with each combination of pairs of elements from +self+; + * for each pair +a+ and +b+, the block should return a numeric: * - * With a block, calls the block with each element pair; - * for each element pair +a+ and +b+, the block should return an integer: * - Negative when +b+ is to follow +a+. * - Zero when +a+ and +b+ are equivalent. * - Positive when +a+ is to follow +b+. * * Example: - * a = 'abcde'.split('').shuffle - * a # => ["e", "b", "d", "a", "c"] - * a1 = a.sort {|a, b| a <=> b } - * a1 # => ["a", "b", "c", "d", "e"] - * a2 = a.sort {|a, b| b <=> a } - * a2 # => ["e", "d", "c", "b", "a"] + * + * a = [3, 2, 0, 1] + * a.sort {|a, b| a <=> b } # => [0, 1, 2, 3] + * a.sort {|a, b| b <=> a } # => [3, 2, 1, 0] * * When the block returns zero, the order for +a+ and +b+ is indeterminate, - * and may be unstable: - * a = 'abcde'.split('').shuffle - * a # => ["e", "b", "d", "a", "c"] - * a1 = a.sort {|a, b| 0 } - * a1 # => ["c", "e", "b", "d", "a"] + * and may be unstable. * - * Related: Enumerable#sort_by. + * See an example in Numeric#nonzero? for the idiom to sort more + * complex structure. + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ VALUE @@ -3414,12 +3605,15 @@ static VALUE rb_ary_bsearch_index(VALUE ary); /* * call-seq: - * array.bsearch {|element| ... } -> object - * array.bsearch -> new_enumerator + * bsearch {|element| ... } -> found_element or nil + * bsearch -> new_enumerator + * + * Returns the element from +self+ found by a binary search, + * or +nil+ if the search found no suitable element. * - * Returns an element from +self+ selected by a binary search. + * See {Binary Searching}[rdoc-ref:language/bsearch.rdoc]. * - * See {Binary Searching}[rdoc-ref:bsearch.rdoc]. + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -3428,18 +3622,22 @@ rb_ary_bsearch(VALUE ary) VALUE index_result = rb_ary_bsearch_index(ary); if (FIXNUM_P(index_result)) { - return rb_ary_entry(ary, FIX2LONG(index_result)); + return rb_ary_entry(ary, FIX2LONG(index_result)); } return index_result; } /* * call-seq: - * array.bsearch_index {|element| ... } -> integer or nil - * array.bsearch_index -> new_enumerator + * bsearch_index {|element| ... } -> integer or nil + * bsearch_index -> new_enumerator * - * Searches +self+ as described at method #bsearch, - * but returns the _index_ of the found element instead of the element itself. + * Returns the integer index of the element from +self+ found by a binary search, + * or +nil+ if the search found no suitable element. + * + * See {Binary Searching}[rdoc-ref:language/bsearch.rdoc]. + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -3451,39 +3649,39 @@ rb_ary_bsearch_index(VALUE ary) RETURN_ENUMERATOR(ary, 0, 0); while (low < high) { - mid = low + ((high - low) / 2); - val = rb_ary_entry(ary, mid); - v = rb_yield(val); - if (FIXNUM_P(v)) { - if (v == INT2FIX(0)) return INT2FIX(mid); - smaller = (SIGNED_VALUE)v < 0; /* Fixnum preserves its sign-bit */ - } - else if (v == Qtrue) { - satisfied = 1; - smaller = 1; - } - else if (!RTEST(v)) { - smaller = 0; - } - else if (rb_obj_is_kind_of(v, rb_cNumeric)) { - const VALUE zero = INT2FIX(0); - switch (rb_cmpint(rb_funcallv(v, id_cmp, 1, &zero), v, zero)) { - case 0: return INT2FIX(mid); - case 1: smaller = 1; break; - case -1: smaller = 0; - } - } - else { - rb_raise(rb_eTypeError, "wrong argument type %"PRIsVALUE - " (must be numeric, true, false or nil)", - rb_obj_class(v)); - } - if (smaller) { - high = mid; - } - else { - low = mid + 1; - } + mid = low + ((high - low) / 2); + val = rb_ary_entry(ary, mid); + v = rb_yield(val); + if (FIXNUM_P(v)) { + if (v == INT2FIX(0)) return INT2FIX(mid); + smaller = (SIGNED_VALUE)v < 0; /* Fixnum preserves its sign-bit */ + } + else if (v == Qtrue) { + satisfied = 1; + smaller = 1; + } + else if (!RTEST(v)) { + smaller = 0; + } + else if (rb_obj_is_kind_of(v, rb_cNumeric)) { + const VALUE zero = INT2FIX(0); + switch (rb_cmpint(rb_funcallv(v, id_cmp, 1, &zero), v, zero)) { + case 0: return INT2FIX(mid); + case 1: smaller = 0; break; + case -1: smaller = 1; + } + } + else { + rb_raise(rb_eTypeError, "wrong argument type %"PRIsVALUE + " (must be numeric, true, false or nil)", + rb_obj_class(v)); + } + if (smaller) { + high = mid; + } + else { + low = mid + 1; + } } if (!satisfied) return Qnil; return INT2FIX(low); @@ -3498,26 +3696,24 @@ sort_by_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, dummy)) /* * call-seq: - * array.sort_by! {|element| ... } -> self - * array.sort_by! -> new_enumerator + * sort_by! {|element| ... } -> self + * sort_by! -> new_enumerator * - * Sorts the elements of +self+ in place, - * using an ordering determined by the block; returns self. + * With a block given, sorts the elements of +self+ in place; + * returns self. * * Calls the block with each successive element; - * sorts elements based on the values returned from the block. + * sorts elements based on the values returned from the block: * - * For duplicates returned by the block, the ordering is indeterminate, and may be unstable. - * - * This example sorts strings based on their sizes: * a = ['aaaa', 'bbb', 'cc', 'd'] * a.sort_by! {|element| element.size } * a # => ["d", "cc", "bbb", "aaaa"] * - * Returns a new \Enumerator if no block given: + * For duplicate values returned by the block, the ordering is indeterminate, and may be unstable. * - * a = ['aaaa', 'bbb', 'cc', 'd'] - * a.sort_by! # => #<Enumerator: ["aaaa", "bbb", "cc", "d"]:sort_by!> + * With no block given, returns a new Enumerator. + * + * Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ static VALUE @@ -3527,29 +3723,32 @@ rb_ary_sort_by_bang(VALUE ary) RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length); rb_ary_modify(ary); - sorted = rb_block_call(ary, rb_intern("sort_by"), 0, 0, sort_by_i, 0); - rb_ary_replace(ary, sorted); + if (RARRAY_LEN(ary) > 1) { + sorted = rb_block_call(ary, rb_intern("sort_by"), 0, 0, sort_by_i, 0); + rb_ary_replace(ary, sorted); + } return ary; } /* * call-seq: - * array.map {|element| ... } -> new_array - * array.map -> new_enumerator + * collect {|element| ... } -> new_array + * collect -> new_enumerator + * map {|element| ... } -> new_array + * map -> new_enumerator + * + * With a block given, calls the block with each element of +self+; + * returns a new array whose elements are the return values from the block: * - * Calls the block, if given, with each element of +self+; - * returns a new \Array whose elements are the return values from the block: * a = [:foo, 'bar', 2] * a1 = a.map {|element| element.class } * a1 # => [Symbol, String, Integer] * - * Returns a new \Enumerator if no block given: - * a = [:foo, 'bar', 2] - * a1 = a.map - * a1 # => #<Enumerator: [:foo, "bar", 2]:map> + * With no block given, returns a new Enumerator. * - * Array#collect is an alias for Array#map. + * Related: #collect!; + * see also {Methods for Converting}[rdoc-ref:Array@Methods+for+Converting]. */ static VALUE @@ -3569,20 +3768,22 @@ rb_ary_collect(VALUE ary) /* * call-seq: - * array.map! {|element| ... } -> self - * array.map! -> new_enumerator + * collect! {|element| ... } -> self + * collect! -> new_enumerator + * map! {|element| ... } -> self + * map! -> new_enumerator + * + * With a block given, calls the block with each element of +self+ + * and replaces the element with the block's return value; + * returns +self+: * - * Calls the block, if given, with each element; - * replaces the element with the block's return value: * a = [:foo, 'bar', 2] * a.map! { |element| element.class } # => [Symbol, String, Integer] * - * Returns a new \Enumerator if no block given: - * a = [:foo, 'bar', 2] - * a1 = a.map! - * a1 # => #<Enumerator: [:foo, "bar", 2]:map!> + * With no block given, returns a new Enumerator. * - * Array#collect! is an alias for Array#map!. + * Related: #collect; + * see also {Methods for Converting}[rdoc-ref:Array@Methods+for+Converting]. */ static VALUE @@ -3593,7 +3794,7 @@ rb_ary_collect_bang(VALUE ary) RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length); rb_ary_modify(ary); for (i = 0; i < RARRAY_LEN(ary); i++) { - rb_ary_store(ary, i, rb_yield(RARRAY_AREF(ary, i))); + rb_ary_store(ary, i, rb_yield(RARRAY_AREF(ary, i))); } return ary; } @@ -3605,21 +3806,21 @@ rb_get_values_at(VALUE obj, long olen, int argc, const VALUE *argv, VALUE (*func long beg, len, i, j; for (i=0; i<argc; i++) { - if (FIXNUM_P(argv[i])) { - rb_ary_push(result, (*func)(obj, FIX2LONG(argv[i]))); - continue; - } - /* check if idx is Range */ - if (rb_range_beg_len(argv[i], &beg, &len, olen, 1)) { - long end = olen < beg+len ? olen : beg+len; - for (j = beg; j < end; j++) { - rb_ary_push(result, (*func)(obj, j)); - } - if (beg + len > j) - rb_ary_resize(result, RARRAY_LEN(result) + (beg + len) - j); - continue; - } - rb_ary_push(result, (*func)(obj, NUM2LONG(argv[i]))); + if (FIXNUM_P(argv[i])) { + rb_ary_push(result, (*func)(obj, FIX2LONG(argv[i]))); + continue; + } + /* check if idx is Range */ + if (rb_range_beg_len(argv[i], &beg, &len, olen, 1)) { + long end = olen < beg+len ? olen : beg+len; + for (j = beg; j < end; j++) { + rb_ary_push(result, (*func)(obj, j)); + } + if (beg + len > j) + rb_ary_resize(result, RARRAY_LEN(result) + (beg + len) - j); + continue; + } + rb_ary_push(result, (*func)(obj, NUM2LONG(argv[i]))); } return result; } @@ -3629,63 +3830,133 @@ append_values_at_single(VALUE result, VALUE ary, long olen, VALUE idx) { long beg, len; if (FIXNUM_P(idx)) { - beg = FIX2LONG(idx); + beg = FIX2LONG(idx); } /* check if idx is Range */ else if (rb_range_beg_len(idx, &beg, &len, olen, 1)) { - if (len > 0) { - const VALUE *const src = RARRAY_CONST_PTR_TRANSIENT(ary); - const long end = beg + len; - const long prevlen = RARRAY_LEN(result); - if (beg < olen) { - rb_ary_cat(result, src + beg, end > olen ? olen-beg : len); - } - if (end > olen) { - rb_ary_store(result, prevlen + len - 1, Qnil); - } - } - return result; + if (len > 0) { + const VALUE *const src = RARRAY_CONST_PTR(ary); + const long end = beg + len; + const long prevlen = RARRAY_LEN(result); + if (beg < olen) { + rb_ary_cat(result, src + beg, end > olen ? olen-beg : len); + } + if (end > olen) { + rb_ary_store(result, prevlen + len - 1, Qnil); + } + } + return result; } else { - beg = NUM2LONG(idx); + beg = NUM2LONG(idx); } return rb_ary_push(result, rb_ary_entry(ary, beg)); } /* * call-seq: - * array.values_at(*indexes) -> new_array + * values_at(*specifiers) -> new_array * - * Returns a new \Array whose elements are the elements - * of +self+ at the given \Integer or \Range +indexes+. + * Returns elements from +self+ in a new array; does not modify +self+. * - * For each positive +index+, returns the element at offset +index+: - * a = [:foo, 'bar', 2] - * a.values_at(0, 2) # => [:foo, 2] - * a.values_at(0..1) # => [:foo, "bar"] + * The objects included in the returned array are the elements of +self+ + * selected by the given +specifiers+, + * each of which must be a numeric index or a Range. * - * The given +indexes+ may be in any order, and may repeat: - * a = [:foo, 'bar', 2] - * a.values_at(2, 0, 1, 0, 2) # => [2, :foo, "bar", :foo, 2] - * a.values_at(1, 0..2) # => ["bar", :foo, "bar", 2] + * In brief: * - * Assigns +nil+ for an +index+ that is too large: - * a = [:foo, 'bar', 2] - * a.values_at(0, 3, 1, 3) # => [:foo, nil, "bar", nil] + * a = ['a', 'b', 'c', 'd'] * - * Returns a new empty \Array if no arguments given. + * # Index specifiers. + * a.values_at(2, 0, 2, 0) # => ["c", "a", "c", "a"] # May repeat. + * a.values_at(-4, -3, -2, -1) # => ["a", "b", "c", "d"] # Counts backwards if negative. + * a.values_at(-50, 50) # => [nil, nil] # Outside of self. * - * For each negative +index+, counts backward from the end of the array: - * a = [:foo, 'bar', 2] - * a.values_at(-1, -3) # => [2, :foo] + * # Range specifiers. + * a.values_at(1..3) # => ["b", "c", "d"] # From range.begin to range.end. + * a.values_at(1...3) # => ["b", "c"] # End excluded. + * a.values_at(3..1) # => [] # No such elements. * - * Assigns +nil+ for an +index+ that is too small: - * a = [:foo, 'bar', 2] - * a.values_at(0, -5, 1, -6, 2) # => [:foo, nil, "bar", nil, 2] + * a.values_at(-3..3) # => ["b", "c", "d"] # Negative range.begin counts backwards. + * a.values_at(-50..3) # Raises RangeError. * - * The given +indexes+ may have a mixture of signs: - * a = [:foo, 'bar', 2] - * a.values_at(0, -2, 1, -1) # => [:foo, "bar", "bar", 2] + * a.values_at(1..-2) # => ["b", "c"] # Negative range.end counts backwards. + * a.values_at(1..-50) # => [] # No such elements. + * + * # Mixture of specifiers. + * a.values_at(2..3, 3, 0..1, 0) # => ["c", "d", "d", "a", "b", "a"] + * + * With no +specifiers+ given, returns a new empty array: + * + * a = ['a', 'b', 'c', 'd'] + * a.values_at # => [] + * + * For each numeric specifier +index+, includes an element: + * + * - For each non-negative numeric specifier +index+ that is in-range (less than <tt>self.size</tt>), + * includes the element at offset +index+: + * + * a.values_at(0, 2) # => ["a", "c"] + * a.values_at(0.1, 2.9) # => ["a", "c"] + * + * - For each negative numeric +index+ that is in-range (greater than or equal to <tt>- self.size</tt>), + * counts backwards from the end of +self+: + * + * a.values_at(-1, -4) # => ["d", "a"] + * + * The given indexes may be in any order, and may repeat: + * + * a.values_at(2, 0, 1, 0, 2) # => ["c", "a", "b", "a", "c"] + * + * For each +index+ that is out-of-range, includes +nil+: + * + * a.values_at(4, -5) # => [nil, nil] + * + * For each Range specifier +range+, includes elements + * according to <tt>range.begin</tt> and <tt>range.end</tt>: + * + * - If both <tt>range.begin</tt> and <tt>range.end</tt> + * are non-negative and in-range (less than <tt>self.size</tt>), + * includes elements from index <tt>range.begin</tt> + * through <tt>range.end - 1</tt> (if <tt>range.exclude_end?</tt>), + * or through <tt>range.end</tt> (otherwise): + * + * a.values_at(1..2) # => ["b", "c"] + * a.values_at(1...2) # => ["b"] + * + * - If <tt>range.begin</tt> is negative and in-range (greater than or equal to <tt>- self.size</tt>), + * counts backwards from the end of +self+: + * + * a.values_at(-2..3) # => ["c", "d"] + * + * - If <tt>range.begin</tt> is negative and out-of-range, raises an exception: + * + * a.values_at(-5..3) # Raises RangeError. + * + * - If <tt>range.end</tt> is positive and out-of-range, + * extends the returned array with +nil+ elements: + * + * a.values_at(1..5) # => ["b", "c", "d", nil, nil] + * + * - If <tt>range.end</tt> is negative and in-range, + * counts backwards from the end of +self+: + * + * a.values_at(1..-2) # => ["b", "c"] + * + * - If <tt>range.end</tt> is negative and out-of-range, + * returns an empty array: + * + * a.values_at(1..-5) # => [] + * + * The given ranges may be in any order and may repeat: + * + * a.values_at(2..3, 0..1, 2..3) # => ["c", "d", "a", "b", "c", "d"] + * + * The given specifiers may be any mixture of indexes and ranges: + * + * a.values_at(3, 1..2, 0, 2..3) # => ["d", "b", "c", "a", "c", "d"] + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -3694,7 +3965,7 @@ rb_ary_values_at(int argc, VALUE *argv, VALUE ary) long i, olen = RARRAY_LEN(ary); VALUE result = rb_ary_new_capa(argc); for (i = 0; i < argc; ++i) { - append_values_at_single(result, ary, olen, argv[i]); + append_values_at_single(result, ary, olen, argv[i]); } RB_GC_GUARD(ary); return result; @@ -3703,21 +3974,22 @@ rb_ary_values_at(int argc, VALUE *argv, VALUE ary) /* * call-seq: - * array.select {|element| ... } -> new_array - * array.select -> new_enumerator + * select {|element| ... } -> new_array + * select -> new_enumerator + * filter {|element| ... } -> new_array + * filter -> new_enumerator * - * Calls the block, if given, with each element of +self+; - * returns a new \Array containing those elements of +self+ + * With a block given, calls the block with each element of +self+; + * returns a new array containing those elements of +self+ * for which the block returns a truthy value: - * a = [:foo, 'bar', 2, :bam] - * a1 = a.select {|element| element.to_s.start_with?('b') } - * a1 # => ["bar", :bam] * - * Returns a new \Enumerator if no block given: * a = [:foo, 'bar', 2, :bam] - * a.select # => #<Enumerator: [:foo, "bar", 2, :bam]:select> + * a.select {|element| element.to_s.start_with?('b') } + * # => ["bar", :bam] + * + * With no block given, returns a new Enumerator. * - * Array#filter is an alias for Array#select. + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -3729,9 +4001,9 @@ rb_ary_select(VALUE ary) RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length); result = rb_ary_new2(RARRAY_LEN(ary)); for (i = 0; i < RARRAY_LEN(ary); i++) { - if (RTEST(rb_yield(RARRAY_AREF(ary, i)))) { - rb_ary_push(result, rb_ary_elt(ary, i)); - } + if (RTEST(rb_yield(RARRAY_AREF(ary, i)))) { + rb_ary_push(result, rb_ary_elt(ary, i)); + } } return result; } @@ -3749,12 +4021,12 @@ select_bang_i(VALUE a) long i1, i2; for (i1 = i2 = 0; i1 < RARRAY_LEN(ary); arg->len[0] = ++i1) { - VALUE v = RARRAY_AREF(ary, i1); - if (!RTEST(rb_yield(v))) continue; - if (i1 != i2) { - rb_ary_store(ary, i2, v); - } - arg->len[1] = ++i2; + VALUE v = RARRAY_AREF(ary, i1); + if (!RTEST(rb_yield(v))) continue; + if (i1 != i2) { + rb_ary_store(ary, i2, v); + } + arg->len[1] = ++i2; } return (i1 == i2) ? Qnil : ary; } @@ -3768,38 +4040,39 @@ select_bang_ensure(VALUE a) long i1 = arg->len[0], i2 = arg->len[1]; if (i2 < len && i2 < i1) { - long tail = 0; + long tail = 0; rb_ary_modify(ary); - if (i1 < len) { - tail = len - i1; - RARRAY_PTR_USE_TRANSIENT(ary, ptr, { - MEMMOVE(ptr + i2, ptr + i1, VALUE, tail); - }); - } - ARY_SET_LEN(ary, i2 + tail); + if (i1 < len) { + tail = len - i1; + RARRAY_PTR_USE(ary, ptr, { + MEMMOVE(ptr + i2, ptr + i1, VALUE, tail); + }); + } + ARY_SET_LEN(ary, i2 + tail); } return ary; } /* * call-seq: - * array.select! {|element| ... } -> self or nil - * array.select! -> new_enumerator + * select! {|element| ... } -> self or nil + * select! -> new_enumerator + * filter! {|element| ... } -> self or nil + * filter! -> new_enumerator * - * Calls the block, if given with each element of +self+; + * With a block given, calls the block with each element of +self+; * removes from +self+ those elements for which the block returns +false+ or +nil+. * * Returns +self+ if any elements were removed: + * * a = [:foo, 'bar', 2, :bam] * a.select! {|element| element.to_s.start_with?('b') } # => ["bar", :bam] * * Returns +nil+ if no elements were removed. * - * Returns a new \Enumerator if no block given: - * a = [:foo, 'bar', 2, :bam] - * a.select! # => #<Enumerator: [:foo, "bar", 2, :bam]:select!> + * With no block given, returns a new Enumerator. * - * Array#filter! is an alias for Array#select!. + * Related: see {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ static VALUE @@ -3817,17 +4090,18 @@ rb_ary_select_bang(VALUE ary) /* * call-seq: - * array.keep_if {|element| ... } -> self - * array.keep_if -> new_enumeration + * keep_if {|element| ... } -> self + * keep_if -> new_enumerator + * + * With a block given, calls the block with each element of +self+; + * removes the element from +self+ if the block does not return a truthy value: * - * Retains those elements for which the block returns a truthy value; - * deletes all other elements; returns +self+: * a = [:foo, 'bar', 2, :bam] * a.keep_if {|element| element.to_s.start_with?('b') } # => ["bar", :bam] * - * Returns a new \Enumerator if no block given: - * a = [:foo, 'bar', 2, :bam] - * a.keep_if # => #<Enumerator: [:foo, "bar", 2, :bam]:keep_if> + * With no block given, returns a new Enumerator. + * + * Related: see {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ static VALUE @@ -3843,44 +4117,49 @@ ary_resize_smaller(VALUE ary, long len) { rb_ary_modify(ary); if (RARRAY_LEN(ary) > len) { - ARY_SET_LEN(ary, len); - if (len * 2 < ARY_CAPA(ary) && - ARY_CAPA(ary) > ARY_DEFAULT_SIZE) { - ary_resize_capa(ary, len * 2); - } + ARY_SET_LEN(ary, len); + if (len * 2 < ARY_CAPA(ary) && + ARY_CAPA(ary) > ARY_DEFAULT_SIZE) { + ary_resize_capa(ary, len * 2); + } } } /* * call-seq: - * array.delete(obj) -> deleted_object - * array.delete(obj) {|nosuch| ... } -> deleted_object or block_return + * delete(object) -> last_removed_object + * delete(object) {|element| ... } -> last_removed_object or block_return * - * Removes zero or more elements from +self+; returns +self+. + * Removes zero or more elements from +self+. * - * When no block is given, - * removes from +self+ each element +ele+ such that <tt>ele == obj</tt>; - * returns the last deleted element: - * s1 = 'bar'; s2 = 'bar' - * a = [:foo, s1, 2, s2] - * a.delete('bar') # => "bar" - * a # => [:foo, 2] + * With no block given, + * removes from +self+ each element +ele+ such that <tt>ele == object</tt>; + * returns the last removed element: * - * Returns +nil+ if no elements removed. + * a = [0, 1, 2, 2.0] + * a.delete(2) # => 2.0 + * a # => [0, 1] + * + * Returns +nil+ if no elements removed: * - * When a block is given, - * removes from +self+ each element +ele+ such that <tt>ele == obj</tt>. + * a.delete(2) # => nil + * + * With a block given, + * removes from +self+ each element +ele+ such that <tt>ele == object</tt>. * * If any such elements are found, ignores the block - * and returns the last deleted element: - * s1 = 'bar'; s2 = 'bar' - * a = [:foo, s1, 2, s2] - * deleted_obj = a.delete('bar') {|obj| fail 'Cannot happen' } - * a # => [:foo, 2] + * and returns the last removed element: * - * If no such elements are found, returns the block's return value: - * a = [:foo, 'bar', 2] - * a.delete(:nosuch) {|obj| "#{obj} not found" } # => "nosuch not found" + * a = [0, 1, 2, 2.0] + * a.delete(2) {|element| fail 'Cannot happen' } # => 2.0 + * a # => [0, 1] + * + * If no such element is found, returns the block's return value: + * + * a.delete(2) {|element| "Element #{element} not found." } + * # => "Element 2 not found." + * + * Related: see {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ VALUE @@ -3890,22 +4169,22 @@ rb_ary_delete(VALUE ary, VALUE item) long i1, i2; for (i1 = i2 = 0; i1 < RARRAY_LEN(ary); i1++) { - VALUE e = RARRAY_AREF(ary, i1); + VALUE e = RARRAY_AREF(ary, i1); - if (rb_equal(e, item)) { - v = e; - continue; - } - if (i1 != i2) { - rb_ary_store(ary, i2, e); - } - i2++; + if (rb_equal(e, item)) { + v = e; + continue; + } + if (i1 != i2) { + rb_ary_store(ary, i2, e); + } + i2++; } if (RARRAY_LEN(ary) == i2) { - if (rb_block_given_p()) { - return rb_yield(item); - } - return Qnil; + if (rb_block_given_p()) { + return rb_yield(item); + } + return Qnil; } ary_resize_smaller(ary, i2); @@ -3920,18 +4199,18 @@ rb_ary_delete_same(VALUE ary, VALUE item) long i1, i2; for (i1 = i2 = 0; i1 < RARRAY_LEN(ary); i1++) { - VALUE e = RARRAY_AREF(ary, i1); + VALUE e = RARRAY_AREF(ary, i1); - if (e == item) { - continue; - } - if (i1 != i2) { - rb_ary_store(ary, i2, e); - } - i2++; + if (e == item) { + continue; + } + if (i1 != i2) { + rb_ary_store(ary, i2, e); + } + i2++; } if (RARRAY_LEN(ary) == i2) { - return; + return; } ary_resize_smaller(ary, i2); @@ -3945,13 +4224,13 @@ rb_ary_delete_at(VALUE ary, long pos) if (pos >= len) return Qnil; if (pos < 0) { - pos += len; - if (pos < 0) return Qnil; + pos += len; + if (pos < 0) return Qnil; } rb_ary_modify(ary); del = RARRAY_AREF(ary, pos); - RARRAY_PTR_USE_TRANSIENT(ary, ptr, { + RARRAY_PTR_USE(ary, ptr, { MEMMOVE(ptr+pos, ptr+pos+1, VALUE, len-pos-1); }); ARY_INCREASE_LEN(ary, -1); @@ -3961,23 +4240,30 @@ rb_ary_delete_at(VALUE ary, long pos) /* * call-seq: - * array.delete_at(index) -> deleted_object or nil + * delete_at(index) -> removed_object or nil * - * Deletes an element from +self+, per the given \Integer +index+. + * Removes the element of +self+ at the given +index+, which must be an + * {integer-convertible object}[rdoc-ref:implicit_conversion.rdoc@Integer-Convertible+Objects]. * * When +index+ is non-negative, deletes the element at offset +index+: + * * a = [:foo, 'bar', 2] * a.delete_at(1) # => "bar" * a # => [:foo, 2] * - * If index is too large, returns +nil+. - * * When +index+ is negative, counts backward from the end of the array: + * * a = [:foo, 'bar', 2] * a.delete_at(-2) # => "bar" * a # => [:foo, 2] * - * If +index+ is too small (far from zero), returns nil. + * When +index+ is out of range, returns +nil+. + * + * a = [:foo, 'bar', 2] + * a.delete_at(3) # => nil + * a.delete_at(-4) # => nil + * + * Related: see {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ static VALUE @@ -4010,7 +4296,7 @@ ary_slice_bang_by_rb_ary_splice(VALUE ary, long pos, long len) return rb_ary_new2(0); } else { - VALUE arg2 = rb_ary_new4(len, RARRAY_CONST_PTR_TRANSIENT(ary)+pos); + VALUE arg2 = rb_ary_new4(len, RARRAY_CONST_PTR(ary)+pos); rb_ary_splice(ary, pos, len, 0, 0); return arg2; } @@ -4018,63 +4304,94 @@ ary_slice_bang_by_rb_ary_splice(VALUE ary, long pos, long len) /* * call-seq: - * array.slice!(n) -> object or nil - * array.slice!(start, length) -> new_array or nil - * array.slice!(range) -> new_array or nil + * slice!(index) -> object or nil + * slice!(start, length) -> new_array or nil + * slice!(range) -> new_array or nil * * Removes and returns elements from +self+. * - * When the only argument is an \Integer +n+, - * removes and returns the _nth_ element in +self+: - * a = [:foo, 'bar', 2] - * a.slice!(1) # => "bar" - * a # => [:foo, 2] + * With numeric argument +index+ given, + * removes and returns the element at offset +index+: * - * If +n+ is negative, counts backwards from the end of +self+: - * a = [:foo, 'bar', 2] - * a.slice!(-1) # => 2 - * a # => [:foo, "bar"] + * a = ['a', 'b', 'c', 'd'] + * a.slice!(2) # => "c" + * a # => ["a", "b", "d"] + * a.slice!(2.1) # => "d" + * a # => ["a", "b"] * - * If +n+ is out of range, returns +nil+. + * If +index+ is negative, counts backwards from the end of +self+: * - * When the only arguments are Integers +start+ and +length+, - * removes +length+ elements from +self+ beginning at offset +start+; - * returns the deleted objects in a new Array: - * a = [:foo, 'bar', 2] - * a.slice!(0, 2) # => [:foo, "bar"] - * a # => [2] + * a = ['a', 'b', 'c', 'd'] + * a.slice!(-2) # => "c" + * a # => ["a", "b", "d"] + * + * If +index+ is out of range, returns +nil+. + * + * With numeric arguments +start+ and +length+ given, + * removes +length+ elements from +self+ beginning at zero-based offset +start+; + * returns the removed objects in a new array: + * + * a = ['a', 'b', 'c', 'd'] + * a.slice!(1, 2) # => ["b", "c"] + * a # => ["a", "d"] + * a.slice!(0.1, 1.1) # => ["a"] + * a # => ["d"] + * + * If +start+ is negative, counts backwards from the end of +self+: + * + * a = ['a', 'b', 'c', 'd'] + * a.slice!(-2, 1) # => ["c"] + * a # => ["a", "b", "d"] + * + * If +start+ is out-of-range, returns +nil+: + * + * a = ['a', 'b', 'c', 'd'] + * a.slice!(5, 1) # => nil + * a.slice!(-5, 1) # => nil * * If <tt>start + length</tt> exceeds the array size, * removes and returns all elements from offset +start+ to the end: - * a = [:foo, 'bar', 2] - * a.slice!(1, 50) # => ["bar", 2] - * a # => [:foo] + * + * a = ['a', 'b', 'c', 'd'] + * a.slice!(2, 50) # => ["c", "d"] + * a # => ["a", "b"] * * If <tt>start == a.size</tt> and +length+ is non-negative, - * returns a new empty \Array. + * returns a new empty array. * * If +length+ is negative, returns +nil+. * - * When the only argument is a \Range object +range+, - * treats <tt>range.min</tt> as +start+ above and <tt>range.size</tt> as +length+ above: - * a = [:foo, 'bar', 2] - * a.slice!(1..2) # => ["bar", 2] - * a # => [:foo] + * With Range argument +range+ given, + * treats <tt>range.min</tt> as +start+ (as above) + * and <tt>range.size</tt> as +length+ (as above): * - * If <tt>range.start == a.size</tt>, returns a new empty \Array. + * a = ['a', 'b', 'c', 'd'] + * a.slice!(1..2) # => ["b", "c"] + * a # => ["a", "d"] * - * If <tt>range.start</tt> is larger than the array size, returns +nil+. + * If <tt>range.start == a.size</tt>, returns a new empty array: * - * If <tt>range.end</tt> is negative, counts backwards from the end of the array: - * a = [:foo, 'bar', 2] - * a.slice!(0..-2) # => [:foo, "bar"] - * a # => [2] + * a = ['a', 'b', 'c', 'd'] + * a.slice!(4..5) # => [] + * + * If <tt>range.start</tt> is larger than the array size, returns +nil+: + * + * a = ['a', 'b', 'c', 'd'] + a.slice!(5..6) # => nil * * If <tt>range.start</tt> is negative, - * calculates the start index backwards from the end of the array: - * a = [:foo, 'bar', 2] - * a.slice!(-2..2) # => ["bar", 2] - * a # => [:foo] + * calculates the start index by counting backwards from the end of +self+: + * + * a = ['a', 'b', 'c', 'd'] + * a.slice!(-2..2) # => ["c"] + * + * If <tt>range.end</tt> is negative, + * calculates the end index by counting backwards from the end of +self+: + * + * a = ['a', 'b', 'c', 'd'] + * a.slice!(0..-2) # => ["a", "b", "c"] + * + * Related: see {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ static VALUE @@ -4088,23 +4405,23 @@ rb_ary_slice_bang(int argc, VALUE *argv, VALUE ary) arg1 = argv[0]; if (argc == 2) { - pos = NUM2LONG(argv[0]); - len = NUM2LONG(argv[1]); + pos = NUM2LONG(argv[0]); + len = NUM2LONG(argv[1]); return ary_slice_bang_by_rb_ary_splice(ary, pos, len); } if (!FIXNUM_P(arg1)) { - switch (rb_range_beg_len(arg1, &pos, &len, RARRAY_LEN(ary), 0)) { - case Qtrue: - /* valid range */ + switch (rb_range_beg_len(arg1, &pos, &len, RARRAY_LEN(ary), 0)) { + case Qtrue: + /* valid range */ return ary_slice_bang_by_rb_ary_splice(ary, pos, len); - case Qnil: - /* invalid range */ - return Qnil; - default: - /* not a range */ - break; - } + case Qnil: + /* invalid range */ + return Qnil; + default: + /* not a range */ + break; + } } return rb_ary_delete_at(ary, NUM2LONG(arg1)); @@ -4116,11 +4433,11 @@ ary_reject(VALUE orig, VALUE result) long i; for (i = 0; i < RARRAY_LEN(orig); i++) { - VALUE v = RARRAY_AREF(orig, i); + VALUE v = RARRAY_AREF(orig, i); if (!RTEST(rb_yield(v))) { - rb_ary_push(result, v); - } + rb_ary_push(result, v); + } } return result; } @@ -4133,12 +4450,12 @@ reject_bang_i(VALUE a) long i1, i2; for (i1 = i2 = 0; i1 < RARRAY_LEN(ary); arg->len[0] = ++i1) { - VALUE v = RARRAY_AREF(ary, i1); - if (RTEST(rb_yield(v))) continue; - if (i1 != i2) { - rb_ary_store(ary, i2, v); - } - arg->len[1] = ++i2; + VALUE v = RARRAY_AREF(ary, i1); + if (RTEST(rb_yield(v))) continue; + if (i1 != i2) { + rb_ary_store(ary, i2, v); + } + arg->len[1] = ++i2; } return (i1 == i2) ? Qnil : ary; } @@ -4155,20 +4472,22 @@ ary_reject_bang(VALUE ary) /* * call-seq: - * array.reject! {|element| ... } -> self or nil - * array.reject! -> new_enumerator + * reject! {|element| ... } -> self or nil + * reject! -> new_enumerator * - * Removes each element for which the block returns a truthy value. + * With a block given, calls the block with each element of +self+; + * removes each element for which the block returns a truthy value. * * Returns +self+ if any elements removed: + * * a = [:foo, 'bar', 2, 'bat'] * a.reject! {|element| element.to_s.start_with?('b') } # => [:foo, 2] * * Returns +nil+ if no elements removed. * - * Returns a new \Enumerator if no block given: - * a = [:foo, 'bar', 2] - * a.reject! # => #<Enumerator: [:foo, "bar", 2]:reject!> + * With no block given, returns a new Enumerator. + * + * Related: see {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ static VALUE @@ -4181,18 +4500,19 @@ rb_ary_reject_bang(VALUE ary) /* * call-seq: - * array.reject {|element| ... } -> new_array - * array.reject -> new_enumerator + * reject {|element| ... } -> new_array + * reject -> new_enumerator * - * Returns a new \Array whose elements are all those from +self+ + * With a block given, returns a new array whose elements are all those from +self+ * for which the block returns +false+ or +nil+: + * * a = [:foo, 'bar', 2, 'bat'] * a1 = a.reject {|element| element.to_s.start_with?('b') } * a1 # => [:foo, 2] * - * Returns a new \Enumerator if no block given: - * a = [:foo, 'bar', 2] - * a.reject # => #<Enumerator: [:foo, "bar", 2]:reject> + * With no block given, returns a new Enumerator. + * + * Related: {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -4208,17 +4528,19 @@ rb_ary_reject(VALUE ary) /* * call-seq: - * array.delete_if {|element| ... } -> self - * array.delete_if -> Enumerator + * delete_if {|element| ... } -> self + * delete_if -> new_numerator * - * Removes each element in +self+ for which the block returns a truthy value; + * With a block given, calls the block with each element of +self+; + * removes the element if the block returns a truthy value; * returns +self+: + * * a = [:foo, 'bar', 2, 'bat'] * a.delete_if {|element| element.to_s.start_with?('b') } # => [:foo, 2] * - * Returns a new \Enumerator if no block given: - * a = [:foo, 'bar', 2] - * a.delete_if # => #<Enumerator: [:foo, "bar", 2]:delete_if> + * With no block given, returns a new Enumerator. + * + * Related: see {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ static VALUE @@ -4250,60 +4572,104 @@ take_items(VALUE obj, long n) if (!NIL_P(result)) return rb_ary_subseq(result, 0, n); result = rb_ary_new2(n); args[0] = result; args[1] = (VALUE)n; - if (rb_check_block_call(obj, idEach, 0, 0, take_i, (VALUE)args) == Qundef) - rb_raise(rb_eTypeError, "wrong argument type %"PRIsVALUE" (must respond to :each)", - rb_obj_class(obj)); + if (UNDEF_P(rb_check_block_call(obj, idEach, 0, 0, take_i, (VALUE)args))) + rb_raise(rb_eTypeError, "wrong argument type %"PRIsVALUE" (must respond to :each)", + rb_obj_class(obj)); return result; } /* * call-seq: - * array.zip(*other_arrays) -> new_array - * array.zip(*other_arrays) {|other_array| ... } -> nil + * zip(*other_arrays) -> new_array + * zip(*other_arrays) {|sub_array| ... } -> nil * - * When no block given, returns a new \Array +new_array+ of size <tt>self.size</tt> - * whose elements are Arrays. + * With no block given, combines +self+ with the collection of +other_arrays+; + * returns a new array of sub-arrays: * - * Each nested array <tt>new_array[n]</tt> is of size <tt>other_arrays.size+1</tt>, - * and contains: - * - The _nth_ element of +self+. - * - The _nth_ element of each of the +other_arrays+. + * [0, 1].zip(['zero', 'one'], [:zero, :one]) + * # => [[0, "zero", :zero], [1, "one", :one]] + * + * Returned: + * + * - The outer array is of size <tt>self.size</tt>. + * - Each sub-array is of size <tt>other_arrays.size + 1</tt>. + * - The _nth_ sub-array contains (in order): + * + * - The _nth_ element of +self+. + * - The _nth_ element of each of the other arrays, as available. + * + * Example: + * + * a = [0, 1] + * zipped = a.zip(['zero', 'one'], [:zero, :one]) + * # => [[0, "zero", :zero], [1, "one", :one]] + * zipped.size # => 2 # Same size as a. + * zipped.first.size # => 3 # Size of other arrays plus 1. + * + * When the other arrays are all the same size as +self+, + * the returned sub-arrays are a rearrangement containing exactly elements of all the arrays + * (including +self+), with no omissions or additions: * - * If all +other_arrays+ and +self+ are the same size: * a = [:a0, :a1, :a2, :a3] * b = [:b0, :b1, :b2, :b3] * c = [:c0, :c1, :c2, :c3] * d = a.zip(b, c) - * d # => [[:a0, :b0, :c0], [:a1, :b1, :c1], [:a2, :b2, :c2], [:a3, :b3, :c3]] + * pp d + * # => + * [[:a0, :b0, :c0], + * [:a1, :b1, :c1], + * [:a2, :b2, :c2], + * [:a3, :b3, :c3]] + * + * When one of the other arrays is smaller than +self+, + * pads the corresponding sub-array with +nil+ elements: * - * If any array in +other_arrays+ is smaller than +self+, - * fills to <tt>self.size</tt> with +nil+: * a = [:a0, :a1, :a2, :a3] * b = [:b0, :b1, :b2] * c = [:c0, :c1] * d = a.zip(b, c) - * d # => [[:a0, :b0, :c0], [:a1, :b1, :c1], [:a2, :b2, nil], [:a3, nil, nil]] + * pp d + * # => + * [[:a0, :b0, :c0], + * [:a1, :b1, :c1], + * [:a2, :b2, nil], + * [:a3, nil, nil]] + * + * When one of the other arrays is larger than +self+, + * _ignores_ its trailing elements: * - * If any array in +other_arrays+ is larger than +self+, - * its trailing elements are ignored: * a = [:a0, :a1, :a2, :a3] * b = [:b0, :b1, :b2, :b3, :b4] * c = [:c0, :c1, :c2, :c3, :c4, :c5] * d = a.zip(b, c) - * d # => [[:a0, :b0, :c0], [:a1, :b1, :c1], [:a2, :b2, :c2], [:a3, :b3, :c3]] + * pp d + * # => + * [[:a0, :b0, :c0], + * [:a1, :b1, :c1], + * [:a2, :b2, :c2], + * [:a3, :b3, :c3]] * - * When a block is given, calls the block with each of the sub-arrays (formed as above); returns nil + * With a block given, calls the block with each of the other arrays; + * returns +nil+: + * + * d = [] * a = [:a0, :a1, :a2, :a3] * b = [:b0, :b1, :b2, :b3] * c = [:c0, :c1, :c2, :c3] - * a.zip(b, c) {|sub_array| p sub_array} # => nil - * - * Output: - * [:a0, :b0, :c0] - * [:a1, :b1, :c1] - * [:a2, :b2, :c2] - * [:a3, :b3, :c3] + * a.zip(b, c) {|sub_array| d.push(sub_array.reverse) } # => nil + * pp d + * # => + * [[:c0, :b0, :a0], + * [:c1, :b1, :a1], + * [:c2, :b2, :a2], + * [:c3, :b3, :a3]] + * + * For an *object* in *other_arrays* that is not actually an array, + * forms the "other array" as <tt>object.to_ary</tt>, if defined, + * or as <tt>object.each.to_a</tt> otherwise. + * + * Related: see {Methods for Converting}[rdoc-ref:Array@Methods+for+Converting]. */ static VALUE @@ -4314,51 +4680,51 @@ rb_ary_zip(int argc, VALUE *argv, VALUE ary) VALUE result = Qnil; for (i=0; i<argc; i++) { - argv[i] = take_items(argv[i], len); + argv[i] = take_items(argv[i], len); } if (rb_block_given_p()) { - int arity = rb_block_arity(); - - if (arity > 1) { - VALUE work, *tmp; - - tmp = ALLOCV_N(VALUE, work, argc+1); - - for (i=0; i<RARRAY_LEN(ary); i++) { - tmp[0] = RARRAY_AREF(ary, i); - for (j=0; j<argc; j++) { - tmp[j+1] = rb_ary_elt(argv[j], i); - } - rb_yield_values2(argc+1, tmp); - } - - if (work) ALLOCV_END(work); - } - else { - for (i=0; i<RARRAY_LEN(ary); i++) { - VALUE tmp = rb_ary_new2(argc+1); - - rb_ary_push(tmp, RARRAY_AREF(ary, i)); - for (j=0; j<argc; j++) { - rb_ary_push(tmp, rb_ary_elt(argv[j], i)); - } - rb_yield(tmp); - } - } + int arity = rb_block_arity(); + + if (arity > 1) { + VALUE work, *tmp; + + tmp = ALLOCV_N(VALUE, work, argc+1); + + for (i=0; i<RARRAY_LEN(ary); i++) { + tmp[0] = RARRAY_AREF(ary, i); + for (j=0; j<argc; j++) { + tmp[j+1] = rb_ary_elt(argv[j], i); + } + rb_yield_values2(argc+1, tmp); + } + + if (work) ALLOCV_END(work); + } + else { + for (i=0; i<RARRAY_LEN(ary); i++) { + VALUE tmp = rb_ary_new2(argc+1); + + rb_ary_push(tmp, RARRAY_AREF(ary, i)); + for (j=0; j<argc; j++) { + rb_ary_push(tmp, rb_ary_elt(argv[j], i)); + } + rb_yield(tmp); + } + } } else { - result = rb_ary_new_capa(len); + result = rb_ary_new_capa(len); - for (i=0; i<len; i++) { - VALUE tmp = rb_ary_new_capa(argc+1); + for (i=0; i<len; i++) { + VALUE tmp = rb_ary_new_capa(argc+1); - rb_ary_push(tmp, RARRAY_AREF(ary, i)); - for (j=0; j<argc; j++) { - rb_ary_push(tmp, rb_ary_elt(argv[j], i)); - } - rb_ary_push(result, tmp); - } + rb_ary_push(tmp, RARRAY_AREF(ary, i)); + for (j=0; j<argc; j++) { + rb_ary_push(tmp, rb_ary_elt(argv[j], i)); + } + rb_ary_push(result, tmp); + } } return result; @@ -4366,12 +4732,17 @@ rb_ary_zip(int argc, VALUE *argv, VALUE ary) /* * call-seq: - * array.transpose -> new_array + * transpose -> new_array + * + * Returns a new array that is +self+ + * as a {transposed matrix}[https://en.wikipedia.org/wiki/Transpose]: * - * Transposes the rows and columns in an \Array of Arrays; - * the nested Arrays must all be the same size: * a = [[:a0, :a1], [:b0, :b1], [:c0, :c1]] * a.transpose # => [[:a0, :b0, :c0], [:a1, :b1, :c1]] + * + * The elements of +self+ must all be the same size. + * + * Related: see {Methods for Converting}[rdoc-ref:Array@Methods+for+Converting]. */ static VALUE @@ -4383,32 +4754,38 @@ rb_ary_transpose(VALUE ary) alen = RARRAY_LEN(ary); if (alen == 0) return rb_ary_dup(ary); for (i=0; i<alen; i++) { - tmp = to_ary(rb_ary_elt(ary, i)); - if (elen < 0) { /* first element */ - elen = RARRAY_LEN(tmp); - result = rb_ary_new2(elen); - for (j=0; j<elen; j++) { - rb_ary_store(result, j, rb_ary_new2(alen)); - } - } - else if (elen != RARRAY_LEN(tmp)) { - rb_raise(rb_eIndexError, "element size differs (%ld should be %ld)", - RARRAY_LEN(tmp), elen); - } - for (j=0; j<elen; j++) { - rb_ary_store(rb_ary_elt(result, j), i, rb_ary_elt(tmp, j)); - } + tmp = to_ary(rb_ary_elt(ary, i)); + if (elen < 0) { /* first element */ + elen = RARRAY_LEN(tmp); + result = rb_ary_new2(elen); + for (j=0; j<elen; j++) { + rb_ary_store(result, j, rb_ary_new2(alen)); + } + } + else if (elen != RARRAY_LEN(tmp)) { + rb_raise(rb_eIndexError, "element size differs (%ld should be %ld)", + RARRAY_LEN(tmp), elen); + } + for (j=0; j<elen; j++) { + rb_ary_store(rb_ary_elt(result, j), i, rb_ary_elt(tmp, j)); + } } return result; } /* * call-seq: - * array.replace(other_array) -> self + * initialize_copy(other_array) -> self + * replace(other_array) -> self * - * Replaces the content of +self+ with the content of +other_array+; returns +self+: - * a = [:foo, 'bar', 2] - * a.replace(['foo', :bar, 3]) # => ["foo", :bar, 3] + * Replaces the elements of +self+ with the elements of +other_array+, which must be an + * {array-convertible object}[rdoc-ref:implicit_conversion.rdoc@Array-Convertible+Objects]; + * returns +self+: + * + * a = ['a', 'b', 'c'] # => ["a", "b", "c"] + * a.replace(['d', 'e']) # => ["d", "e"] + * + * Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ VALUE @@ -4418,35 +4795,39 @@ rb_ary_replace(VALUE copy, VALUE orig) orig = to_ary(orig); if (copy == orig) return copy; - if (RARRAY_LEN(orig) <= RARRAY_EMBED_LEN_MAX) { - VALUE shared_root = 0; + rb_ary_reset(copy); - if (ARY_OWNS_HEAP_P(copy)) { - ary_heap_free(copy); - } - else if (ARY_SHARED_P(copy)) { - shared_root = ARY_SHARED_ROOT(copy); - FL_UNSET_SHARED(copy); - } - FL_SET_EMBED(copy); - ary_memcpy(copy, 0, RARRAY_LEN(orig), RARRAY_CONST_PTR_TRANSIENT(orig)); - if (shared_root) { - rb_ary_decrement_share(shared_root); - } - ARY_SET_LEN(copy, RARRAY_LEN(orig)); + /* orig has enough space to embed the contents of orig. */ + if (RARRAY_LEN(orig) <= ary_embed_capa(copy)) { + RUBY_ASSERT(ARY_EMBED_P(copy)); + ary_memcpy(copy, 0, RARRAY_LEN(orig), RARRAY_CONST_PTR(orig)); + ARY_SET_EMBED_LEN(copy, RARRAY_LEN(orig)); + } + /* orig is embedded but copy does not have enough space to embed the + * contents of orig. */ + else if (ARY_EMBED_P(orig)) { + long len = ARY_EMBED_LEN(orig); + VALUE *ptr = ary_heap_alloc_buffer(len); + + FL_UNSET_EMBED(copy); + ARY_SET_PTR(copy, ptr); + ARY_SET_LEN(copy, len); + ARY_SET_CAPA(copy, len); + + // No allocation and exception expected that could leave `copy` in a + // bad state from the edits above. + ary_memcpy(copy, 0, len, RARRAY_CONST_PTR(orig)); } + /* Otherwise, orig is on heap and copy does not have enough space to embed + * the contents of orig. */ else { VALUE shared_root = ary_make_shared(orig); - if (ARY_OWNS_HEAP_P(copy)) { - ary_heap_free(copy); - } - else { - rb_ary_unshare_safe(copy); - } FL_UNSET_EMBED(copy); ARY_SET_PTR(copy, ARY_HEAP_PTR(orig)); ARY_SET_LEN(copy, ARY_HEAP_LEN(orig)); rb_ary_set_shared(copy, shared_root); + + RUBY_ASSERT(RB_OBJ_SHAREABLE_P(copy) ? RB_OBJ_SHAREABLE_P(shared_root) : 1); } ary_verify(copy); return copy; @@ -4454,11 +4835,14 @@ rb_ary_replace(VALUE copy, VALUE orig) /* * call-seq: - * array.clear -> self + * clear -> self + * + * Removes all elements from +self+; returns +self+: * - * Removes all elements from +self+: * a = [:foo, 'bar', 2] * a.clear # => [] + * + * Related: see {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ VALUE @@ -4466,11 +4850,9 @@ rb_ary_clear(VALUE ary) { rb_ary_modify_check(ary); if (ARY_SHARED_P(ary)) { - if (!ARY_EMBED_P(ary)) { - rb_ary_unshare(ary); - FL_SET_EMBED(ary); - ARY_SET_EMBED_LEN(ary, 0); - } + rb_ary_unshare(ary); + FL_SET_EMBED(ary); + ARY_SET_EMBED_LEN(ary, 0); } else { ARY_SET_LEN(ary, 0); @@ -4484,171 +4866,182 @@ rb_ary_clear(VALUE ary) /* * call-seq: - * array.fill(obj) -> self - * array.fill(obj, start) -> self - * array.fill(obj, start, length) -> self - * array.fill(obj, range) -> self - * array.fill {|index| ... } -> self - * array.fill(start) {|index| ... } -> self - * array.fill(start, length) {|index| ... } -> self - * array.fill(range) {|index| ... } -> self - * - * Replaces specified elements in +self+ with specified objects; returns +self+. - * - * With argument +obj+ and no block given, replaces all elements with that one object: - * a = ['a', 'b', 'c', 'd'] - * a # => ["a", "b", "c", "d"] - * a.fill(:X) # => [:X, :X, :X, :X] + * fill(object, start = nil, count = nil) -> self + * fill(object, range) -> self + * fill(start = nil, count = nil) {|element| ... } -> self + * fill(range) {|element| ... } -> self * - * With arguments +obj+ and \Integer +start+, and no block given, - * replaces elements based on the given start. + * Replaces selected elements in +self+; + * may add elements to +self+; + * always returns +self+ (never a new array). * - * If +start+ is in range (<tt>0 <= start < array.size</tt>), - * replaces all elements from offset +start+ through the end: - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, 2) # => ["a", "b", :X, :X] + * In brief: * - * If +start+ is too large (<tt>start >= array.size</tt>), does nothing: - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, 4) # => ["a", "b", "c", "d"] - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, 5) # => ["a", "b", "c", "d"] + * # Non-negative start. + * ['a', 'b', 'c', 'd'].fill('-', 1, 2) # => ["a", "-", "-", "d"] + * ['a', 'b', 'c', 'd'].fill(1, 2) {|e| e.to_s } # => ["a", "1", "2", "d"] * - * If +start+ is negative, counts from the end (starting index is <tt>start + array.size</tt>): - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, -2) # => ["a", "b", :X, :X] + * # Extends with specified values if necessary. + * ['a', 'b', 'c', 'd'].fill('-', 3, 2) # => ["a", "b", "c", "-", "-"] + * ['a', 'b', 'c', 'd'].fill(3, 2) {|e| e.to_s } # => ["a", "b", "c", "3", "4"] * - * If +start+ is too small (less than and far from zero), replaces all elements: - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, -6) # => [:X, :X, :X, :X] - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, -50) # => [:X, :X, :X, :X] + * # Fills with nils if necessary. + * ['a', 'b', 'c', 'd'].fill('-', 6, 2) # => ["a", "b", "c", "d", nil, nil, "-", "-"] + * ['a', 'b', 'c', 'd'].fill(6, 2) {|e| e.to_s } # => ["a", "b", "c", "d", nil, nil, "6", "7"] * - * With arguments +obj+, \Integer +start+, and \Integer +length+, and no block given, - * replaces elements based on the given +start+ and +length+. + * # For negative start, counts backwards from the end. + * ['a', 'b', 'c', 'd'].fill('-', -3, 3) # => ["a", "-", "-", "-"] + * ['a', 'b', 'c', 'd'].fill(-3, 3) {|e| e.to_s } # => ["a", "1", "2", "3"] * - * If +start+ is in range, replaces +length+ elements beginning at offset +start+: - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, 1, 1) # => ["a", :X, "c", "d"] + * # Range. + * ['a', 'b', 'c', 'd'].fill('-', 1..2) # => ["a", "-", "-", "d"] + * ['a', 'b', 'c', 'd'].fill(1..2) {|e| e.to_s } # => ["a", "1", "2", "d"] * - * If +start+ is negative, counts from the end: - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, -2, 1) # => ["a", "b", :X, "d"] + * When arguments +start+ and +count+ are given, + * they select the elements of +self+ to be replaced; + * each must be an + * {integer-convertible object}[rdoc-ref:implicit_conversion.rdoc@Integer-Convertible+Objects] + * (or +nil+): * - * If +start+ is large (<tt>start >= array.size</tt>), extends +self+ with +nil+: - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, 5, 0) # => ["a", "b", "c", "d", nil] - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, 5, 2) # => ["a", "b", "c", "d", nil, :X, :X] + * - +start+ specifies the zero-based offset of the first element to be replaced; + * +nil+ means zero. + * - +count+ is the number of consecutive elements to be replaced; + * +nil+ means "all the rest." * - * If +length+ is zero or negative, replaces no elements: - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, 1, 0) # => ["a", "b", "c", "d"] - * a.fill(:X, 1, -1) # => ["a", "b", "c", "d"] + * With argument +object+ given, + * that one object is used for all replacements: * - * With arguments +obj+ and \Range +range+, and no block given, - * replaces elements based on the given range. + * o = Object.new # => #<Object:0x0000014e7bff7600> + * a = ['a', 'b', 'c', 'd'] # => ["a", "b", "c", "d"] + * a.fill(o, 1, 2) + * # => ["a", #<Object:0x0000014e7bff7600>, #<Object:0x0000014e7bff7600>, "d"] * - * If the range is positive and ascending (<tt>0 < range.begin <= range.end</tt>), - * replaces elements from <tt>range.begin</tt> to <tt>range.end</tt>: - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, (1..1)) # => ["a", :X, "c", "d"] + * With a block given, the block is called once for each element to be replaced; + * the value passed to the block is the _index_ of the element to be replaced + * (not the element itself); + * the block's return value replaces the element: * - * If <tt>range.first</tt> is negative, replaces no elements: - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, (-1..1)) # => ["a", "b", "c", "d"] + * a = ['a', 'b', 'c', 'd'] # => ["a", "b", "c", "d"] + * a.fill(1, 2) {|element| element.to_s } # => ["a", "1", "2", "d"] * - * If <tt>range.last</tt> is negative, counts from the end: - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, (0..-2)) # => [:X, :X, :X, "d"] - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, (1..-2)) # => ["a", :X, :X, "d"] + * For arguments +start+ and +count+: * - * If <tt>range.last</tt> and <tt>range.last</tt> are both negative, - * both count from the end of the array: - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, (-1..-1)) # => ["a", "b", "c", :X] - * a = ['a', 'b', 'c', 'd'] - * a.fill(:X, (-2..-2)) # => ["a", "b", :X, "d"] + * - If +start+ is non-negative, + * replaces +count+ elements beginning at offset +start+: * - * With no arguments and a block given, calls the block with each index; - * replaces the corresponding element with the block's return value: - * a = ['a', 'b', 'c', 'd'] - * a.fill { |index| "new_#{index}" } # => ["new_0", "new_1", "new_2", "new_3"] + * ['a', 'b', 'c', 'd'].fill('-', 0, 2) # => ["-", "-", "c", "d"] + * ['a', 'b', 'c', 'd'].fill('-', 1, 2) # => ["a", "-", "-", "d"] + * ['a', 'b', 'c', 'd'].fill('-', 2, 2) # => ["a", "b", "-", "-"] * - * With argument +start+ and a block given, calls the block with each index - * from offset +start+ to the end; replaces the corresponding element - * with the block's return value: + * ['a', 'b', 'c', 'd'].fill(0, 2) {|e| e.to_s } # => ["0", "1", "c", "d"] + * ['a', 'b', 'c', 'd'].fill(1, 2) {|e| e.to_s } # => ["a", "1", "2", "d"] + * ['a', 'b', 'c', 'd'].fill(2, 2) {|e| e.to_s } # => ["a", "b", "2", "3"] * - * If start is in range (<tt>0 <= start < array.size</tt>), - * replaces from offset +start+ to the end: - * a = ['a', 'b', 'c', 'd'] - * a.fill(1) { |index| "new_#{index}" } # => ["a", "new_1", "new_2", "new_3"] + * Extends +self+ if necessary: * - * If +start+ is too large(<tt>start >= array.size</tt>), does nothing: - * a = ['a', 'b', 'c', 'd'] - * a.fill(4) { |index| fail 'Cannot happen' } # => ["a", "b", "c", "d"] - * a = ['a', 'b', 'c', 'd'] - * a.fill(4) { |index| fail 'Cannot happen' } # => ["a", "b", "c", "d"] + * ['a', 'b', 'c', 'd'].fill('-', 3, 2) # => ["a", "b", "c", "-", "-"] + * ['a', 'b', 'c', 'd'].fill('-', 4, 2) # => ["a", "b", "c", "d", "-", "-"] * - * If +start+ is negative, counts from the end: - * a = ['a', 'b', 'c', 'd'] - * a.fill(-2) { |index| "new_#{index}" } # => ["a", "b", "new_2", "new_3"] + * ['a', 'b', 'c', 'd'].fill(3, 2) {|e| e.to_s } # => ["a", "b", "c", "3", "4"] + * ['a', 'b', 'c', 'd'].fill(4, 2) {|e| e.to_s } # => ["a", "b", "c", "d", "4", "5"] * - * If start is too small (<tt>start <= -array.size</tt>, replaces all elements: - * a = ['a', 'b', 'c', 'd'] - * a.fill(-6) { |index| "new_#{index}" } # => ["new_0", "new_1", "new_2", "new_3"] - * a = ['a', 'b', 'c', 'd'] - * a.fill(-50) { |index| "new_#{index}" } # => ["new_0", "new_1", "new_2", "new_3"] + * Fills with +nil+ if necessary: * - * With arguments +start+ and +length+, and a block given, - * calls the block for each index specified by start length; - * replaces the corresponding element with the block's return value. + * ['a', 'b', 'c', 'd'].fill('-', 5, 2) # => ["a", "b", "c", "d", nil, "-", "-"] + * ['a', 'b', 'c', 'd'].fill('-', 6, 2) # => ["a", "b", "c", "d", nil, nil, "-", "-"] * - * If +start+ is in range, replaces +length+ elements beginning at offset +start+: - * a = ['a', 'b', 'c', 'd'] - * a.fill(1, 1) { |index| "new_#{index}" } # => ["a", "new_1", "c", "d"] + * ['a', 'b', 'c', 'd'].fill(5, 2) {|e| e.to_s } # => ["a", "b", "c", "d", nil, "5", "6"] + * ['a', 'b', 'c', 'd'].fill(6, 2) {|e| e.to_s } # => ["a", "b", "c", "d", nil, nil, "6", "7"] * - * If start is negative, counts from the end: - * a = ['a', 'b', 'c', 'd'] - * a.fill(-2, 1) { |index| "new_#{index}" } # => ["a", "b", "new_2", "d"] + * Does nothing if +count+ is non-positive: * - * If +start+ is large (<tt>start >= array.size</tt>), extends +self+ with +nil+: - * a = ['a', 'b', 'c', 'd'] - * a.fill(5, 0) { |index| "new_#{index}" } # => ["a", "b", "c", "d", nil] - * a = ['a', 'b', 'c', 'd'] - * a.fill(5, 2) { |index| "new_#{index}" } # => ["a", "b", "c", "d", nil, "new_5", "new_6"] + * ['a', 'b', 'c', 'd'].fill('-', 2, 0) # => ["a", "b", "c", "d"] + * ['a', 'b', 'c', 'd'].fill('-', 2, -100) # => ["a", "b", "c", "d"] + * ['a', 'b', 'c', 'd'].fill('-', 6, -100) # => ["a", "b", "c", "d"] * - * If +length+ is zero or less, replaces no elements: - * a = ['a', 'b', 'c', 'd'] - * a.fill(1, 0) { |index| "new_#{index}" } # => ["a", "b", "c", "d"] - * a.fill(1, -1) { |index| "new_#{index}" } # => ["a", "b", "c", "d"] + * ['a', 'b', 'c', 'd'].fill(2, 0) {|e| fail 'Cannot happen' } # => ["a", "b", "c", "d"] + * ['a', 'b', 'c', 'd'].fill(2, -100) {|e| fail 'Cannot happen' } # => ["a", "b", "c", "d"] + * ['a', 'b', 'c', 'd'].fill(6, -100) {|e| fail 'Cannot happen' } # => ["a", "b", "c", "d"] * - * With arguments +obj+ and +range+, and a block given, - * calls the block with each index in the given range; - * replaces the corresponding element with the block's return value. + * - If +start+ is negative, counts backwards from the end of +self+: * - * If the range is positive and ascending (<tt>range 0 < range.begin <= range.end</tt>, - * replaces elements from <tt>range.begin</tt> to <tt>range.end</tt>: - * a = ['a', 'b', 'c', 'd'] - * a.fill(1..1) { |index| "new_#{index}" } # => ["a", "new_1", "c", "d"] + * ['a', 'b', 'c', 'd'].fill('-', -4, 3) # => ["-", "-", "-", "d"] + * ['a', 'b', 'c', 'd'].fill('-', -3, 3) # => ["a", "-", "-", "-"] * - * If +range.first+ is negative, does nothing: - * a = ['a', 'b', 'c', 'd'] - * a.fill(-1..1) { |index| fail 'Cannot happen' } # => ["a", "b", "c", "d"] + * ['a', 'b', 'c', 'd'].fill(-4, 3) {|e| e.to_s } # => ["0", "1", "2", "d"] + * ['a', 'b', 'c', 'd'].fill(-3, 3) {|e| e.to_s } # => ["a", "1", "2", "3"] * - * If <tt>range.last</tt> is negative, counts from the end: - * a = ['a', 'b', 'c', 'd'] - * a.fill(0..-2) { |index| "new_#{index}" } # => ["new_0", "new_1", "new_2", "d"] - * a = ['a', 'b', 'c', 'd'] - * a.fill(1..-2) { |index| "new_#{index}" } # => ["a", "new_1", "new_2", "d"] + * Extends +self+ if necessary: * - * If <tt>range.first</tt> and <tt>range.last</tt> are both negative, - * both count from the end: - * a = ['a', 'b', 'c', 'd'] - * a.fill(-1..-1) { |index| "new_#{index}" } # => ["a", "b", "c", "new_3"] - * a = ['a', 'b', 'c', 'd'] - * a.fill(-2..-2) { |index| "new_#{index}" } # => ["a", "b", "new_2", "d"] + * ['a', 'b', 'c', 'd'].fill('-', -2, 3) # => ["a", "b", "-", "-", "-"] + * ['a', 'b', 'c', 'd'].fill('-', -1, 3) # => ["a", "b", "c", "-", "-", "-"] + * + * ['a', 'b', 'c', 'd'].fill(-2, 3) {|e| e.to_s } # => ["a", "b", "2", "3", "4"] + * ['a', 'b', 'c', 'd'].fill(-1, 3) {|e| e.to_s } # => ["a", "b", "c", "3", "4", "5"] + * + * Starts at the beginning of +self+ if +start+ is negative and out-of-range: + * + * ['a', 'b', 'c', 'd'].fill('-', -5, 2) # => ["-", "-", "c", "d"] + * ['a', 'b', 'c', 'd'].fill('-', -6, 2) # => ["-", "-", "c", "d"] + * + * ['a', 'b', 'c', 'd'].fill(-5, 2) {|e| e.to_s } # => ["0", "1", "c", "d"] + * ['a', 'b', 'c', 'd'].fill(-6, 2) {|e| e.to_s } # => ["0", "1", "c", "d"] + * + * Does nothing if +count+ is non-positive: + * + * ['a', 'b', 'c', 'd'].fill('-', -2, 0) # => ["a", "b", "c", "d"] + * ['a', 'b', 'c', 'd'].fill('-', -2, -1) # => ["a", "b", "c", "d"] + * + * ['a', 'b', 'c', 'd'].fill(-2, 0) {|e| fail 'Cannot happen' } # => ["a", "b", "c", "d"] + * ['a', 'b', 'c', 'd'].fill(-2, -1) {|e| fail 'Cannot happen' } # => ["a", "b", "c", "d"] + * + * When argument +range+ is given, + * it must be a Range object whose members are numeric; + * its +begin+ and +end+ values determine the elements of +self+ + * to be replaced: + * + * - If both +begin+ and +end+ are positive, they specify the first and last elements + * to be replaced: + * + * ['a', 'b', 'c', 'd'].fill('-', 1..2) # => ["a", "-", "-", "d"] + * ['a', 'b', 'c', 'd'].fill(1..2) {|e| e.to_s } # => ["a", "1", "2", "d"] + * + * If +end+ is smaller than +begin+, replaces no elements: + * + * ['a', 'b', 'c', 'd'].fill('-', 2..1) # => ["a", "b", "c", "d"] + * ['a', 'b', 'c', 'd'].fill(2..1) {|e| e.to_s } # => ["a", "b", "c", "d"] + * + * - If either is negative (or both are negative), counts backwards from the end of +self+: + * + * ['a', 'b', 'c', 'd'].fill('-', -3..2) # => ["a", "-", "-", "d"] + * ['a', 'b', 'c', 'd'].fill('-', 1..-2) # => ["a", "-", "-", "d"] + * ['a', 'b', 'c', 'd'].fill('-', -3..-2) # => ["a", "-", "-", "d"] + * + * ['a', 'b', 'c', 'd'].fill(-3..2) {|e| e.to_s } # => ["a", "1", "2", "d"] + * ['a', 'b', 'c', 'd'].fill(1..-2) {|e| e.to_s } # => ["a", "1", "2", "d"] + * ['a', 'b', 'c', 'd'].fill(-3..-2) {|e| e.to_s } # => ["a", "1", "2", "d"] + * + * - If the +end+ value is excluded (see Range#exclude_end?), omits the last replacement: + * + * ['a', 'b', 'c', 'd'].fill('-', 1...2) # => ["a", "-", "c", "d"] + * ['a', 'b', 'c', 'd'].fill('-', 1...-2) # => ["a", "-", "c", "d"] + * + * ['a', 'b', 'c', 'd'].fill(1...2) {|e| e.to_s } # => ["a", "1", "c", "d"] + * ['a', 'b', 'c', 'd'].fill(1...-2) {|e| e.to_s } # => ["a", "1", "c", "d"] + * + * - If the range is endless (see {Endless Ranges}[rdoc-ref:Range@Endless+Ranges]), + * replaces elements to the end of +self+: + * + * ['a', 'b', 'c', 'd'].fill('-', 1..) # => ["a", "-", "-", "-"] + * ['a', 'b', 'c', 'd'].fill(1..) {|e| e.to_s } # => ["a", "1", "2", "3"] + * + * - If the range is beginless (see {Beginless Ranges}[rdoc-ref:Range@Beginless+Ranges]), + * replaces elements from the beginning of +self+: + * + * ['a', 'b', 'c', 'd'].fill('-', ..2) # => ["-", "-", "-", "d"] + * ['a', 'b', 'c', 'd'].fill(..2) {|e| e.to_s } # => ["0", "1", "2", "d"] + * + * Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ static VALUE @@ -4658,73 +5051,74 @@ rb_ary_fill(int argc, VALUE *argv, VALUE ary) long beg = 0, end = 0, len = 0; if (rb_block_given_p()) { - rb_scan_args(argc, argv, "02", &arg1, &arg2); - argc += 1; /* hackish */ + rb_scan_args(argc, argv, "02", &arg1, &arg2); + argc += 1; /* hackish */ } else { - rb_scan_args(argc, argv, "12", &item, &arg1, &arg2); + rb_scan_args(argc, argv, "12", &item, &arg1, &arg2); } switch (argc) { case 1: - beg = 0; - len = RARRAY_LEN(ary); - break; + beg = 0; + len = RARRAY_LEN(ary); + break; case 2: - if (rb_range_beg_len(arg1, &beg, &len, RARRAY_LEN(ary), 1)) { - break; - } - /* fall through */ + if (rb_range_beg_len(arg1, &beg, &len, RARRAY_LEN(ary), 1)) { + break; + } + /* fall through */ case 3: - beg = NIL_P(arg1) ? 0 : NUM2LONG(arg1); - if (beg < 0) { - beg = RARRAY_LEN(ary) + beg; - if (beg < 0) beg = 0; - } - len = NIL_P(arg2) ? RARRAY_LEN(ary) - beg : NUM2LONG(arg2); - break; + beg = NIL_P(arg1) ? 0 : NUM2LONG(arg1); + if (beg < 0) { + beg = RARRAY_LEN(ary) + beg; + if (beg < 0) beg = 0; + } + len = NIL_P(arg2) ? RARRAY_LEN(ary) - beg : NUM2LONG(arg2); + break; } rb_ary_modify(ary); if (len < 0) { return ary; } if (beg >= ARY_MAX_SIZE || len > ARY_MAX_SIZE - beg) { - rb_raise(rb_eArgError, "argument too big"); + rb_raise(rb_eArgError, "argument too big"); } end = beg + len; if (RARRAY_LEN(ary) < end) { - if (end >= ARY_CAPA(ary)) { - ary_resize_capa(ary, end); - } - ary_mem_clear(ary, RARRAY_LEN(ary), end - RARRAY_LEN(ary)); - ARY_SET_LEN(ary, end); + if (end >= ARY_CAPA(ary)) { + ary_resize_capa(ary, end); + } + ary_mem_clear(ary, RARRAY_LEN(ary), end - RARRAY_LEN(ary)); + ARY_SET_LEN(ary, end); } - if (item == Qundef) { - VALUE v; - long i; + if (UNDEF_P(item)) { + VALUE v; + long i; - for (i=beg; i<end; i++) { - v = rb_yield(LONG2NUM(i)); - if (i>=RARRAY_LEN(ary)) break; - ARY_SET(ary, i, v); - } + for (i=beg; i<end; i++) { + v = rb_yield(LONG2NUM(i)); + if (i>=RARRAY_LEN(ary)) break; + ARY_SET(ary, i, v); + } } else { - ary_memfill(ary, beg, len, item); + ary_memfill(ary, beg, len, item); } return ary; } /* * call-seq: - * array + other_array -> new_array + * self + other_array -> new_array * - * Returns a new \Array containing all elements of +array+ + * Returns a new array containing all elements of +self+ * followed by all elements of +other_array+: + * * a = [0, 1] + [2, 3] * a # => [0, 1, 2, 3] * - * Related: #concat. + * Related: see {Methods for Combining}[rdoc-ref:Array@Methods+for+Combining]. */ VALUE @@ -4739,8 +5133,8 @@ rb_ary_plus(VALUE x, VALUE y) len = xlen + ylen; z = rb_ary_new2(len); - ary_memcpy(z, 0, xlen, RARRAY_CONST_PTR_TRANSIENT(x)); - ary_memcpy(z, xlen, ylen, RARRAY_CONST_PTR_TRANSIENT(y)); + ary_memcpy(z, 0, xlen, RARRAY_CONST_PTR(x)); + ary_memcpy(z, xlen, ylen, RARRAY_CONST_PTR(y)); ARY_SET_LEN(z, len); return z; } @@ -4750,7 +5144,7 @@ ary_append(VALUE x, VALUE y) { long n = RARRAY_LEN(y); if (n > 0) { - rb_ary_splice(x, RARRAY_LEN(x), 0, RARRAY_CONST_PTR_TRANSIENT(y), n); + rb_ary_splice(x, RARRAY_LEN(x), 0, RARRAY_CONST_PTR(y), n); } RB_GC_GUARD(y); return x; @@ -4758,11 +5152,15 @@ ary_append(VALUE x, VALUE y) /* * call-seq: - * array.concat(*other_arrays) -> self + * concat(*other_arrays) -> self + * + * Adds to +self+ all elements from each array in +other_arrays+; returns +self+: * - * Adds to +array+ all elements from each \Array in +other_arrays+; returns +self+: * a = [0, 1] - * a.concat([2, 3], [4, 5]) # => [0, 1, 2, 3, 4, 5] + * a.concat(['two', 'three'], [:four, :five], a) + * # => [0, 1, "two", "three", :four, :five, 0, 1] + * + * Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ static VALUE @@ -4771,15 +5169,15 @@ rb_ary_concat_multi(int argc, VALUE *argv, VALUE ary) rb_ary_modify_check(ary); if (argc == 1) { - rb_ary_concat(ary, argv[0]); + rb_ary_concat(ary, argv[0]); } else if (argc > 1) { - int i; - VALUE args = rb_ary_tmp_new(argc); - for (i = 0; i < argc; i++) { - rb_ary_concat(args, argv[i]); - } - ary_append(ary, args); + int i; + VALUE args = rb_ary_hidden_new(argc); + for (i = 0; i < argc; i++) { + rb_ary_concat(args, argv[i]); + } + ary_append(ary, args); } ary_verify(ary); @@ -4794,17 +5192,20 @@ rb_ary_concat(VALUE x, VALUE y) /* * call-seq: - * array * n -> new_array - * array * string_separator -> new_string + * self * n -> new_array + * self * string_separator -> new_string + * + * When non-negative integer argument +n+ is given, + * returns a new array built by concatenating +n+ copies of +self+: * - * When non-negative argument \Integer +n+ is given, - * returns a new \Array built by concatenating the +n+ copies of +self+: * a = ['x', 'y'] * a * 3 # => ["x", "y", "x", "y", "x", "y"] * - * When \String argument +string_separator+ is given, - * equivalent to <tt>array.join(string_separator)</tt>: - * [0, [0, 1], {foo: 0}] * ', ' # => "0, 0, 1, {:foo=>0}" + * When string argument +string_separator+ is given, + * equivalent to <tt>self.join(string_separator)</tt>: + * + * [0, [0, 1], {foo: 0}] * ', ' # => "0, 0, 1, {foo: 0}" + * */ static VALUE @@ -4816,35 +5217,35 @@ rb_ary_times(VALUE ary, VALUE times) tmp = rb_check_string_type(times); if (!NIL_P(tmp)) { - return rb_ary_join(ary, tmp); + return rb_ary_join(ary, tmp); } len = NUM2LONG(times); if (len == 0) { ary2 = ary_new(rb_cArray, 0); - goto out; + goto out; } if (len < 0) { - rb_raise(rb_eArgError, "negative argument"); + rb_raise(rb_eArgError, "negative argument"); } if (ARY_MAX_SIZE/len < RARRAY_LEN(ary)) { - rb_raise(rb_eArgError, "argument too big"); + rb_raise(rb_eArgError, "argument too big"); } len *= RARRAY_LEN(ary); ary2 = ary_new(rb_cArray, len); ARY_SET_LEN(ary2, len); - ptr = RARRAY_CONST_PTR_TRANSIENT(ary); + ptr = RARRAY_CONST_PTR(ary); t = RARRAY_LEN(ary); if (0 < t) { - ary_memcpy(ary2, 0, t, ptr); - while (t <= len/2) { - ary_memcpy(ary2, t, t, RARRAY_CONST_PTR_TRANSIENT(ary2)); + ary_memcpy(ary2, 0, t, ptr); + while (t <= len/2) { + ary_memcpy(ary2, t, t, RARRAY_CONST_PTR(ary2)); t *= 2; } if (t < len) { - ary_memcpy(ary2, t, len-t, RARRAY_CONST_PTR_TRANSIENT(ary2)); + ary_memcpy(ary2, t, len-t, RARRAY_CONST_PTR(ary2)); } } out: @@ -4853,16 +5254,18 @@ rb_ary_times(VALUE ary, VALUE times) /* * call-seq: - * array.assoc(obj) -> found_array or nil + * assoc(object) -> found_array or nil + * + * Returns the first element +ele+ in +self+ such that +ele+ is an array + * and <tt>ele[0] == object</tt>: * - * Returns the first element in +self+ that is an \Array - * whose first element <tt>==</tt> +obj+: * a = [{foo: 0}, [2, 4], [4, 5, 6], [4, 5]] * a.assoc(4) # => [4, 5, 6] * * Returns +nil+ if no such element is found. * - * Related: #rassoc. + * Related: Array#rassoc; + * see also {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ VALUE @@ -4872,26 +5275,29 @@ rb_ary_assoc(VALUE ary, VALUE key) VALUE v; for (i = 0; i < RARRAY_LEN(ary); ++i) { - v = rb_check_array_type(RARRAY_AREF(ary, i)); - if (!NIL_P(v) && RARRAY_LEN(v) > 0 && - rb_equal(RARRAY_AREF(v, 0), key)) - return v; + v = rb_check_array_type(RARRAY_AREF(ary, i)); + if (!NIL_P(v) && RARRAY_LEN(v) > 0 && + rb_equal(RARRAY_AREF(v, 0), key)) + return v; } return Qnil; } /* * call-seq: - * array.rassoc(obj) -> found_array or nil + * rassoc(object) -> found_array or nil + * + * Returns the first element +ele+ in +self+ such that +ele+ is an array + * and <tt>ele[1] == object</tt>: * - * Returns the first element in +self+ that is an \Array - * whose second element <tt>==</tt> +obj+: * a = [{foo: 0}, [2, 4], [4, 5, 6], [4, 5]] * a.rassoc(4) # => [2, 4] + * a.rassoc(5) # => [4, 5, 6] * * Returns +nil+ if no such element is found. * - * Related: #assoc. + * Related: Array#assoc; + * see also {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ VALUE @@ -4901,11 +5307,11 @@ rb_ary_rassoc(VALUE ary, VALUE value) VALUE v; for (i = 0; i < RARRAY_LEN(ary); ++i) { - v = RARRAY_AREF(ary, i); - if (RB_TYPE_P(v, T_ARRAY) && - RARRAY_LEN(v) > 1 && - rb_equal(RARRAY_AREF(v, 1), value)) - return v; + v = rb_check_array_type(RARRAY_AREF(ary, i)); + if (RB_TYPE_P(v, T_ARRAY) && + RARRAY_LEN(v) > 1 && + rb_equal(RARRAY_AREF(v, 1), value)) + return v; } return Qnil; } @@ -4924,41 +5330,48 @@ recursive_equal(VALUE ary1, VALUE ary2, int recur) len1 = RARRAY_LEN(ary1); for (i = 0; i < len1; i++) { - if (*p1 != *p2) { - if (rb_equal(*p1, *p2)) { - len1 = RARRAY_LEN(ary1); - if (len1 != RARRAY_LEN(ary2)) - return Qfalse; - if (len1 < i) - return Qtrue; + if (*p1 != *p2) { + if (rb_equal(*p1, *p2)) { + len1 = RARRAY_LEN(ary1); + if (len1 != RARRAY_LEN(ary2)) + return Qfalse; + if (len1 < i) + return Qtrue; p1 = RARRAY_CONST_PTR(ary1) + i; p2 = RARRAY_CONST_PTR(ary2) + i; - } - else { - return Qfalse; - } - } - p1++; - p2++; + } + else { + return Qfalse; + } + } + p1++; + p2++; } return Qtrue; } /* * call-seq: - * array == other_array -> true or false + * self == other_array -> true or false * - * Returns +true+ if both <tt>array.size == other_array.size</tt> - * and for each index +i+ in +array+, <tt>array[i] == other_array[i]</tt>: - * a0 = [:foo, 'bar', 2] - * a1 = [:foo, 'bar', 2.0] - * a1 == a0 # => true - * [] == [] # => true + * Returns whether both: * - * Otherwise, returns +false+. + * - +self+ and +other_array+ are the same size. + * - Their corresponding elements are the same; + * that is, for each index +i+ in <tt>(0...self.size)</tt>, + * <tt>self[i] == other_array[i]</tt>. + * + * Examples: + * + * [:foo, 'bar', 2] == [:foo, 'bar', 2] # => true + * [:foo, 'bar', 2] == [:foo, 'bar', 2.0] # => true + * [:foo, 'bar', 2] == [:foo, 'bar'] # => false # Different sizes. + * [:foo, 'bar', 2] == [:foo, 'bar', 3] # => false # Different elements. * * This method is different from method Array#eql?, * which compares elements using <tt>Object#eql?</tt>. + * + * Related: see {Methods for Comparing}[rdoc-ref:Array@Methods+for+Comparing]. */ static VALUE @@ -4966,13 +5379,13 @@ rb_ary_equal(VALUE ary1, VALUE ary2) { if (ary1 == ary2) return Qtrue; if (!RB_TYPE_P(ary2, T_ARRAY)) { - if (!rb_respond_to(ary2, idTo_ary)) { - return Qfalse; - } - return rb_equal(ary2, ary1); + if (!rb_respond_to(ary2, idTo_ary)) { + return Qfalse; + } + return rb_equal(ary2, ary1); } if (RARRAY_LEN(ary1) != RARRAY_LEN(ary2)) return Qfalse; - if (RARRAY_CONST_PTR_TRANSIENT(ary1) == RARRAY_CONST_PTR_TRANSIENT(ary2)) return Qtrue; + if (RARRAY_CONST_PTR(ary1) == RARRAY_CONST_PTR(ary2)) return Qtrue; return rb_exec_recursive_paired(recursive_equal, ary1, ary2, ary2); } @@ -4983,26 +5396,29 @@ recursive_eql(VALUE ary1, VALUE ary2, int recur) if (recur) return Qtrue; /* Subtle! */ for (i=0; i<RARRAY_LEN(ary1); i++) { - if (!rb_eql(rb_ary_elt(ary1, i), rb_ary_elt(ary2, i))) - return Qfalse; + if (!rb_eql(rb_ary_elt(ary1, i), rb_ary_elt(ary2, i))) + return Qfalse; } return Qtrue; } /* * call-seq: - * array.eql? other_array -> true or false + * eql?(other_array) -> true or false * * Returns +true+ if +self+ and +other_array+ are the same size, - * and if, for each index +i+ in +self+, <tt>self[i].eql? other_array[i]</tt>: + * and if, for each index +i+ in +self+, <tt>self[i].eql?(other_array[i])</tt>: + * * a0 = [:foo, 'bar', 2] * a1 = [:foo, 'bar', 2] * a1.eql?(a0) # => true * * Otherwise, returns +false+. * - * This method is different from method {Array#==}[#method-i-3D-3D], + * This method is different from method Array#==, * which compares using method <tt>Object#==</tt>. + * + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ static VALUE @@ -5011,46 +5427,71 @@ rb_ary_eql(VALUE ary1, VALUE ary2) if (ary1 == ary2) return Qtrue; if (!RB_TYPE_P(ary2, T_ARRAY)) return Qfalse; if (RARRAY_LEN(ary1) != RARRAY_LEN(ary2)) return Qfalse; - if (RARRAY_CONST_PTR_TRANSIENT(ary1) == RARRAY_CONST_PTR_TRANSIENT(ary2)) return Qtrue; + if (RARRAY_CONST_PTR(ary1) == RARRAY_CONST_PTR(ary2)) return Qtrue; return rb_exec_recursive_paired(recursive_eql, ary1, ary2, ary2); } +static VALUE +ary_hash_values(long len, const VALUE *elements, const VALUE ary) +{ + long i; + st_index_t h; + VALUE n; + + h = rb_hash_start(len); + h = rb_hash_uint(h, (st_index_t)rb_ary_hash_values); + for (i=0; i<len; i++) { + n = rb_hash(elements[i]); + h = rb_hash_uint(h, NUM2LONG(n)); + if (ary) { + len = RARRAY_LEN(ary); + elements = RARRAY_CONST_PTR(ary); + } + } + h = rb_hash_end(h); + return ST2FIX(h); +} + +VALUE +rb_ary_hash_values(long len, const VALUE *elements) +{ + return ary_hash_values(len, elements, 0); +} + /* * call-seq: - * array.hash -> integer + * hash -> integer * * Returns the integer hash value for +self+. * - * Two arrays with the same content will have the same hash code (and will compare using eql?): - * [0, 1, 2].hash == [0, 1, 2].hash # => true - * [0, 1, 2].hash == [0, 1, 3].hash # => false + * Two arrays with the same content will have the same hash value + * (and will compare using eql?): + * + * ['a', 'b'].hash == ['a', 'b'].hash # => true + * ['a', 'b'].hash == ['a', 'c'].hash # => false + * ['a', 'b'].hash == ['a'].hash # => false + * */ static VALUE rb_ary_hash(VALUE ary) { - long i; - st_index_t h; - VALUE n; - - h = rb_hash_start(RARRAY_LEN(ary)); - h = rb_hash_uint(h, (st_index_t)rb_ary_hash); - for (i=0; i<RARRAY_LEN(ary); i++) { - n = rb_hash(RARRAY_AREF(ary, i)); - h = rb_hash_uint(h, NUM2LONG(n)); - } - h = rb_hash_end(h); - return ST2FIX(h); + RBIMPL_ASSERT_OR_ASSUME(ary); + return ary_hash_values(RARRAY_LEN(ary), RARRAY_CONST_PTR(ary), ary); } /* * call-seq: - * array.include?(obj) -> true or false + * include?(object) -> true or false * - * Returns +true+ if for some index +i+ in +self+, <tt>obj == self[i]</tt>; - * otherwise +false+: - * [0, 1, 2].include?(2) # => true - * [0, 1, 2].include?(3) # => false + * Returns whether for some element +element+ in +self+, + * <tt>object == element</tt>: + * + * [0, 1, 2].include?(2) # => true + * [0, 1, 2].include?(2.0) # => true + * [0, 1, 2].include?(2.1) # => false + * + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ VALUE @@ -5060,10 +5501,10 @@ rb_ary_includes(VALUE ary, VALUE item) VALUE e; for (i=0; i<RARRAY_LEN(ary); i++) { - e = RARRAY_AREF(ary, i); - if (rb_equal(e, item)) { - return Qtrue; - } + e = RARRAY_AREF(ary, i); + if (rb_equal(e, item)) { + return Qtrue; + } } return Qfalse; } @@ -5075,10 +5516,10 @@ rb_ary_includes_by_eql(VALUE ary, VALUE item) VALUE e; for (i=0; i<RARRAY_LEN(ary); i++) { - e = RARRAY_AREF(ary, i); - if (rb_eql(item, e)) { - return Qtrue; - } + e = RARRAY_AREF(ary, i); + if (rb_eql(item, e)) { + return Qtrue; + } } return Qfalse; } @@ -5091,38 +5532,54 @@ recursive_cmp(VALUE ary1, VALUE ary2, int recur) if (recur) return Qundef; /* Subtle! */ len = RARRAY_LEN(ary1); if (len > RARRAY_LEN(ary2)) { - len = RARRAY_LEN(ary2); + len = RARRAY_LEN(ary2); } for (i=0; i<len; i++) { - VALUE e1 = rb_ary_elt(ary1, i), e2 = rb_ary_elt(ary2, i); - VALUE v = rb_funcallv(e1, id_cmp, 1, &e2); - if (v != INT2FIX(0)) { - return v; - } + VALUE e1 = rb_ary_elt(ary1, i), e2 = rb_ary_elt(ary2, i); + VALUE v = rb_funcallv(e1, id_cmp, 1, &e2); + if (v != INT2FIX(0)) { + return v; + } } return Qundef; } /* * call-seq: - * array <=> other_array -> -1, 0, or 1 + * self <=> other_array -> -1, 0, or 1 + * + * Returns -1, 0, or 1 as +self+ is determined + * to be less than, equal to, or greater than +other_array+. + * + * Iterates over each index +i+ in <tt>(0...self.size)</tt>: + * + * - Computes <tt>result[i]</tt> as <tt>self[i] <=> other_array[i]</tt>. + * - Immediately returns 1 if <tt>result[i]</tt> is 1: * - * Returns -1, 0, or 1 as +self+ is less than, equal to, or greater than +other_array+. - * For each index +i+ in +self+, evaluates <tt>result = self[i] <=> other_array[i]</tt>. + * [0, 1, 2] <=> [0, 0, 2] # => 1 * - * Returns -1 if any result is -1: - * [0, 1, 2] <=> [0, 1, 3] # => -1 + * - Immediately returns -1 if <tt>result[i]</tt> is -1: * - * Returns 1 if any result is 1: - * [0, 1, 2] <=> [0, 1, 1] # => 1 + * [0, 1, 2] <=> [0, 2, 2] # => -1 * - * When all results are zero: - * - Returns -1 if +array+ is smaller than +other_array+: - * [0, 1, 2] <=> [0, 1, 2, 3] # => -1 - * - Returns 1 if +array+ is larger than +other_array+: - * [0, 1, 2] <=> [0, 1] # => 1 - * - Returns 0 if +array+ and +other_array+ are the same size: - * [0, 1, 2] <=> [0, 1, 2] # => 0 + * - Continues if <tt>result[i]</tt> is 0. + * + * When every +result+ is 0, + * returns <tt>self.size <=> other_array.size</tt> + * (see Integer#<=>): + * + * [0, 1, 2] <=> [0, 1] # => 1 + * [0, 1, 2] <=> [0, 1, 2] # => 0 + * [0, 1, 2] <=> [0, 1, 2, 3] # => -1 + * + * Note that when +other_array+ is larger than +self+, + * its trailing elements do not affect the result: + * + * [0, 1, 2] <=> [0, 1, 2, -3] # => -1 + * [0, 1, 2] <=> [0, 1, 2, 0] # => -1 + * [0, 1, 2] <=> [0, 1, 2, 3] # => -1 + * + * Related: see {Methods for Comparing}[rdoc-ref:Array@Methods+for+Comparing]. */ VALUE @@ -5135,7 +5592,7 @@ rb_ary_cmp(VALUE ary1, VALUE ary2) if (NIL_P(ary2)) return Qnil; if (ary1 == ary2) return INT2FIX(0); v = rb_exec_recursive_paired(recursive_cmp, ary1, ary2, ary2); - if (v != Qundef) return v; + if (!UNDEF_P(v)) return v; len = RARRAY_LEN(ary1) - RARRAY_LEN(ary2); if (len == 0) return INT2FIX(0); if (len > 0) return INT2FIX(1); @@ -5148,8 +5605,8 @@ ary_add_hash(VALUE hash, VALUE ary) long i; for (i=0; i<RARRAY_LEN(ary); i++) { - VALUE elt = RARRAY_AREF(ary, i); - rb_hash_add_new_element(hash, elt, elt); + VALUE elt = RARRAY_AREF(ary, i); + rb_hash_add_new_element(hash, elt, elt); } return hash; } @@ -5177,8 +5634,8 @@ ary_add_hash_by(VALUE hash, VALUE ary) long i; for (i = 0; i < RARRAY_LEN(ary); ++i) { - VALUE v = rb_ary_elt(ary, i), k = rb_yield(v); - rb_hash_add_new_element(hash, k, v); + VALUE v = rb_ary_elt(ary, i), k = rb_yield(v); + rb_hash_add_new_element(hash, k, v); } return hash; } @@ -5190,33 +5647,25 @@ ary_make_hash_by(VALUE ary) return ary_add_hash_by(hash, ary); } -static inline void -ary_recycle_hash(VALUE hash) -{ - assert(RBASIC_CLASS(hash) == 0); - if (RHASH_ST_TABLE_P(hash)) { - st_table *tbl = RHASH_ST_TABLE(hash); - st_free_table(tbl); - RHASH_ST_CLEAR(hash); - } -} - /* * call-seq: - * array - other_array -> new_array + * self - other_array -> new_array * - * Returns a new \Array containing only those elements from +array+ - * that are not found in \Array +other_array+; - * items are compared using <tt>eql?</tt>; - * the order from +array+ is preserved: - * [0, 1, 1, 2, 1, 1, 3, 1, 1] - [1] # => [0, 2, 3] - * [0, 1, 2, 3] - [3, 0] # => [1, 2] - * [0, 1, 2] - [4] # => [0, 1, 2] + * Returns a new array containing only those elements of +self+ + * that are not found in +other_array+; + * the order from +self+ is preserved: * - * Related: Array#difference. + * [0, 1, 1, 2, 1, 1, 3, 1, 1] - [1] # => [0, 2, 3] + * [0, 1, 1, 2, 1, 1, 3, 1, 1] - [3, 2, 0, :foo] # => [1, 1, 1, 1, 1, 1] + * [0, 1, 2] - [:foo] # => [0, 1, 2] + * + * Element are compared using method <tt>#eql?</tt> + * (as defined in each element of +self+). + * + * Related: see {Methods for Combining}[rdoc-ref:Array@Methods+for+Combining]. */ -static VALUE +VALUE rb_ary_diff(VALUE ary1, VALUE ary2) { VALUE ary3; @@ -5228,37 +5677,40 @@ rb_ary_diff(VALUE ary1, VALUE ary2) ary3 = rb_ary_new(); if (RARRAY_LEN(ary1) <= SMALL_ARRAY_LEN || RARRAY_LEN(ary2) <= SMALL_ARRAY_LEN) { - for (i=0; i<RARRAY_LEN(ary1); i++) { - VALUE elt = rb_ary_elt(ary1, i); - if (rb_ary_includes_by_eql(ary2, elt)) continue; - rb_ary_push(ary3, elt); - } - return ary3; + for (i=0; i<RARRAY_LEN(ary1); i++) { + VALUE elt = rb_ary_elt(ary1, i); + if (rb_ary_includes_by_eql(ary2, elt)) continue; + rb_ary_push(ary3, elt); + } + return ary3; } hash = ary_make_hash(ary2); for (i=0; i<RARRAY_LEN(ary1); i++) { if (rb_hash_stlike_lookup(hash, RARRAY_AREF(ary1, i), NULL)) continue; - rb_ary_push(ary3, rb_ary_elt(ary1, i)); + rb_ary_push(ary3, rb_ary_elt(ary1, i)); } - ary_recycle_hash(hash); + return ary3; } /* * call-seq: - * array.difference(*other_arrays) -> new_array + * difference(*other_arrays = []) -> new_array * - * Returns a new \Array containing only those elements from +self+ - * that are not found in any of the Arrays +other_arrays+; + * Returns a new array containing only those elements from +self+ + * that are not found in any of the given +other_arrays+; * items are compared using <tt>eql?</tt>; order from +self+ is preserved: + * * [0, 1, 1, 2, 1, 1, 3, 1, 1].difference([1]) # => [0, 2, 3] - * [0, 1, 2, 3].difference([3, 0], [1, 3]) # => [2] - * [0, 1, 2].difference([4]) # => [0, 1, 2] + * [0, 1, 2, 3].difference([3, 0], [1, 3]) # => [2] + * [0, 1, 2].difference([4]) # => [0, 1, 2] + * [0, 1, 2].difference # => [0, 1, 2] * - * Returns a copy of +self+ if no arguments given. + * Returns a copy of +self+ if no arguments are given. * - * Related: Array#-. + * Related: Array#-; + * see also {Methods for Combining}[rdoc-ref:Array@Methods+for+Combining]. */ static VALUE @@ -5282,7 +5734,7 @@ rb_ary_difference_multi(int argc, VALUE *argv, VALUE ary) VALUE elt = rb_ary_elt(ary, i); for (j = 0; j < argc; j++) { if (is_hash[j]) { - if (rb_hash_stlike_lookup(argv[j], RARRAY_AREF(ary, i), NULL)) + if (rb_hash_stlike_lookup(argv[j], elt, NULL)) break; } else { @@ -5300,17 +5752,25 @@ rb_ary_difference_multi(int argc, VALUE *argv, VALUE ary) /* * call-seq: - * array & other_array -> new_array + * self & other_array -> new_array + * + * Returns a new array containing the _intersection_ of +self+ and +other_array+; + * that is, containing those elements found in both +self+ and +other_array+: * - * Returns a new \Array containing each element found in both +array+ and \Array +other_array+; - * duplicates are omitted; items are compared using <tt>eql?</tt>: * [0, 1, 2, 3] & [1, 2] # => [1, 2] - * [0, 1, 0, 1] & [0, 1] # => [0, 1] * - * Preserves order from +array+: + * Omits duplicates: + * + * [0, 1, 1, 0] & [0, 1] # => [0, 1] + * + * Preserves order from +self+: + * * [0, 1, 2] & [3, 2, 1, 0] # => [0, 1, 2] * - * Related: Array#intersection. + * Identifies common elements using method <tt>#eql?</tt> + * (as defined in each element of +self+). + * + * Related: see {Methods for Combining}[rdoc-ref:Array@Methods+for+Combining]. */ @@ -5326,45 +5786,47 @@ rb_ary_and(VALUE ary1, VALUE ary2) if (RARRAY_LEN(ary1) == 0 || RARRAY_LEN(ary2) == 0) return ary3; if (RARRAY_LEN(ary1) <= SMALL_ARRAY_LEN && RARRAY_LEN(ary2) <= SMALL_ARRAY_LEN) { - for (i=0; i<RARRAY_LEN(ary1); i++) { - v = RARRAY_AREF(ary1, i); - if (!rb_ary_includes_by_eql(ary2, v)) continue; - if (rb_ary_includes_by_eql(ary3, v)) continue; - rb_ary_push(ary3, v); - } - return ary3; + for (i=0; i<RARRAY_LEN(ary1); i++) { + v = RARRAY_AREF(ary1, i); + if (!rb_ary_includes_by_eql(ary2, v)) continue; + if (rb_ary_includes_by_eql(ary3, v)) continue; + rb_ary_push(ary3, v); + } + return ary3; } hash = ary_make_hash(ary2); for (i=0; i<RARRAY_LEN(ary1); i++) { - v = RARRAY_AREF(ary1, i); - vv = (st_data_t)v; + v = RARRAY_AREF(ary1, i); + vv = (st_data_t)v; if (rb_hash_stlike_delete(hash, &vv, 0)) { - rb_ary_push(ary3, v); - } + rb_ary_push(ary3, v); + } } - ary_recycle_hash(hash); return ary3; } /* * call-seq: - * array.intersection(*other_arrays) -> new_array + * intersection(*other_arrays) -> new_array + * + * Returns a new array containing each element in +self+ that is +#eql?+ + * to at least one element in each of the given +other_arrays+; + * duplicates are omitted: * - * Returns a new \Array containing each element found both in +self+ - * and in all of the given Arrays +other_arrays+; - * duplicates are omitted; items are compared using <tt>eql?</tt>: - * [0, 1, 2, 3].intersection([0, 1, 2], [0, 1, 3]) # => [0, 1] * [0, 0, 1, 1, 2, 3].intersection([0, 1, 2], [0, 1, 3]) # => [0, 1] * - * Preserves order from +self+: + * Each element must correctly implement method <tt>#hash</tt>. + * + * Order from +self+ is preserved: + * * [0, 1, 2].intersection([2, 1, 0]) # => [0, 1, 2] * - * Returns a copy of +self+ if no arguments given. + * Returns a copy of +self+ if no arguments are given. * - * Related: Array#&. + * Related: see {Methods for Combining}[rdoc-ref:Array@Methods+for+Combining]. */ static VALUE @@ -5413,52 +5875,59 @@ rb_ary_union_hash(VALUE hash, VALUE ary2) /* * call-seq: - * array | other_array -> new_array + * self | other_array -> new_array * - * Returns the union of +array+ and \Array +other_array+; + * Returns the union of +self+ and +other_array+; * duplicates are removed; order is preserved; * items are compared using <tt>eql?</tt>: + * * [0, 1] | [2, 3] # => [0, 1, 2, 3] * [0, 1, 1] | [2, 2, 3] # => [0, 1, 2, 3] * [0, 1, 2] | [3, 2, 1, 0] # => [0, 1, 2, 3] * - * Related: Array#union. + * Related: see {Methods for Combining}[rdoc-ref:Array@Methods+for+Combining]. */ static VALUE rb_ary_or(VALUE ary1, VALUE ary2) { - VALUE hash, ary3; + VALUE hash; ary2 = to_ary(ary2); if (RARRAY_LEN(ary1) + RARRAY_LEN(ary2) <= SMALL_ARRAY_LEN) { - ary3 = rb_ary_new(); + VALUE ary3 = rb_ary_new(); rb_ary_union(ary3, ary1); rb_ary_union(ary3, ary2); - return ary3; + return ary3; } hash = ary_make_hash(ary1); rb_ary_union_hash(hash, ary2); - ary3 = rb_hash_values(hash); - ary_recycle_hash(hash); - return ary3; + return rb_hash_values(hash); } /* * call-seq: - * array.union(*other_arrays) -> new_array + * union(*other_arrays) -> new_array + * + * Returns a new array that is the union of the elements of +self+ + * and all given arrays +other_arrays+; + * items are compared using <tt>eql?</tt>: * - * Returns a new \Array that is the union of +self+ and all given Arrays +other_arrays+; - * duplicates are removed; order is preserved; items are compared using <tt>eql?</tt>: * [0, 1, 2, 3].union([4, 5], [6, 7]) # => [0, 1, 2, 3, 4, 5, 6, 7] + * + * Removes duplicates (preserving the first found): + * * [0, 1, 1].union([2, 1], [3, 1]) # => [0, 1, 2, 3] - * [0, 1, 2, 3].union([3, 2], [1, 0]) # => [0, 1, 2, 3] * - * Returns a copy of +self+ if no arguments given. + * Preserves order (preserving the position of the first found): + * + * [3, 2, 1, 0].union([5, 3], [4, 2]) # => [3, 2, 1, 0, 5, 4] * - * Related: Array#|. + * With no arguments given, returns a copy of +self+. + * + * Related: see {Methods for Combining}[rdoc-ref:Array@Methods+for+Combining]. */ static VALUE @@ -5466,7 +5935,7 @@ rb_ary_union_multi(int argc, VALUE *argv, VALUE ary) { int i; long sum; - VALUE hash, ary_union; + VALUE hash; sum = RARRAY_LEN(ary); for (i = 0; i < argc; i++) { @@ -5475,7 +5944,7 @@ rb_ary_union_multi(int argc, VALUE *argv, VALUE ary) } if (sum <= SMALL_ARRAY_LEN) { - ary_union = rb_ary_new(); + VALUE ary_union = rb_ary_new(); rb_ary_union(ary_union, ary); for (i = 0; i < argc; i++) rb_ary_union(ary_union, argv[i]); @@ -5486,23 +5955,21 @@ rb_ary_union_multi(int argc, VALUE *argv, VALUE ary) hash = ary_make_hash(ary); for (i = 0; i < argc; i++) rb_ary_union_hash(hash, argv[i]); - ary_union = rb_hash_values(hash); - ary_recycle_hash(hash); - return ary_union; + return rb_hash_values(hash); } /* * call-seq: - * ary.intersect?(other_ary) -> true or false + * intersect?(other_array) -> true or false + * + * Returns whether +other_array+ has at least one element that is +#eql?+ to some element of +self+: * - * Returns +true+ if the array and +other_ary+ have at least one element in - * common, otherwise returns +false+. + * [1, 2, 3].intersect?([3, 4, 5]) # => true + * [1, 2, 3].intersect?([4, 5, 6]) # => false * - * a = [ 1, 2, 3 ] - * b = [ 3, 4, 5 ] - * c = [ 5, 6, 7 ] - * a.intersect?(b) #=> true - * a.intersect?(c) #=> false + * Each element must correctly implement method <tt>#hash</tt>. + * + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ static VALUE @@ -5541,7 +6008,6 @@ rb_ary_intersect_p(VALUE ary1, VALUE ary2) break; } } - ary_recycle_hash(hash); return result; } @@ -5637,41 +6103,55 @@ ary_max_opt_string(VALUE ary, long i, VALUE vmax) /* * call-seq: - * array.max -> element - * array.max {|a, b| ... } -> element - * array.max(n) -> new_array - * array.max(n) {|a, b| ... } -> new_array + * max -> element + * max(count) -> new_array + * max {|a, b| ... } -> element + * max(count) {|a, b| ... } -> new_array * * Returns one of the following: + * * - The maximum-valued element from +self+. - * - A new \Array of maximum-valued elements selected from +self+. + * - A new array of maximum-valued elements from +self+. * - * When no block is given, each element in +self+ must respond to method <tt><=></tt> - * with an \Integer. + * Does not modify +self+. + * + * With no block given, each element in +self+ must respond to method <tt>#<=></tt> + * with a numeric. * * With no argument and no block, returns the element in +self+ - * having the maximum value per method <tt><=></tt>: - * [0, 1, 2].max # => 2 + * having the maximum value per method <tt>#<=></tt>: + * + * [1, 0, 3, 2].max # => 3 + * + * With non-negative numeric argument +count+ and no block, + * returns a new array with at most +count+ elements, + * in descending order, per method <tt>#<=></tt>: * - * With an argument \Integer +n+ and no block, returns a new \Array with at most +n+ elements, - * in descending order per method <tt><=></tt>: - * [0, 1, 2, 3].max(3) # => [3, 2, 1] - * [0, 1, 2, 3].max(6) # => [3, 2, 1, 0] + * [1, 0, 3, 2].max(3) # => [3, 2, 1] + * [1, 0, 3, 2].max(3.0) # => [3, 2, 1] + * [1, 0, 3, 2].max(9) # => [3, 2, 1, 0] + * [1, 0, 3, 2].max(0) # => [] * - * When a block is given, the block must return an \Integer. + * With a block given, the block must return a numeric. * - * With a block and no argument, calls the block <tt>self.size-1</tt> times to compare elements; + * With a block and no argument, calls the block <tt>self.size - 1</tt> times to compare elements; * returns the element having the maximum value per the block: - * ['0', '00', '000'].max {|a, b| a.size <=> b.size } # => "000" * - * With an argument +n+ and a block, returns a new \Array with at most +n+ elements, - * in descending order per the block: - * ['0', '00', '000'].max(2) {|a, b| a.size <=> b.size } # => ["000", "00"] + * ['0', '', '000', '00'].max {|a, b| a.size <=> b.size } + * # => "000" + * + * With non-negative numeric argument +count+ and a block, + * returns a new array with at most +count+ elements, + * in descending order, per the block: + * + * ['0', '', '000', '00'].max(2) {|a, b| a.size <=> b.size } + * # => ["000", "00"] + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE rb_ary_max(int argc, VALUE *argv, VALUE ary) { - struct cmp_opt_data cmp_opt = { 0, 0 }; VALUE result = Qundef, v; VALUE num; long i; @@ -5681,23 +6161,23 @@ rb_ary_max(int argc, VALUE *argv, VALUE ary) const long n = RARRAY_LEN(ary); if (rb_block_given_p()) { - for (i = 0; i < RARRAY_LEN(ary); i++) { - v = RARRAY_AREF(ary, i); - if (result == Qundef || rb_cmpint(rb_yield_values(2, v, result), v, result) > 0) { - result = v; - } - } + for (i = 0; i < RARRAY_LEN(ary); i++) { + v = RARRAY_AREF(ary, i); + if (UNDEF_P(result) || rb_cmpint(rb_yield_values(2, v, result), v, result) > 0) { + result = v; + } + } } else if (n > 0) { result = RARRAY_AREF(ary, 0); if (n > 1) { - if (FIXNUM_P(result) && CMP_OPTIMIZABLE(cmp_opt, Integer)) { + if (FIXNUM_P(result) && CMP_OPTIMIZABLE(INTEGER)) { return ary_max_opt_fixnum(ary, 1, result); } - else if (STRING_P(result) && CMP_OPTIMIZABLE(cmp_opt, String)) { + else if (STRING_P(result) && CMP_OPTIMIZABLE(STRING)) { return ary_max_opt_string(ary, 1, result); } - else if (RB_FLOAT_TYPE_P(result) && CMP_OPTIMIZABLE(cmp_opt, Float)) { + else if (RB_FLOAT_TYPE_P(result) && CMP_OPTIMIZABLE(FLOAT)) { return ary_max_opt_float(ary, 1, result); } else { @@ -5705,7 +6185,7 @@ rb_ary_max(int argc, VALUE *argv, VALUE ary) } } } - if (result == Qundef) return Qnil; + if (UNDEF_P(result)) return Qnil; return result; } @@ -5800,41 +6280,55 @@ ary_min_opt_string(VALUE ary, long i, VALUE vmin) /* * call-seq: - * array.min -> element - * array.min { |a, b| ... } -> element - * array.min(n) -> new_array - * array.min(n) { |a, b| ... } -> new_array + * min -> element + * min(count) -> new_array + * min {|a, b| ... } -> element + * min(count) {|a, b| ... } -> new_array * * Returns one of the following: + * * - The minimum-valued element from +self+. - * - A new \Array of minimum-valued elements selected from +self+. + * - A new array of minimum-valued elements from +self+. * - * When no block is given, each element in +self+ must respond to method <tt><=></tt> - * with an \Integer. + * Does not modify +self+. + * + * With no block given, each element in +self+ must respond to method <tt>#<=></tt> + * with a numeric. * * With no argument and no block, returns the element in +self+ - * having the minimum value per method <tt><=></tt>: - * [0, 1, 2].min # => 0 + * having the minimum value per method <tt>#<=></tt>: + * + * [1, 0, 3, 2].min # => 0 * - * With \Integer argument +n+ and no block, returns a new \Array with at most +n+ elements, - * in ascending order per method <tt><=></tt>: - * [0, 1, 2, 3].min(3) # => [0, 1, 2] - * [0, 1, 2, 3].min(6) # => [0, 1, 2, 3] + * With non-negative numeric argument +count+ and no block, + * returns a new array with at most +count+ elements, + * in ascending order, per method <tt>#<=></tt>: * - * When a block is given, the block must return an Integer. + * [1, 0, 3, 2].min(3) # => [0, 1, 2] + * [1, 0, 3, 2].min(3.0) # => [0, 1, 2] + * [1, 0, 3, 2].min(9) # => [0, 1, 2, 3] + * [1, 0, 3, 2].min(0) # => [] * - * With a block and no argument, calls the block <tt>self.size-1</tt> times to compare elements; + * With a block given, the block must return a numeric. + * + * With a block and no argument, calls the block <tt>self.size - 1</tt> times to compare elements; * returns the element having the minimum value per the block: - * ['0', '00', '000'].min { |a, b| a.size <=> b.size } # => "0" * - * With an argument +n+ and a block, returns a new \Array with at most +n+ elements, - * in ascending order per the block: - * ['0', '00', '000'].min(2) {|a, b| a.size <=> b.size } # => ["0", "00"] + * ['0', '', '000', '00'].min {|a, b| a.size <=> b.size } + * # => "" + * + * With non-negative numeric argument +count+ and a block, + * returns a new array with at most +count+ elements, + * in ascending order, per the block: + * + * ['0', '', '000', '00'].min(2) {|a, b| a.size <=> b.size } + * # => ["", "0"] + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE rb_ary_min(int argc, VALUE *argv, VALUE ary) { - struct cmp_opt_data cmp_opt = { 0, 0 }; VALUE result = Qundef, v; VALUE num; long i; @@ -5844,23 +6338,23 @@ rb_ary_min(int argc, VALUE *argv, VALUE ary) const long n = RARRAY_LEN(ary); if (rb_block_given_p()) { - for (i = 0; i < RARRAY_LEN(ary); i++) { - v = RARRAY_AREF(ary, i); - if (result == Qundef || rb_cmpint(rb_yield_values(2, v, result), v, result) < 0) { - result = v; - } - } + for (i = 0; i < RARRAY_LEN(ary); i++) { + v = RARRAY_AREF(ary, i); + if (UNDEF_P(result) || rb_cmpint(rb_yield_values(2, v, result), v, result) < 0) { + result = v; + } + } } else if (n > 0) { result = RARRAY_AREF(ary, 0); if (n > 1) { - if (FIXNUM_P(result) && CMP_OPTIMIZABLE(cmp_opt, Integer)) { + if (FIXNUM_P(result) && CMP_OPTIMIZABLE(INTEGER)) { return ary_min_opt_fixnum(ary, 1, result); } - else if (STRING_P(result) && CMP_OPTIMIZABLE(cmp_opt, String)) { + else if (STRING_P(result) && CMP_OPTIMIZABLE(STRING)) { return ary_min_opt_string(ary, 1, result); } - else if (RB_FLOAT_TYPE_P(result) && CMP_OPTIMIZABLE(cmp_opt, Float)) { + else if (RB_FLOAT_TYPE_P(result) && CMP_OPTIMIZABLE(FLOAT)) { return ary_min_opt_float(ary, 1, result); } else { @@ -5868,29 +6362,31 @@ rb_ary_min(int argc, VALUE *argv, VALUE ary) } } } - if (result == Qundef) return Qnil; + if (UNDEF_P(result)) return Qnil; return result; } /* * call-seq: - * array.minmax -> [min_val, max_val] - * array.minmax {|a, b| ... } -> [min_val, max_val] - * - * Returns a new 2-element \Array containing the minimum and maximum values - * from +self+, either per method <tt><=></tt> or per a given block:. - * - * When no block is given, each element in +self+ must respond to method <tt><=></tt> - * with an \Integer; - * returns a new 2-element \Array containing the minimum and maximum values - * from +self+, per method <tt><=></tt>: - * [0, 1, 2].minmax # => [0, 2] - * - * When a block is given, the block must return an \Integer; - * the block is called <tt>self.size-1</tt> times to compare elements; - * returns a new 2-element \Array containing the minimum and maximum values - * from +self+, per the block: - * ['0', '00', '000'].minmax {|a, b| a.size <=> b.size } # => ["0", "000"] + * minmax -> array + * minmax {|a, b| ... } -> array + * + * Returns a 2-element array containing the minimum-valued and maximum-valued + * elements from +self+; + * does not modify +self+. + * + * With no block given, the minimum and maximum values are determined using method <tt>#<=></tt>: + * + * [1, 0, 3, 2].minmax # => [0, 3] + * + * With a block given, the block must return a numeric; + * the block is called <tt>self.size - 1</tt> times to compare elements; + * returns the elements having the minimum and maximum values per the block: + * + * ['0', '', '000', '00'].minmax {|a, b| a.size <=> b.size } + * # => ["", "000"] + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE rb_ary_minmax(VALUE ary) @@ -5910,30 +6406,30 @@ push_value(st_data_t key, st_data_t val, st_data_t ary) /* * call-seq: - * array.uniq! -> self or nil - * array.uniq! {|element| ... } -> self or nil + * uniq! -> self or nil + * uniq! {|element| ... } -> self or nil * * Removes duplicate elements from +self+, the first occurrence always being retained; * returns +self+ if any elements removed, +nil+ otherwise. * * With no block given, identifies and removes elements using method <tt>eql?</tt> - * to compare. + * to compare elements: * - * Returns +self+ if any elements removed: * a = [0, 0, 1, 1, 2, 2] * a.uniq! # => [0, 1, 2] - * - * Returns +nil+ if no elements removed. + * a.uniq! # => nil * * With a block given, calls the block for each element; - * identifies (using method <tt>eql?</tt>) and removes - * elements for which the block returns duplicate values. + * identifies and omits "duplicate" elements using method <tt>eql?</tt> + * to compare <i>block return values</i>; + * that is, an element is a duplicate if its block return value + * is the same as that of a previous element: * - * Returns +self+ if any elements removed: * a = ['a', 'aa', 'aaa', 'b', 'bb', 'bbb'] - * a.uniq! {|element| element.size } # => ['a', 'aa', 'aaa'] + * a.uniq! {|element| element.size } # => ["a", "aa", "aaa"] + * a.uniq! {|element| element.size } # => nil * - * Returns +nil+ if no elements removed. + * Related: see {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ static VALUE rb_ary_uniq_bang(VALUE ary) @@ -5945,45 +6441,50 @@ rb_ary_uniq_bang(VALUE ary) if (RARRAY_LEN(ary) <= 1) return Qnil; if (rb_block_given_p()) - hash = ary_make_hash_by(ary); + hash = ary_make_hash_by(ary); else - hash = ary_make_hash(ary); + hash = ary_make_hash(ary); hash_size = RHASH_SIZE(hash); if (RARRAY_LEN(ary) == hash_size) { - return Qnil; + return Qnil; } rb_ary_modify_check(ary); ARY_SET_LEN(ary, 0); - if (ARY_SHARED_P(ary) && !ARY_EMBED_P(ary)) { - rb_ary_unshare(ary); - FL_SET_EMBED(ary); + if (ARY_SHARED_P(ary)) { + rb_ary_unshare(ary); + FL_SET_EMBED(ary); } ary_resize_capa(ary, hash_size); rb_hash_foreach(hash, push_value, ary); - ary_recycle_hash(hash); return ary; } /* * call-seq: - * array.uniq -> new_array - * array.uniq {|element| ... } -> new_array + * uniq -> new_array + * uniq {|element| ... } -> new_array * - * Returns a new \Array containing those elements from +self+ that are not duplicates, + * Returns a new array containing those elements from +self+ that are not duplicates, * the first occurrence always being retained. * - * With no block given, identifies and omits duplicates using method <tt>eql?</tt> - * to compare. + * With no block given, identifies and omits duplicate elements using method <tt>eql?</tt> + * to compare elements: + * * a = [0, 0, 1, 1, 2, 2] * a.uniq # => [0, 1, 2] * * With a block given, calls the block for each element; - * identifies (using method <tt>eql?</tt>) and omits duplicate values, - * that is, those elements for which the block returns the same value: + * identifies and omits "duplicate" elements using method <tt>eql?</tt> + * to compare <i>block return values</i>; + * that is, an element is a duplicate if its block return value + * is the same as that of a previous element: + * * a = ['a', 'aa', 'aaa', 'b', 'bb', 'bbb'] * a.uniq {|element| element.size } # => ["a", "aa", "aaa"] + * + * Related: {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -5996,15 +6497,12 @@ rb_ary_uniq(VALUE ary) uniq = rb_ary_dup(ary); } else if (rb_block_given_p()) { - hash = ary_make_hash_by(ary); - uniq = rb_hash_values(hash); + hash = ary_make_hash_by(ary); + uniq = rb_hash_values(hash); } else { - hash = ary_make_hash(ary); - uniq = rb_hash_values(hash); - } - if (hash) { - ary_recycle_hash(hash); + hash = ary_make_hash(ary); + uniq = rb_hash_values(hash); } return uniq; @@ -6012,11 +6510,18 @@ rb_ary_uniq(VALUE ary) /* * call-seq: - * array.compact! -> self or nil + * compact! -> self or nil * - * Removes all +nil+ elements from +self+. + * Removes all +nil+ elements from +self+; + * Returns +self+ if any elements are removed, +nil+ otherwise: * - * Returns +self+ if any elements removed, otherwise +nil+. + * a = [nil, 0, nil, false, nil, '', nil, [], nil, {}] + * a.compact! # => [0, false, "", [], {}] + * a # => [0, false, "", [], {}] + * a.compact! # => nil + * + * Related: Array#compact; + * see also {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ static VALUE @@ -6026,16 +6531,16 @@ rb_ary_compact_bang(VALUE ary) long n; rb_ary_modify(ary); - p = t = (VALUE *)RARRAY_CONST_PTR_TRANSIENT(ary); /* WB: no new reference */ + p = t = (VALUE *)RARRAY_CONST_PTR(ary); /* WB: no new reference */ end = p + RARRAY_LEN(ary); while (t < end) { - if (NIL_P(*t)) t++; - else *p++ = *t++; + if (NIL_P(*t)) t++; + else *p++ = *t++; } - n = p - RARRAY_CONST_PTR_TRANSIENT(ary); + n = p - RARRAY_CONST_PTR(ary); if (RARRAY_LEN(ary) == n) { - return Qnil; + return Qnil; } ary_resize_smaller(ary, n); @@ -6044,11 +6549,16 @@ rb_ary_compact_bang(VALUE ary) /* * call-seq: - * array.compact -> new_array + * compact -> new_array * - * Returns a new \Array containing all non-+nil+ elements from +self+: - * a = [nil, 0, nil, 1, nil, 2, nil] - * a.compact # => [0, 1, 2] + * Returns a new array containing only the non-+nil+ elements from +self+; + * element order is preserved: + * + * a = [nil, 0, nil, false, nil, '', nil, [], nil, {}] + * a.compact # => [0, false, "", [], {}] + * + * Related: Array#compact!; + * see also {Methods for Deleting}[rdoc-ref:Array@Methods+for+Deleting]. */ static VALUE @@ -6061,26 +6571,29 @@ rb_ary_compact(VALUE ary) /* * call-seq: - * array.count -> an_integer - * array.count(obj) -> an_integer - * array.count {|element| ... } -> an_integer + * count -> integer + * count(object) -> integer + * count {|element| ... } -> integer * * Returns a count of specified elements. * * With no argument and no block, returns the count of all elements: - * [0, 1, 2].count # => 3 - * [].count # => 0 * - * With argument +obj+, returns the count of elements <tt>==</tt> to +obj+: - * [0, 1, 2, 0.0].count(0) # => 2 - * [0, 1, 2].count(3) # => 0 + * [0, :one, 'two', 3, 3.0].count # => 5 + * + * With argument +object+ given, returns the count of elements <tt>==</tt> to +object+: + * + * [0, :one, 'two', 3, 3.0].count(3) # => 2 * * With no argument and a block given, calls the block with each element; * returns the count of elements for which the block returns a truthy value: - * [0, 1, 2, 3].count {|element| element > 1} # => 2 * - * With argument +obj+ and a block given, issues a warning, ignores the block, - * and returns the count of elements <tt>==</tt> to +obj+: + * [0, 1, 2, 3].count {|element| element > 1 } # => 2 + * + * With argument +object+ and a block given, issues a warning, ignores the block, + * and returns the count of elements <tt>==</tt> to +object+. + * + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ static VALUE @@ -6089,25 +6602,25 @@ rb_ary_count(int argc, VALUE *argv, VALUE ary) long i, n = 0; if (rb_check_arity(argc, 0, 1) == 0) { - VALUE v; + VALUE v; - if (!rb_block_given_p()) - return LONG2NUM(RARRAY_LEN(ary)); + if (!rb_block_given_p()) + return LONG2NUM(RARRAY_LEN(ary)); - for (i = 0; i < RARRAY_LEN(ary); i++) { - v = RARRAY_AREF(ary, i); - if (RTEST(rb_yield(v))) n++; - } + for (i = 0; i < RARRAY_LEN(ary); i++) { + v = RARRAY_AREF(ary, i); + if (RTEST(rb_yield(v))) n++; + } } else { VALUE obj = argv[0]; - if (rb_block_given_p()) { - rb_warn("given block not used"); - } - for (i = 0; i < RARRAY_LEN(ary); i++) { - if (rb_equal(RARRAY_AREF(ary, i), obj)) n++; - } + if (rb_block_given_p()) { + rb_warn("given block not used"); + } + for (i = 0; i < RARRAY_LEN(ary); i++) { + if (rb_equal(RARRAY_AREF(ary, i), obj)) n++; + } } return LONG2NUM(n); @@ -6117,9 +6630,8 @@ static VALUE flatten(VALUE ary, int level) { long i; - VALUE stack, result, tmp = 0, elt, vmemo; - st_table *memo = 0; - st_data_t id; + VALUE stack, result, tmp = 0, elt; + VALUE memo = Qfalse; for (i = 0; i < RARRAY_LEN(ary); i++) { elt = RARRAY_AREF(ary, i); @@ -6133,7 +6645,7 @@ flatten(VALUE ary, int level) } result = ary_new(0, RARRAY_LEN(ary)); - ary_memcpy(result, 0, i, RARRAY_CONST_PTR_TRANSIENT(ary)); + ary_memcpy(result, 0, i, RARRAY_CONST_PTR(ary)); ARY_SET_LEN(result, i); stack = ary_new(0, ARY_DEFAULT_SIZE); @@ -6141,64 +6653,58 @@ flatten(VALUE ary, int level) rb_ary_push(stack, LONG2NUM(i + 1)); if (level < 0) { - vmemo = rb_hash_new(); - RBASIC_CLEAR_CLASS(vmemo); - memo = st_init_numtable(); - rb_hash_st_table_set(vmemo, memo); - st_insert(memo, (st_data_t)ary, (st_data_t)Qtrue); - st_insert(memo, (st_data_t)tmp, (st_data_t)Qtrue); + memo = rb_obj_hide(rb_ident_hash_new()); + rb_hash_aset(memo, ary, Qtrue); + rb_hash_aset(memo, tmp, Qtrue); } ary = tmp; i = 0; while (1) { - while (i < RARRAY_LEN(ary)) { - elt = RARRAY_AREF(ary, i++); - if (level >= 0 && RARRAY_LEN(stack) / 2 >= level) { - rb_ary_push(result, elt); - continue; - } - tmp = rb_check_array_type(elt); - if (RBASIC(result)->klass) { - if (memo) { - RB_GC_GUARD(vmemo); - st_clear(memo); - } - rb_raise(rb_eRuntimeError, "flatten reentered"); - } - if (NIL_P(tmp)) { - rb_ary_push(result, elt); - } - else { - if (memo) { - id = (st_data_t)tmp; - if (st_is_member(memo, id)) { - st_clear(memo); - rb_raise(rb_eArgError, "tried to flatten recursive array"); - } - st_insert(memo, id, (st_data_t)Qtrue); - } - rb_ary_push(stack, ary); - rb_ary_push(stack, LONG2NUM(i)); - ary = tmp; - i = 0; - } - } - if (RARRAY_LEN(stack) == 0) { - break; - } - if (memo) { - id = (st_data_t)ary; - st_delete(memo, &id, 0); - } - tmp = rb_ary_pop(stack); - i = NUM2LONG(tmp); - ary = rb_ary_pop(stack); + while (i < RARRAY_LEN(ary)) { + elt = RARRAY_AREF(ary, i++); + if (level >= 0 && RARRAY_LEN(stack) / 2 >= level) { + rb_ary_push(result, elt); + continue; + } + tmp = rb_check_array_type(elt); + if (RBASIC(result)->klass) { + if (RTEST(memo)) { + rb_hash_clear(memo); + } + rb_raise(rb_eRuntimeError, "flatten reentered"); + } + if (NIL_P(tmp)) { + rb_ary_push(result, elt); + } + else { + if (memo) { + if (rb_hash_aref(memo, tmp) == Qtrue) { + rb_hash_clear(memo); + rb_raise(rb_eArgError, "tried to flatten recursive array"); + } + rb_hash_aset(memo, tmp, Qtrue); + } + rb_ary_push(stack, ary); + rb_ary_push(stack, LONG2NUM(i)); + ary = tmp; + i = 0; + } + } + if (RARRAY_LEN(stack) == 0) { + break; + } + if (memo) { + rb_hash_delete(memo, ary); + } + tmp = rb_ary_pop(stack); + i = NUM2LONG(tmp); + ary = rb_ary_pop(stack); } if (memo) { - st_clear(memo); + rb_hash_clear(memo); } RBASIC_SET_CLASS(result, rb_cArray); @@ -6207,30 +6713,37 @@ flatten(VALUE ary, int level) /* * call-seq: - * array.flatten! -> self or nil - * array.flatten!(level) -> self or nil - * - * Replaces each nested \Array in +self+ with the elements from that \Array; - * returns +self+ if any changes, +nil+ otherwise. - * - * With non-negative \Integer argument +level+, flattens recursively through +level+ levels: - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten!(1) # => [0, 1, [2, 3], 4, 5] - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten!(2) # => [0, 1, 2, 3, 4, 5] - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten!(3) # => [0, 1, 2, 3, 4, 5] - * [0, 1, 2].flatten!(1) # => nil - * - * With no argument, a +nil+ argument, or with negative argument +level+, flattens all levels: - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten! # => [0, 1, 2, 3, 4, 5] - * [0, 1, 2].flatten! # => nil - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten!(-1) # => [0, 1, 2, 3, 4, 5] - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten!(-2) # => [0, 1, 2, 3, 4, 5] - * [0, 1, 2].flatten!(-1) # => nil + * flatten!(depth = nil) -> self or nil + * + * Returns +self+ as a recursively flattening of +self+ to +depth+ levels of recursion; + * +depth+ must be an + * {integer-convertible object}[rdoc-ref:implicit_conversion.rdoc@Integer-Convertible+Objects], + * or +nil+. + * At each level of recursion: + * + * - Each element that is an array is "flattened" + * (that is, replaced by its individual array elements). + * - Each element that is not an array is unchanged + * (even if the element is an object that has instance method +flatten+). + * + * Returns +nil+ if no elements were flattened. + * + * With non-negative integer argument +depth+, flattens recursively through +depth+ levels: + * + * a = [ 0, [ 1, [2, 3], 4 ], 5, {foo: 0}, Set.new([6, 7]) ] + * a # => [0, [1, [2, 3], 4], 5, {:foo=>0}, #<Set: {6, 7}>] + * a.dup.flatten!(1) # => [0, 1, [2, 3], 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * a.dup.flatten!(1.1) # => [0, 1, [2, 3], 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * a.dup.flatten!(2) # => [0, 1, 2, 3, 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * a.dup.flatten!(3) # => [0, 1, 2, 3, 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * + * With +nil+ or negative argument +depth+, flattens all levels: + * + * a.dup.flatten! # => [0, 1, 2, 3, 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * a.dup.flatten!(-1) # => [0, 1, 2, 3, 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * + * Related: Array#flatten; + * see also {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. */ static VALUE @@ -6246,9 +6759,9 @@ rb_ary_flatten_bang(int argc, VALUE *argv, VALUE ary) result = flatten(ary, level); if (result == ary) { - return Qnil; + return Qnil; } - if (!(mod = ARY_EMBED_P(result))) rb_obj_freeze(result); + if (!(mod = ARY_EMBED_P(result))) rb_ary_freeze(result); rb_ary_replace(ary, result); if (mod) ARY_SET_EMBED_LEN(result, 0); @@ -6257,32 +6770,37 @@ rb_ary_flatten_bang(int argc, VALUE *argv, VALUE ary) /* * call-seq: - * array.flatten -> new_array - * array.flatten(level) -> new_array - * - * Returns a new \Array that is a recursive flattening of +self+: - * - Each non-Array element is unchanged. - * - Each \Array is replaced by its individual elements. - * - * With non-negative \Integer argument +level+, flattens recursively through +level+ levels: - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten(0) # => [0, [1, [2, 3], 4], 5] - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten(1) # => [0, 1, [2, 3], 4, 5] - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten(2) # => [0, 1, 2, 3, 4, 5] - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten(3) # => [0, 1, 2, 3, 4, 5] - * - * With no argument, a +nil+ argument, or with negative argument +level+, flattens all levels: - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten # => [0, 1, 2, 3, 4, 5] - * [0, 1, 2].flatten # => [0, 1, 2] - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten(-1) # => [0, 1, 2, 3, 4, 5] - * a = [ 0, [ 1, [2, 3], 4 ], 5 ] - * a.flatten(-2) # => [0, 1, 2, 3, 4, 5] - * [0, 1, 2].flatten(-1) # => [0, 1, 2] + * flatten(depth = nil) -> new_array + * + * Returns a new array that is a recursive flattening of +self+ + * to +depth+ levels of recursion; + * +depth+ must be an + * {integer-convertible object}[rdoc-ref:implicit_conversion.rdoc@Integer-Convertible+Objects] + * or +nil+. + * At each level of recursion: + * + * - Each element that is an array is "flattened" + * (that is, replaced by its individual array elements). + * - Each element that is not an array is unchanged + * (even if the element is an object that has instance method +flatten+). + * + * With non-negative integer argument +depth+, flattens recursively through +depth+ levels: + * + * a = [ 0, [ 1, [2, 3], 4 ], 5, {foo: 0}, Set.new([6, 7]) ] + * a # => [0, [1, [2, 3], 4], 5, {:foo=>0}, #<Set: {6, 7}>] + * a.flatten(0) # => [0, [1, [2, 3], 4], 5, {:foo=>0}, #<Set: {6, 7}>] + * a.flatten(1 ) # => [0, 1, [2, 3], 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * a.flatten(1.1) # => [0, 1, [2, 3], 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * a.flatten(2) # => [0, 1, 2, 3, 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * a.flatten(3) # => [0, 1, 2, 3, 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * + * With +nil+ or negative +depth+, flattens all levels. + * + * a.flatten # => [0, 1, 2, 3, 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * a.flatten(-1) # => [0, 1, 2, 3, 4, 5, {:foo=>0}, #<Set: {6, 7}>] + * + * Related: Array#flatten!; + * see also {Methods for Converting}[rdoc-ref:Array@Methods+for+Converting]. */ static VALUE @@ -6314,16 +6832,16 @@ rb_ary_shuffle_bang(rb_execution_context_t *ec, VALUE ary, VALUE randgen) rb_ary_modify(ary); i = len = RARRAY_LEN(ary); RARRAY_PTR_USE(ary, ptr, { - while (i) { - long j = RAND_UPTO(i); - VALUE tmp; - if (len != RARRAY_LEN(ary) || ptr != RARRAY_CONST_PTR_TRANSIENT(ary)) { + while (i > 1) { + long j = RAND_UPTO(i); + VALUE tmp; + if (len != RARRAY_LEN(ary) || ptr != RARRAY_CONST_PTR(ary)) { rb_raise(rb_eRuntimeError, "modified during shuffle"); - } - tmp = ptr[--i]; - ptr[i] = ptr[j]; - ptr[j] = tmp; - } + } + tmp = ptr[--i]; + ptr[i] = ptr[j]; + ptr[j] = tmp; + } }); /* WB: no new reference */ return ary; } @@ -6336,6 +6854,14 @@ rb_ary_shuffle(rb_execution_context_t *ec, VALUE ary, VALUE randgen) return ary; } +static const rb_data_type_t ary_sample_memo_type = { + .wrap_struct_name = "ary_sample_memo", + .function = { + .dfree = (RUBY_DATA_FUNC)st_free_table, + }, + .flags = RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FREE_IMMEDIATELY +}; + static VALUE ary_sample(rb_execution_context_t *ec, VALUE ary, VALUE randgen, VALUE nv, VALUE to_array) { @@ -6346,120 +6872,119 @@ ary_sample(rb_execution_context_t *ec, VALUE ary, VALUE randgen, VALUE nv, VALUE len = RARRAY_LEN(ary); if (!to_array) { - if (len < 2) - i = 0; - else - i = RAND_UPTO(len); + if (len < 2) + i = 0; + else + i = RAND_UPTO(len); - return rb_ary_elt(ary, i); + return rb_ary_elt(ary, i); } n = NUM2LONG(nv); if (n < 0) rb_raise(rb_eArgError, "negative sample number"); if (n > len) n = len; if (n <= numberof(idx)) { - for (i = 0; i < n; ++i) { - rnds[i] = RAND_UPTO(len - i); - } + for (i = 0; i < n; ++i) { + rnds[i] = RAND_UPTO(len - i); + } } k = len; len = RARRAY_LEN(ary); if (len < k && n <= numberof(idx)) { - for (i = 0; i < n; ++i) { - if (rnds[i] >= len) return rb_ary_new_capa(0); - } + for (i = 0; i < n; ++i) { + if (rnds[i] >= len) return rb_ary_new_capa(0); + } } if (n > len) n = len; switch (n) { case 0: - return rb_ary_new_capa(0); + return rb_ary_new_capa(0); case 1: - i = rnds[0]; - return rb_ary_new_from_args(1, RARRAY_AREF(ary, i)); + i = rnds[0]; + return rb_ary_new_from_args(1, RARRAY_AREF(ary, i)); case 2: - i = rnds[0]; - j = rnds[1]; - if (j >= i) j++; - return rb_ary_new_from_args(2, RARRAY_AREF(ary, i), RARRAY_AREF(ary, j)); + i = rnds[0]; + j = rnds[1]; + if (j >= i) j++; + return rb_ary_new_from_args(2, RARRAY_AREF(ary, i), RARRAY_AREF(ary, j)); case 3: - i = rnds[0]; - j = rnds[1]; - k = rnds[2]; - { - long l = j, g = i; - if (j >= i) l = i, g = ++j; - if (k >= l && (++k >= g)) ++k; - } - return rb_ary_new_from_args(3, RARRAY_AREF(ary, i), RARRAY_AREF(ary, j), RARRAY_AREF(ary, k)); + i = rnds[0]; + j = rnds[1]; + k = rnds[2]; + { + long l = j, g = i; + if (j >= i) l = i, g = ++j; + if (k >= l && (++k >= g)) ++k; + } + return rb_ary_new_from_args(3, RARRAY_AREF(ary, i), RARRAY_AREF(ary, j), RARRAY_AREF(ary, k)); } memo_threshold = - len < 2560 ? len / 128 : - len < 5120 ? len / 64 : - len < 10240 ? len / 32 : - len / 16; + len < 2560 ? len / 128 : + len < 5120 ? len / 64 : + len < 10240 ? len / 32 : + len / 16; if (n <= numberof(idx)) { - long sorted[numberof(idx)]; - sorted[0] = idx[0] = rnds[0]; - for (i=1; i<n; i++) { - k = rnds[i]; - for (j = 0; j < i; ++j) { - if (k < sorted[j]) break; - ++k; - } - memmove(&sorted[j+1], &sorted[j], sizeof(sorted[0])*(i-j)); - sorted[j] = idx[i] = k; - } - result = rb_ary_new_capa(n); - RARRAY_PTR_USE_TRANSIENT(result, ptr_result, { - for (i=0; i<n; i++) { - ptr_result[i] = RARRAY_AREF(ary, idx[i]); - } - }); + long sorted[numberof(idx)]; + sorted[0] = idx[0] = rnds[0]; + for (i=1; i<n; i++) { + k = rnds[i]; + for (j = 0; j < i; ++j) { + if (k < sorted[j]) break; + ++k; + } + memmove(&sorted[j+1], &sorted[j], sizeof(sorted[0])*(i-j)); + sorted[j] = idx[i] = k; + } + result = rb_ary_new_capa(n); + RARRAY_PTR_USE(result, ptr_result, { + for (i=0; i<n; i++) { + ptr_result[i] = RARRAY_AREF(ary, idx[i]); + } + }); } else if (n <= memo_threshold / 2) { - long max_idx = 0; -#undef RUBY_UNTYPED_DATA_WARNING -#define RUBY_UNTYPED_DATA_WARNING 0 - VALUE vmemo = Data_Wrap_Struct(0, 0, st_free_table, 0); - st_table *memo = st_init_numtable_with_size(n); - DATA_PTR(vmemo) = memo; - result = rb_ary_new_capa(n); - RARRAY_PTR_USE(result, ptr_result, { - for (i=0; i<n; i++) { - long r = RAND_UPTO(len-i) + i; - ptr_result[i] = r; - if (r > max_idx) max_idx = r; - } - len = RARRAY_LEN(ary); - if (len <= max_idx) n = 0; - else if (n > len) n = len; - RARRAY_PTR_USE_TRANSIENT(ary, ptr_ary, { - for (i=0; i<n; i++) { - long j2 = j = ptr_result[i]; - long i2 = i; - st_data_t value; - if (st_lookup(memo, (st_data_t)i, &value)) i2 = (long)value; - if (st_lookup(memo, (st_data_t)j, &value)) j2 = (long)value; - st_insert(memo, (st_data_t)j, (st_data_t)i2); - ptr_result[i] = ptr_ary[j2]; - } - }); - }); - DATA_PTR(vmemo) = 0; - st_free_table(memo); + long max_idx = 0; + VALUE vmemo = TypedData_Wrap_Struct(0, &ary_sample_memo_type, 0); + st_table *memo = st_init_numtable_with_size(n); + RTYPEDDATA_DATA(vmemo) = memo; + result = rb_ary_new_capa(n); + RARRAY_PTR_USE(result, ptr_result, { + for (i=0; i<n; i++) { + long r = RAND_UPTO(len-i) + i; + ptr_result[i] = r; + if (r > max_idx) max_idx = r; + } + len = RARRAY_LEN(ary); + if (len <= max_idx) n = 0; + else if (n > len) n = len; + RARRAY_PTR_USE(ary, ptr_ary, { + for (i=0; i<n; i++) { + long j2 = j = ptr_result[i]; + long i2 = i; + st_data_t value; + if (st_lookup(memo, (st_data_t)i, &value)) i2 = (long)value; + if (st_lookup(memo, (st_data_t)j, &value)) j2 = (long)value; + st_insert(memo, (st_data_t)j, (st_data_t)i2); + ptr_result[i] = ptr_ary[j2]; + } + }); + }); + RTYPEDDATA_DATA(vmemo) = 0; + st_free_table(memo); + RB_GC_GUARD(vmemo); } else { - result = rb_ary_dup(ary); - RBASIC_CLEAR_CLASS(result); - RB_GC_GUARD(ary); - RARRAY_PTR_USE(result, ptr_result, { - for (i=0; i<n; i++) { - j = RAND_UPTO(len-i) + i; - nv = ptr_result[j]; - ptr_result[j] = ptr_result[i]; - ptr_result[i] = nv; - } - }); - RBASIC_SET_CLASS_RAW(result, rb_cArray); + result = rb_ary_dup(ary); + RBASIC_CLEAR_CLASS(result); + RB_GC_GUARD(ary); + RARRAY_PTR_USE(result, ptr_result, { + for (i=0; i<n; i++) { + j = RAND_UPTO(len-i) + i; + nv = ptr_result[j]; + ptr_result[j] = ptr_result[i]; + ptr_result[i] = nv; + } + }); + RBASIC_SET_CLASS_RAW(result, rb_cArray); } ARY_SET_LEN(result, n); @@ -6467,6 +6992,12 @@ ary_sample(rb_execution_context_t *ec, VALUE ary, VALUE randgen, VALUE nv, VALUE } static VALUE +ary_sized_alloc(rb_execution_context_t *ec, VALUE self) +{ + return rb_ary_new2(RARRAY_LEN(self)); +} + +static VALUE ary_sample0(rb_execution_context_t *ec, VALUE ary) { return ary_sample(ec, ary, rb_cRandom, Qfalse, Qfalse); @@ -6478,7 +7009,7 @@ rb_ary_cycle_size(VALUE self, VALUE args, VALUE eobj) long mul; VALUE n = Qnil; if (args && (RARRAY_LEN(args) > 0)) { - n = RARRAY_AREF(args, 0); + n = RARRAY_AREF(args, 0); } if (RARRAY_LEN(self) == 0) return INT2FIX(0); if (NIL_P(n)) return DBL2NUM(HUGE_VAL); @@ -6490,32 +7021,36 @@ rb_ary_cycle_size(VALUE self, VALUE args, VALUE eobj) /* * call-seq: - * array.cycle {|element| ... } -> nil - * array.cycle(count) {|element| ... } -> nil - * array.cycle -> new_enumerator - * array.cycle(count) -> new_enumerator + * cycle(count = nil) {|element| ... } -> nil + * cycle(count = nil) -> new_enumerator * - * When called with positive \Integer argument +count+ and a block, - * calls the block with each element, then does so again, + * With a block given, may call the block, depending on the value of argument +count+; + * +count+ must be an + * {integer-convertible object}[rdoc-ref:implicit_conversion.rdoc@Integer-Convertible+Objects], + * or +nil+. + * + * When +count+ is positive, + * calls the block with each element, then does so repeatedly, * until it has done so +count+ times; returns +nil+: + * * output = [] * [0, 1].cycle(2) {|element| output.push(element) } # => nil * output # => [0, 1, 0, 1] * - * If +count+ is zero or negative, does not call the block: - * [0, 1].cycle(0) {|element| fail 'Cannot happen' } # => nil + * When +count+ is zero or negative, does not call the block: + * + * [0, 1].cycle(0) {|element| fail 'Cannot happen' } # => nil * [0, 1].cycle(-1) {|element| fail 'Cannot happen' } # => nil * - * When a block is given, and argument is omitted or +nil+, cycles forever: + * When +count+ is +nil+, cycles forever: + * * # Prints 0 and 1 forever. * [0, 1].cycle {|element| puts element } * [0, 1].cycle(nil) {|element| puts element } * - * When no block is given, returns a new \Enumerator: + * With no block given, returns a new Enumerator. * - * [0, 1].cycle(2) # => #<Enumerator: [0, 1]:cycle(2)> - * [0, 1].cycle # => # => #<Enumerator: [0, 1]:cycle> - * [0, 1].cycle.first(5) # => [0, 1, 0, 1, 0] + * Related: see {Methods for Iterating}[rdoc-ref:Array@Methods+for+Iterating]. */ static VALUE rb_ary_cycle(int argc, VALUE *argv, VALUE ary) @@ -6541,9 +7076,6 @@ rb_ary_cycle(int argc, VALUE *argv, VALUE ary) return Qnil; } -#define tmpary(n) rb_ary_tmp_new(n) -#define tmpary_discard(a) (ary_discard(a), RBASIC_SET_CLASS_RAW(a, rb_cArray)) - /* * Build a ruby array of the corresponding values and yield it to the * associated block. @@ -6579,32 +7111,32 @@ permute0(const long n, const long r, long *const p, char *const used, const VALU long i = 0, index = 0; for (;;) { - const char *const unused = memchr(&used[i], 0, n-i); - if (!unused) { - if (!index) break; - i = p[--index]; /* pop index */ - used[i++] = 0; /* index unused */ - } - else { - i = unused - used; - p[index] = i; - used[i] = 1; /* mark index used */ - ++index; - if (index < r-1) { /* if not done yet */ - p[index] = i = 0; - continue; - } - for (i = 0; i < n; ++i) { - if (used[i]) continue; - p[index] = i; - if (!yield_indexed_values(values, r, p)) { - rb_raise(rb_eRuntimeError, "permute reentered"); - } - } - i = p[--index]; /* pop index */ - used[i] = 0; /* index unused */ - p[index] = ++i; - } + const char *const unused = memchr(&used[i], 0, n-i); + if (!unused) { + if (!index) break; + i = p[--index]; /* pop index */ + used[i++] = 0; /* index unused */ + } + else { + i = unused - used; + p[index] = i; + used[i] = 1; /* mark index used */ + ++index; + if (index < r-1) { /* if not done yet */ + p[index] = i = 0; + continue; + } + for (i = 0; i < n; ++i) { + if (used[i]) continue; + p[index] = i; + if (!yield_indexed_values(values, r, p)) { + rb_raise(rb_eRuntimeError, "permute reentered"); + } + } + i = p[--index]; /* pop index */ + used[i] = 0; /* index unused */ + p[index] = ++i; + } } } @@ -6617,14 +7149,14 @@ descending_factorial(long from, long how_many) { VALUE cnt; if (how_many > 0) { - cnt = LONG2FIX(from); - while (--how_many > 0) { - long v = --from; - cnt = rb_int_mul(cnt, LONG2FIX(v)); - } + cnt = LONG2FIX(from); + while (--how_many > 0) { + long v = --from; + cnt = rb_int_mul(cnt, LONG2FIX(v)); + } } else { - cnt = LONG2FIX(how_many == 0); + cnt = LONG2FIX(how_many == 0); } return cnt; } @@ -6635,18 +7167,18 @@ binomial_coefficient(long comb, long size) VALUE r; long i; if (comb > size-comb) { - comb = size-comb; + comb = size-comb; } if (comb < 0) { - return LONG2FIX(0); + return LONG2FIX(0); } else if (comb == 0) { - return LONG2FIX(1); + return LONG2FIX(1); } r = LONG2FIX(size); for (i = 1; i < comb; ++i) { - r = rb_int_mul(r, LONG2FIX(size - i)); - r = rb_int_idiv(r, LONG2FIX(i + 1)); + r = rb_int_mul(r, LONG2FIX(size - i)); + r = rb_int_idiv(r, LONG2FIX(i + 1)); } return r; } @@ -6662,66 +7194,44 @@ rb_ary_permutation_size(VALUE ary, VALUE args, VALUE eobj) /* * call-seq: - * array.permutation {|element| ... } -> self - * array.permutation(n) {|element| ... } -> self - * array.permutation -> new_enumerator - * array.permutation(n) -> new_enumerator + * permutation(count = self.size) {|permutation| ... } -> self + * permutation(count = self.size) -> new_enumerator * - * When invoked with a block, yield all permutations of elements of +self+; returns +self+. - * The order of permutations is indeterminate. + * Iterates over permutations of the elements of +self+; + * the order of permutations is indeterminate. * - * When a block and an in-range positive \Integer argument +n+ (<tt>0 < n <= self.size</tt>) - * are given, calls the block with all +n+-tuple permutations of +self+. + * With a block and an in-range positive integer argument +count+ (<tt>0 < count <= self.size</tt>) given, + * calls the block with each permutation of +self+ of size +count+; + * returns +self+: * - * Example: - * a = [0, 1, 2] - * a.permutation(2) {|permutation| p permutation } - * Output: - * [0, 1] - * [0, 2] - * [1, 0] - * [1, 2] - * [2, 0] - * [2, 1] - * Another example: - * a = [0, 1, 2] - * a.permutation(3) {|permutation| p permutation } - * Output: - * [0, 1, 2] - * [0, 2, 1] - * [1, 0, 2] - * [1, 2, 0] - * [2, 0, 1] - * [2, 1, 0] - * - * When +n+ is zero, calls the block once with a new empty \Array: * a = [0, 1, 2] - * a.permutation(0) {|permutation| p permutation } - * Output: - * [] + * perms = [] + * a.permutation(1) {|perm| perms.push(perm) } + * perms # => [[0], [1], [2]] * - * When +n+ is out of range (negative or larger than <tt>self.size</tt>), + * perms = [] + * a.permutation(2) {|perm| perms.push(perm) } + * perms # => [[0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1]] + * + * perms = [] + * a.permutation(3) {|perm| perms.push(perm) } + * perms # => [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]] + * + * When +count+ is zero, calls the block once with a new empty array: + * + * perms = [] + * a.permutation(0) {|perm| perms.push(perm) } + * perms # => [[]] + * + * When +count+ is out of range (negative or larger than <tt>self.size</tt>), * does not call the block: - * a = [0, 1, 2] + * * a.permutation(-1) {|permutation| fail 'Cannot happen' } * a.permutation(4) {|permutation| fail 'Cannot happen' } * - * When a block given but no argument, - * behaves the same as <tt>a.permutation(a.size)</tt>: - * a = [0, 1, 2] - * a.permutation {|permutation| p permutation } - * Output: - * [0, 1, 2] - * [0, 2, 1] - * [1, 0, 2] - * [1, 2, 0] - * [2, 0, 1] - * [2, 1, 0] - * - * Returns a new \Enumerator if no block given: - * a = [0, 1, 2] - * a.permutation # => #<Enumerator: [0, 1, 2]:permutation> - * a.permutation(2) # => #<Enumerator: [0, 1, 2]:permutation(2)> + * With no block given, returns a new Enumerator. + * + * Related: {Methods for Iterating}[rdoc-ref:Array@Methods+for+Iterating]. */ static VALUE @@ -6736,28 +7246,28 @@ rb_ary_permutation(int argc, VALUE *argv, VALUE ary) r = NUM2LONG(argv[0]); /* Permutation size from argument */ if (r < 0 || n < r) { - /* no permutations: yield nothing */ + /* no permutations: yield nothing */ } else if (r == 0) { /* exactly one permutation: the zero-length array */ - rb_yield(rb_ary_new2(0)); + rb_yield(rb_ary_new2(0)); } else if (r == 1) { /* this is a special, easy case */ - for (i = 0; i < RARRAY_LEN(ary); i++) { - rb_yield(rb_ary_new3(1, RARRAY_AREF(ary, i))); - } + for (i = 0; i < RARRAY_LEN(ary); i++) { + rb_yield(rb_ary_new3(1, RARRAY_AREF(ary, i))); + } } else { /* this is the general case */ - volatile VALUE t0; - long *p = ALLOCV_N(long, t0, r+roomof(n, sizeof(long))); - char *used = (char*)(p + r); - VALUE ary0 = ary_make_shared_copy(ary); /* private defensive copy of ary */ - RBASIC_CLEAR_CLASS(ary0); + volatile VALUE t0; + long *p = ALLOCV_N(long, t0, r+roomof(n, sizeof(long))); + char *used = (char*)(p + r); + VALUE ary0 = ary_make_shared_copy(ary); /* private defensive copy of ary */ + RBASIC_CLEAR_CLASS(ary0); - MEMZERO(used, char, n); /* initialize array */ + MEMZERO(used, char, n); /* initialize array */ - permute0(n, r, p, used, ary0); /* compute and yield permutations */ - ALLOCV_END(t0); - RBASIC_SET_CLASS_RAW(ary0, rb_cArray); + permute0(n, r, p, used, ary0); /* compute and yield permutations */ + ALLOCV_END(t0); + RBASIC_SET_CLASS_RAW(ary0, rb_cArray); } return ary; } @@ -6770,16 +7280,16 @@ combinate0(const long len, const long n, long *const stack, const VALUE values) MEMZERO(stack+1, long, n); stack[0] = -1; for (;;) { - for (lev++; lev < n; lev++) { - stack[lev+1] = stack[lev]+1; - } - if (!yield_indexed_values(values, n, stack+1)) { - rb_raise(rb_eRuntimeError, "combination reentered"); - } - do { - if (lev == 0) return; - stack[lev--]++; - } while (stack[lev+1]+n == len+lev+1); + for (lev++; lev < n; lev++) { + stack[lev+1] = stack[lev]+1; + } + if (!yield_indexed_values(values, n, stack+1)) { + rb_raise(rb_eRuntimeError, "combination reentered"); + } + do { + if (lev == 0) return; + stack[lev--]++; + } while (stack[lev+1]+n == len+lev+1); } } @@ -6794,44 +7304,46 @@ rb_ary_combination_size(VALUE ary, VALUE args, VALUE eobj) /* * call-seq: - * array.combination(n) {|element| ... } -> self - * array.combination(n) -> new_enumerator + * combination(count) {|element| ... } -> self + * combination(count) -> new_enumerator * - * Calls the block, if given, with combinations of elements of +self+; - * returns +self+. The order of combinations is indeterminate. + * When a block and a positive + * {integer-convertible object}[rdoc-ref:implicit_conversion.rdoc@Integer-Convertible+Objects] + * argument +count+ (<tt>0 < count <= self.size</tt>) + * are given, calls the block with each combination of +self+ of size +count+; + * returns +self+: * - * When a block and an in-range positive \Integer argument +n+ (<tt>0 < n <= self.size</tt>) - * are given, calls the block with all +n+-tuple combinations of +self+. + * a = %w[a b c] # => ["a", "b", "c"] + * a.combination(2) {|combination| p combination } # => ["a", "b", "c"] * - * Example: - * a = [0, 1, 2] - * a.combination(2) {|combination| p combination } * Output: - * [0, 1] - * [0, 2] - * [1, 2] * - * Another example: - * a = [0, 1, 2] - * a.combination(3) {|combination| p combination } - * Output: - * [0, 1, 2] + * ["a", "b"] + * ["a", "c"] + * ["b", "c"] + * + * The order of the yielded combinations is not guaranteed. + * + * When +count+ is zero, calls the block once with a new empty array: + * + * a.combination(0) {|combination| p combination } + * [].combination(0) {|combination| p combination } * - * When +n+ is zero, calls the block once with a new empty \Array: - * a = [0, 1, 2] - * a1 = a.combination(0) {|combination| p combination } * Output: + * + * [] * [] * - * When +n+ is out of range (negative or larger than <tt>self.size</tt>), + * When +count+ is negative or larger than +self.size+ and +self+ is non-empty, * does not call the block: - * a = [0, 1, 2] - * a.combination(-1) {|combination| fail 'Cannot happen' } - * a.combination(4) {|combination| fail 'Cannot happen' } * - * Returns a new \Enumerator if no block given: - * a = [0, 1, 2] - * a.combination(2) # => #<Enumerator: [0, 1, 2]:combination(2)> + * a.combination(-1) {|combination| fail 'Cannot happen' } # => ["a", "b", "c"] + * a.combination(4) {|combination| fail 'Cannot happen' } # => ["a", "b", "c"] + * + * With no block given, returns a new Enumerator. + * + * Related: Array#permutation; + * see also {Methods for Iterating}[rdoc-ref:Array@Methods+for+Iterating]. */ static VALUE @@ -6843,25 +7355,25 @@ rb_ary_combination(VALUE ary, VALUE num) RETURN_SIZED_ENUMERATOR(ary, 1, &num, rb_ary_combination_size); len = RARRAY_LEN(ary); if (n < 0 || len < n) { - /* yield nothing */ + /* yield nothing */ } else if (n == 0) { - rb_yield(rb_ary_new2(0)); + rb_yield(rb_ary_new2(0)); } else if (n == 1) { - for (i = 0; i < RARRAY_LEN(ary); i++) { - rb_yield(rb_ary_new3(1, RARRAY_AREF(ary, i))); - } + for (i = 0; i < RARRAY_LEN(ary); i++) { + rb_yield(rb_ary_new3(1, RARRAY_AREF(ary, i))); + } } else { - VALUE ary0 = ary_make_shared_copy(ary); /* private defensive copy of ary */ - volatile VALUE t0; - long *stack = ALLOCV_N(long, t0, n+1); + VALUE ary0 = ary_make_shared_copy(ary); /* private defensive copy of ary */ + volatile VALUE t0; + long *stack = ALLOCV_N(long, t0, n+1); - RBASIC_CLEAR_CLASS(ary0); - combinate0(len, n, stack, ary0); - ALLOCV_END(t0); - RBASIC_SET_CLASS_RAW(ary0, rb_cArray); + RBASIC_CLEAR_CLASS(ary0); + combinate0(len, n, stack, ary0); + ALLOCV_END(t0); + RBASIC_SET_CLASS_RAW(ary0, rb_cArray); } return ary; } @@ -6885,19 +7397,19 @@ rpermute0(const long n, const long r, long *const p, const VALUE values) p[index] = i; for (;;) { - if (++index < r-1) { - p[index] = i = 0; - continue; - } - for (i = 0; i < n; ++i) { - p[index] = i; - if (!yield_indexed_values(values, r, p)) { - rb_raise(rb_eRuntimeError, "repeated permute reentered"); - } - } - do { - if (index <= 0) return; - } while ((i = ++p[--index]) >= n); + if (++index < r-1) { + p[index] = i = 0; + continue; + } + for (i = 0; i < n; ++i) { + p[index] = i; + if (!yield_indexed_values(values, r, p)) { + rb_raise(rb_eRuntimeError, "repeated permute reentered"); + } + } + do { + if (index <= 0) return; + } while ((i = ++p[--index]) >= n); } } @@ -6908,68 +7420,51 @@ rb_ary_repeated_permutation_size(VALUE ary, VALUE args, VALUE eobj) long k = NUM2LONG(RARRAY_AREF(args, 0)); if (k < 0) { - return LONG2FIX(0); + return LONG2FIX(0); } if (n <= 0) { - return LONG2FIX(!k); + return LONG2FIX(!k); } return rb_int_positive_pow(n, (unsigned long)k); } /* * call-seq: - * array.repeated_permutation(n) {|permutation| ... } -> self - * array.repeated_permutation(n) -> new_enumerator + * repeated_permutation(size) {|permutation| ... } -> self + * repeated_permutation(size) -> new_enumerator * - * Calls the block with each repeated permutation of length +n+ of the elements of +self+; - * each permutation is an \Array; + * With a block given, calls the block with each repeated permutation of length +size+ + * of the elements of +self+; + * each permutation is an array; * returns +self+. The order of the permutations is indeterminate. * - * When a block and a positive \Integer argument +n+ are given, calls the block with each - * +n+-tuple repeated permutation of the elements of +self+. - * The number of permutations is <tt>self.size**n</tt>. + * If a positive integer argument +size+ is given, + * calls the block with each +size+-tuple repeated permutation of the elements of +self+. + * The number of permutations is <tt>self.size**size</tt>. * - * +n+ = 1: - * a = [0, 1, 2] - * a.repeated_permutation(1) {|permutation| p permutation } - * Output: - * [0] - * [1] - * [2] + * Examples: * - * +n+ = 2: - * a.repeated_permutation(2) {|permutation| p permutation } - * Output: - * [0, 0] - * [0, 1] - * [0, 2] - * [1, 0] - * [1, 1] - * [1, 2] - * [2, 0] - * [2, 1] - * [2, 2] - * - * If +n+ is zero, calls the block once with an empty \Array. - * - * If +n+ is negative, does not call the block: - * a.repeated_permutation(-1) {|permutation| fail 'Cannot happen' } - * - * Returns a new \Enumerator if no block given: - * a = [0, 1, 2] - * a.repeated_permutation(2) # => #<Enumerator: [0, 1, 2]:permutation(2)> - * - * Using Enumerators, it's convenient to show the permutations and counts - * for some values of +n+: - * e = a.repeated_permutation(0) - * e.size # => 1 - * e.to_a # => [[]] - * e = a.repeated_permutation(1) - * e.size # => 3 - * e.to_a # => [[0], [1], [2]] - * e = a.repeated_permutation(2) - * e.size # => 9 - * e.to_a # => [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2]] + * - +size+ is 1: + * + * p = [] + * [0, 1, 2].repeated_permutation(1) {|permutation| p.push(permutation) } + * p # => [[0], [1], [2]] + * + * - +size+ is 2: + * + * p = [] + * [0, 1, 2].repeated_permutation(2) {|permutation| p.push(permutation) } + * p # => [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2]] + * + * If +size+ is zero, calls the block once with an empty array. + * + * If +size+ is negative, does not call the block: + * + * [0, 1, 2].repeated_permutation(-1) {|permutation| fail 'Cannot happen' } + * + * With no block given, returns a new Enumerator. + * + * Related: see {Methods for Combining}[rdoc-ref:Array@Methods+for+Combining]. */ static VALUE rb_ary_repeated_permutation(VALUE ary, VALUE num) @@ -6981,25 +7476,25 @@ rb_ary_repeated_permutation(VALUE ary, VALUE num) r = NUM2LONG(num); /* Permutation size from argument */ if (r < 0) { - /* no permutations: yield nothing */ + /* no permutations: yield nothing */ } else if (r == 0) { /* exactly one permutation: the zero-length array */ - rb_yield(rb_ary_new2(0)); + rb_yield(rb_ary_new2(0)); } else if (r == 1) { /* this is a special, easy case */ - for (i = 0; i < RARRAY_LEN(ary); i++) { - rb_yield(rb_ary_new3(1, RARRAY_AREF(ary, i))); - } + for (i = 0; i < RARRAY_LEN(ary); i++) { + rb_yield(rb_ary_new3(1, RARRAY_AREF(ary, i))); + } } else { /* this is the general case */ - volatile VALUE t0; - long *p = ALLOCV_N(long, t0, r); - VALUE ary0 = ary_make_shared_copy(ary); /* private defensive copy of ary */ - RBASIC_CLEAR_CLASS(ary0); + volatile VALUE t0; + long *p = ALLOCV_N(long, t0, r); + VALUE ary0 = ary_make_shared_copy(ary); /* private defensive copy of ary */ + RBASIC_CLEAR_CLASS(ary0); - rpermute0(n, r, p, ary0); /* compute and yield repeated permutations */ - ALLOCV_END(t0); - RBASIC_SET_CLASS_RAW(ary0, rb_cArray); + rpermute0(n, r, p, ary0); /* compute and yield repeated permutations */ + ALLOCV_END(t0); + RBASIC_SET_CLASS_RAW(ary0, rb_cArray); } return ary; } @@ -7011,19 +7506,19 @@ rcombinate0(const long n, const long r, long *const p, const long rest, const VA p[index] = i; for (;;) { - if (++index < r-1) { - p[index] = i; - continue; - } - for (; i < n; ++i) { - p[index] = i; - if (!yield_indexed_values(values, r, p)) { - rb_raise(rb_eRuntimeError, "repeated combination reentered"); - } - } - do { - if (index <= 0) return; - } while ((i = ++p[--index]) >= n); + if (++index < r-1) { + p[index] = i; + continue; + } + for (; i < n; ++i) { + p[index] = i; + if (!yield_indexed_values(values, r, p)) { + rb_raise(rb_eRuntimeError, "repeated combination reentered"); + } + } + do { + if (index <= 0) return; + } while ((i = ++p[--index]) >= n); } } @@ -7033,62 +7528,48 @@ rb_ary_repeated_combination_size(VALUE ary, VALUE args, VALUE eobj) long n = RARRAY_LEN(ary); long k = NUM2LONG(RARRAY_AREF(args, 0)); if (k == 0) { - return LONG2FIX(1); + return LONG2FIX(1); } return binomial_coefficient(k, n + k - 1); } /* * call-seq: - * array.repeated_combination(n) {|combination| ... } -> self - * array.repeated_combination(n) -> new_enumerator + * repeated_combination(size) {|combination| ... } -> self + * repeated_combination(size) -> new_enumerator * - * Calls the block with each repeated combination of length +n+ of the elements of +self+; - * each combination is an \Array; + * With a block given, calls the block with each repeated combination of length +size+ + * of the elements of +self+; + * each combination is an array; * returns +self+. The order of the combinations is indeterminate. * - * When a block and a positive \Integer argument +n+ are given, calls the block with each - * +n+-tuple repeated combination of the elements of +self+. - * The number of combinations is <tt>(n+1)(n+2)/2</tt>. + * If a positive integer argument +size+ is given, + * calls the block with each +size+-tuple repeated combination of the elements of +self+. + * The number of combinations is <tt>(size+1)(size+2)/2</tt>. * - * +n+ = 1: - * a = [0, 1, 2] - * a.repeated_combination(1) {|combination| p combination } - * Output: - * [0] - * [1] - * [2] + * Examples: * - * +n+ = 2: - * a.repeated_combination(2) {|combination| p combination } - * Output: - * [0, 0] - * [0, 1] - * [0, 2] - * [1, 1] - * [1, 2] - * [2, 2] + * - +size+ is 1: * - * If +n+ is zero, calls the block once with an empty \Array. + * c = [] + * [0, 1, 2].repeated_combination(1) {|combination| c.push(combination) } + * c # => [[0], [1], [2]] * - * If +n+ is negative, does not call the block: - * a.repeated_combination(-1) {|combination| fail 'Cannot happen' } + * - +size+ is 2: * - * Returns a new \Enumerator if no block given: - * a = [0, 1, 2] - * a.repeated_combination(2) # => #<Enumerator: [0, 1, 2]:combination(2)> - * - * Using Enumerators, it's convenient to show the combinations and counts - * for some values of +n+: - * e = a.repeated_combination(0) - * e.size # => 1 - * e.to_a # => [[]] - * e = a.repeated_combination(1) - * e.size # => 3 - * e.to_a # => [[0], [1], [2]] - * e = a.repeated_combination(2) - * e.size # => 6 - * e.to_a # => [[0, 0], [0, 1], [0, 2], [1, 1], [1, 2], [2, 2]] + * c = [] + * [0, 1, 2].repeated_combination(2) {|combination| c.push(combination) } + * c # => [[0, 0], [0, 1], [0, 2], [1, 1], [1, 2], [2, 2]] + * + * If +size+ is zero, calls the block once with an empty array. + * + * If +size+ is negative, does not call the block: + * + * [0, 1, 2].repeated_combination(-1) {|combination| fail 'Cannot happen' } + * + * With no block given, returns a new Enumerator. + * + * Related: see {Methods for Combining}[rdoc-ref:Array@Methods+for+Combining]. */ static VALUE @@ -7100,86 +7581,90 @@ rb_ary_repeated_combination(VALUE ary, VALUE num) RETURN_SIZED_ENUMERATOR(ary, 1, &num, rb_ary_repeated_combination_size); /* Return enumerator if no block */ len = RARRAY_LEN(ary); if (n < 0) { - /* yield nothing */ + /* yield nothing */ } else if (n == 0) { - rb_yield(rb_ary_new2(0)); + rb_yield(rb_ary_new2(0)); } else if (n == 1) { - for (i = 0; i < RARRAY_LEN(ary); i++) { - rb_yield(rb_ary_new3(1, RARRAY_AREF(ary, i))); - } + for (i = 0; i < RARRAY_LEN(ary); i++) { + rb_yield(rb_ary_new3(1, RARRAY_AREF(ary, i))); + } } else if (len == 0) { - /* yield nothing */ + /* yield nothing */ } else { - volatile VALUE t0; - long *p = ALLOCV_N(long, t0, n); - VALUE ary0 = ary_make_shared_copy(ary); /* private defensive copy of ary */ - RBASIC_CLEAR_CLASS(ary0); + volatile VALUE t0; + long *p = ALLOCV_N(long, t0, n); + VALUE ary0 = ary_make_shared_copy(ary); /* private defensive copy of ary */ + RBASIC_CLEAR_CLASS(ary0); - rcombinate0(len, n, p, n, ary0); /* compute and yield repeated combinations */ - ALLOCV_END(t0); - RBASIC_SET_CLASS_RAW(ary0, rb_cArray); + rcombinate0(len, n, p, n, ary0); /* compute and yield repeated combinations */ + ALLOCV_END(t0); + RBASIC_SET_CLASS_RAW(ary0, rb_cArray); } return ary; } /* * call-seq: - * array.product(*other_arrays) -> new_array - * array.product(*other_arrays) {|combination| ... } -> self + * product(*other_arrays) -> new_array + * product(*other_arrays) {|combination| ... } -> self + * + * Computes all combinations of elements from all the arrays, + * including both +self+ and +other_arrays+: * - * Computes and returns or yields all combinations of elements from all the Arrays, - * including both +self+ and +other_arrays+. * - The number of combinations is the product of the sizes of all the arrays, * including both +self+ and +other_arrays+. * - The order of the returned combinations is indeterminate. * - * When no block is given, returns the combinations as an \Array of Arrays: - * a = [0, 1, 2] - * a1 = [3, 4] - * a2 = [5, 6] - * p = a.product(a1) - * p.size # => 6 # a.size * a1.size - * p # => [[0, 3], [0, 4], [1, 3], [1, 4], [2, 3], [2, 4]] - * p = a.product(a1, a2) - * p.size # => 12 # a.size * a1.size * a2.size - * p # => [[0, 3, 5], [0, 3, 6], [0, 4, 5], [0, 4, 6], [1, 3, 5], [1, 3, 6], [1, 4, 5], [1, 4, 6], [2, 3, 5], [2, 3, 6], [2, 4, 5], [2, 4, 6]] - * - * If any argument is an empty \Array, returns an empty \Array. - * - * If no argument is given, returns an \Array of 1-element Arrays, + * With no block given, returns the combinations as an array of arrays: + * + * p = [0, 1].product([2, 3]) + * # => [[0, 2], [0, 3], [1, 2], [1, 3]] + * p.size # => 4 + * p = [0, 1].product([2, 3], [4, 5]) + * # => [[0, 2, 4], [0, 2, 5], [0, 3, 4], [0, 3, 5], [1, 2, 4], [1, 2, 5], [1, 3, 4], [1, 3,... + * p.size # => 8 + * + * If +self+ or any argument is empty, returns an empty array: + * + * [].product([2, 3], [4, 5]) # => [] + * [0, 1].product([2, 3], []) # => [] + * + * If no argument is given, returns an array of 1-element arrays, * each containing an element of +self+: + * * a.product # => [[0], [1], [2]] * - * When a block is given, yields each combination as an \Array; returns +self+: - * a.product(a1) {|combination| p combination } - * Output: - * [0, 3] - * [0, 4] - * [1, 3] - * [1, 4] - * [2, 3] - * [2, 4] - * - * If any argument is an empty \Array, does not call the block: - * a.product(a1, a2, []) {|combination| fail 'Cannot happen' } - * - * If no argument is given, yields each element of +self+ as a 1-element \Array: - * a.product {|combination| p combination } - * Output: - * [0] - * [1] - * [2] + * With a block given, calls the block with each combination; returns +self+: + * + * p = [] + * [0, 1].product([2, 3]) {|combination| p.push(combination) } + * p # => [[0, 2], [0, 3], [1, 2], [1, 3]] + * + * If +self+ or any argument is empty, does not call the block: + * + * [].product([2, 3], [4, 5]) {|combination| fail 'Cannot happen' } + * # => [] + * [0, 1].product([2, 3], []) {|combination| fail 'Cannot happen' } + * # => [0, 1] + * + * If no argument is given, calls the block with each element of +self+ as a 1-element array: + * + * p = [] + * [0, 1].product {|combination| p.push(combination) } + * p # => [[0], [1]] + * + * Related: see {Methods for Combining}[rdoc-ref:Array@Methods+for+Combining]. */ static VALUE rb_ary_product(int argc, VALUE *argv, VALUE ary) { int n = argc+1; /* How many arrays we're operating on */ - volatile VALUE t0 = tmpary(n); + volatile VALUE t0 = rb_ary_hidden_new(n); volatile VALUE t1 = Qundef; VALUE *arrays = RARRAY_PTR(t0); /* The arrays we're computing the product of */ int *counters = ALLOCV_N(int, t1, n); /* The current position in each one */ @@ -7200,64 +7685,64 @@ rb_ary_product(int argc, VALUE *argv, VALUE ary) /* Otherwise, allocate and fill in an array of results */ if (rb_block_given_p()) { - /* Make defensive copies of arrays; exit if any is empty */ - for (i = 0; i < n; i++) { - if (RARRAY_LEN(arrays[i]) == 0) goto done; - arrays[i] = ary_make_shared_copy(arrays[i]); - } + /* Make defensive copies of arrays; exit if any is empty */ + for (i = 0; i < n; i++) { + if (RARRAY_LEN(arrays[i]) == 0) goto done; + arrays[i] = ary_make_shared_copy(arrays[i]); + } } else { - /* Compute the length of the result array; return [] if any is empty */ - for (i = 0; i < n; i++) { - long k = RARRAY_LEN(arrays[i]); - if (k == 0) { - result = rb_ary_new2(0); - goto done; - } + /* Compute the length of the result array; return [] if any is empty */ + for (i = 0; i < n; i++) { + long k = RARRAY_LEN(arrays[i]); + if (k == 0) { + result = rb_ary_new2(0); + goto done; + } if (MUL_OVERFLOW_LONG_P(resultlen, k)) - rb_raise(rb_eRangeError, "too big to product"); - resultlen *= k; - } - result = rb_ary_new2(resultlen); + rb_raise(rb_eRangeError, "too big to product"); + resultlen *= k; + } + result = rb_ary_new2(resultlen); } for (;;) { - int m; - /* fill in one subarray */ - VALUE subarray = rb_ary_new2(n); - for (j = 0; j < n; j++) { - rb_ary_push(subarray, rb_ary_entry(arrays[j], counters[j])); - } - - /* put it on the result array */ - if (NIL_P(result)) { - FL_SET(t0, FL_USER5); - rb_yield(subarray); - if (! FL_TEST(t0, FL_USER5)) { - rb_raise(rb_eRuntimeError, "product reentered"); - } - else { - FL_UNSET(t0, FL_USER5); - } - } - else { - rb_ary_push(result, subarray); - } - - /* - * Increment the last counter. If it overflows, reset to 0 - * and increment the one before it. - */ - m = n-1; - counters[m]++; - while (counters[m] == RARRAY_LEN(arrays[m])) { - counters[m] = 0; - /* If the first counter overflows, we are done */ - if (--m < 0) goto done; - counters[m]++; - } + int m; + /* fill in one subarray */ + VALUE subarray = rb_ary_new2(n); + for (j = 0; j < n; j++) { + rb_ary_push(subarray, rb_ary_entry(arrays[j], counters[j])); + } + + /* put it on the result array */ + if (NIL_P(result)) { + FL_SET(t0, RARRAY_SHARED_ROOT_FLAG); + rb_yield(subarray); + if (!FL_TEST(t0, RARRAY_SHARED_ROOT_FLAG)) { + rb_raise(rb_eRuntimeError, "product reentered"); + } + else { + FL_UNSET(t0, RARRAY_SHARED_ROOT_FLAG); + } + } + else { + rb_ary_push(result, subarray); + } + + /* + * Increment the last counter. If it overflows, reset to 0 + * and increment the one before it. + */ + m = n-1; + counters[m]++; + while (counters[m] == RARRAY_LEN(arrays[m])) { + counters[m] = 0; + /* If the first counter overflows, we are done */ + if (--m < 0) goto done; + counters[m]++; + } } + done: - tmpary_discard(t0); ALLOCV_END(t1); return NIL_P(result) ? ary : result; @@ -7265,18 +7750,20 @@ done: /* * call-seq: - * array.take(n) -> new_array + * take(count) -> new_array * - * Returns a new \Array containing the first +n+ element of +self+, - * where +n+ is a non-negative \Integer; - * does not modify +self+. + * Returns a new array containing the first +count+ element of +self+ + * (as available); + * +count+ must be a non-negative numeric; + * does not modify +self+: * - * Examples: - * a = [0, 1, 2, 3, 4, 5] - * a.take(1) # => [0] - * a.take(2) # => [0, 1] - * a.take(50) # => [0, 1, 2, 3, 4, 5] - * a # => [0, 1, 2, 3, 4, 5] + * a = ['a', 'b', 'c', 'd'] + * a.take(2) # => ["a", "b"] + * a.take(2.1) # => ["a", "b"] + * a.take(50) # => ["a", "b", "c", "d"] + * a.take(0) # => [] + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -7284,29 +7771,30 @@ rb_ary_take(VALUE obj, VALUE n) { long len = NUM2LONG(n); if (len < 0) { - rb_raise(rb_eArgError, "attempt to take negative size"); + rb_raise(rb_eArgError, "attempt to take negative size"); } return rb_ary_subseq(obj, 0, len); } /* * call-seq: - * array.take_while {|element| ... } -> new_array - * array.take_while -> new_enumerator - * - * Returns a new \Array containing zero or more leading elements of +self+; - * does not modify +self+. + * take_while {|element| ... } -> new_array + * take_while -> new_enumerator * * With a block given, calls the block with each successive element of +self+; - * stops if the block returns +false+ or +nil+; - * returns a new Array containing those elements for which the block returned a truthy value: + * stops iterating if the block returns +false+ or +nil+; + * returns a new array containing those elements for which the block returned a truthy value: + * * a = [0, 1, 2, 3, 4, 5] * a.take_while {|element| element < 3 } # => [0, 1, 2] - * a.take_while {|element| true } # => [0, 1, 2, 3, 4, 5] - * a # => [0, 1, 2, 3, 4, 5] + * a.take_while {|element| true } # => [0, 1, 2, 3, 4, 5] + * a.take_while {|element| false } # => [] + * + * With no block given, returns a new Enumerator. + * + * Does not modify +self+. * - * With no block given, returns a new \Enumerator: - * [0, 1].take_while # => #<Enumerator: [0, 1]:take_while> + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -7316,24 +7804,28 @@ rb_ary_take_while(VALUE ary) RETURN_ENUMERATOR(ary, 0, 0); for (i = 0; i < RARRAY_LEN(ary); i++) { - if (!RTEST(rb_yield(RARRAY_AREF(ary, i)))) break; + if (!RTEST(rb_yield(RARRAY_AREF(ary, i)))) break; } return rb_ary_take(ary, LONG2FIX(i)); } /* * call-seq: - * array.drop(n) -> new_array + * drop(count) -> new_array * - * Returns a new \Array containing all but the first +n+ element of +self+, - * where +n+ is a non-negative \Integer; + * Returns a new array containing all but the first +count+ element of +self+, + * where +count+ is a non-negative integer; * does not modify +self+. * * Examples: + * * a = [0, 1, 2, 3, 4, 5] * a.drop(0) # => [0, 1, 2, 3, 4, 5] * a.drop(1) # => [1, 2, 3, 4, 5] * a.drop(2) # => [2, 3, 4, 5] + * a.drop(9) # => [] + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -7342,7 +7834,7 @@ rb_ary_drop(VALUE ary, VALUE n) VALUE result; long pos = NUM2LONG(n); if (pos < 0) { - rb_raise(rb_eArgError, "attempt to drop negative size"); + rb_raise(rb_eArgError, "attempt to drop negative size"); } result = rb_ary_subseq(ary, pos, RARRAY_LEN(ary)); @@ -7352,20 +7844,20 @@ rb_ary_drop(VALUE ary, VALUE n) /* * call-seq: - * array.drop_while {|element| ... } -> new_array - * array.drop_while -> new_enumerator - - * Returns a new \Array containing zero or more trailing elements of +self+; - * does not modify +self+. + * drop_while {|element| ... } -> new_array + * drop_while -> new_enumerator * * With a block given, calls the block with each successive element of +self+; * stops if the block returns +false+ or +nil+; - * returns a new Array _omitting_ those elements for which the block returned a truthy value: + * returns a new array _omitting_ those elements for which the block returned a truthy value; + * does not modify +self+: + * * a = [0, 1, 2, 3, 4, 5] * a.drop_while {|element| element < 3 } # => [3, 4, 5] * - * With no block given, returns a new \Enumerator: - * [0, 1].drop_while # => # => #<Enumerator: [0, 1]:drop_while> + * With no block given, returns a new Enumerator. + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -7375,39 +7867,48 @@ rb_ary_drop_while(VALUE ary) RETURN_ENUMERATOR(ary, 0, 0); for (i = 0; i < RARRAY_LEN(ary); i++) { - if (!RTEST(rb_yield(RARRAY_AREF(ary, i)))) break; + if (!RTEST(rb_yield(RARRAY_AREF(ary, i)))) break; } return rb_ary_drop(ary, LONG2FIX(i)); } /* * call-seq: - * array.any? -> true or false - * array.any? {|element| ... } -> true or false - * array.any?(obj) -> true or false + * any? -> true or false + * any?(object) -> true or false + * any? {|element| ... } -> true or false * - * Returns +true+ if any element of +self+ meets a given criterion. + * Returns whether for any element of +self+, a given criterion is satisfied. * - * With no block given and no argument, returns +true+ if +self+ has any truthy element, - * +false+ otherwise: - * [nil, 0, false].any? # => true - * [nil, false].any? # => false - * [].any? # => false + * With no block and no argument, returns whether any element of +self+ is truthy: * - * With a block given and no argument, calls the block with each element in +self+; - * returns +true+ if the block returns any truthy value, +false+ otherwise: - * [0, 1, 2].any? {|element| element > 1 } # => true - * [0, 1, 2].any? {|element| element > 2 } # => false + * [nil, false, []].any? # => true # Array object is truthy. + * [nil, false, {}].any? # => true # Hash object is truthy. + * [nil, false, ''].any? # => true # String object is truthy. + * [nil, false].any? # => false # Nil and false are not truthy. * - * If argument +obj+ is given, returns +true+ if +obj+.<tt>===</tt> any element, - * +false+ otherwise: - * ['food', 'drink'].any?(/foo/) # => true - * ['food', 'drink'].any?(/bar/) # => false - * [].any?(/foo/) # => false - * [0, 1, 2].any?(1) # => true - * [0, 1, 2].any?(3) # => false + * With argument +object+ given, + * returns whether <tt>object === ele</tt> for any element +ele+ in +self+: + * + * [nil, false, 0].any?(0) # => true + * [nil, false, 1].any?(0) # => false + * [nil, false, 'food'].any?(/foo/) # => true + * [nil, false, 'food'].any?(/bar/) # => false + * + * With a block given, + * calls the block with each element in +self+; + * returns whether the block returns any truthy value: + * + * [0, 1, 2].any? {|ele| ele < 1 } # => true + * [0, 1, 2].any? {|ele| ele < 0 } # => false * - * Related: Enumerable#any? + * With both a block and argument +object+ given, + * ignores the block and uses +object+ as above. + * + * <b>Special case</b>: returns +false+ if +self+ is empty + * (regardless of any given argument or block). + * + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ static VALUE @@ -7421,9 +7922,9 @@ rb_ary_any_p(int argc, VALUE *argv, VALUE ary) if (rb_block_given_p()) { rb_warn("given block not used"); } - for (i = 0; i < RARRAY_LEN(ary); ++i) { - if (RTEST(rb_funcall(argv[0], idEqq, 1, RARRAY_AREF(ary, i)))) return Qtrue; - } + for (i = 0; i < RARRAY_LEN(ary); ++i) { + if (RTEST(rb_funcall(argv[0], idEqq, 1, RARRAY_AREF(ary, i)))) return Qtrue; + } } else if (!rb_block_given_p()) { for (i = 0; i < len; ++i) { @@ -7431,40 +7932,50 @@ rb_ary_any_p(int argc, VALUE *argv, VALUE ary) } } else { - for (i = 0; i < RARRAY_LEN(ary); ++i) { - if (RTEST(rb_yield(RARRAY_AREF(ary, i)))) return Qtrue; - } + for (i = 0; i < RARRAY_LEN(ary); ++i) { + if (RTEST(rb_yield(RARRAY_AREF(ary, i)))) return Qtrue; + } } return Qfalse; } /* * call-seq: - * array.all? -> true or false - * array.all? {|element| ... } -> true or false - * array.all?(obj) -> true or false + * all? -> true or false + * all?(object) -> true or false + * all? {|element| ... } -> true or false * - * Returns +true+ if all elements of +self+ meet a given criterion. + * Returns whether for every element of +self+, + * a given criterion is satisfied. * - * With no block given and no argument, returns +true+ if +self+ contains only truthy elements, - * +false+ otherwise: - * [0, 1, :foo].all? # => true - * [0, nil, 2].all? # => false - * [].all? # => true + * With no block and no argument, + * returns whether every element of +self+ is truthy: + * + * [[], {}, '', 0, 0.0, Object.new].all? # => true # All truthy objects. + * [[], {}, '', 0, 0.0, nil].all? # => false # nil is not truthy. + * [[], {}, '', 0, 0.0, false].all? # => false # false is not truthy. * - * With a block given and no argument, calls the block with each element in +self+; - * returns +true+ if the block returns only truthy values, +false+ otherwise: - * [0, 1, 2].all? { |element| element < 3 } # => true - * [0, 1, 2].all? { |element| element < 2 } # => false + * With argument +object+ given, returns whether <tt>object === ele</tt> + * for every element +ele+ in +self+: * - * If argument +obj+ is given, returns +true+ if <tt>obj.===</tt> every element, +false+ otherwise: + * [0, 0, 0].all?(0) # => true + * [0, 1, 2].all?(1) # => false * ['food', 'fool', 'foot'].all?(/foo/) # => true - * ['food', 'drink'].all?(/bar/) # => false - * [].all?(/foo/) # => true - * [0, 0, 0].all?(0) # => true - * [0, 1, 2].all?(1) # => false + * ['food', 'drink'].all?(/foo/) # => false + * + * With a block given, calls the block with each element in +self+; + * returns whether the block returns only truthy values: + * + * [0, 1, 2].all? { |ele| ele < 3 } # => true + * [0, 1, 2].all? { |ele| ele < 2 } # => false * - * Related: Enumerable#all? + * With both a block and argument +object+ given, + * ignores the block and uses +object+ as above. + * + * <b>Special case</b>: returns +true+ if +self+ is empty + * (regardless of any given argument or block). + * + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ static VALUE @@ -7497,31 +8008,35 @@ rb_ary_all_p(int argc, VALUE *argv, VALUE ary) /* * call-seq: - * array.none? -> true or false - * array.none? {|element| ... } -> true or false - * array.none?(obj) -> true or false + * none? -> true or false + * none?(object) -> true or false + * none? {|element| ... } -> true or false * - * Returns +true+ if no element of +self+ meet a given criterion. + * Returns +true+ if no element of +self+ meets a given criterion, +false+ otherwise. * * With no block given and no argument, returns +true+ if +self+ has no truthy elements, * +false+ otherwise: - * [nil, false].none? # => true + * + * [nil, false].none? # => true * [nil, 0, false].none? # => false - * [].none? # => true + * [].none? # => true * - * With a block given and no argument, calls the block with each element in +self+; - * returns +true+ if the block returns no truthy value, +false+ otherwise: - * [0, 1, 2].none? {|element| element > 3 } # => true - * [0, 1, 2].none? {|element| element > 1 } # => false + * With argument +object+ given, returns +false+ if for any element +element+, + * <tt>object === element</tt>; +true+ otherwise: * - * If argument +obj+ is given, returns +true+ if <tt>obj.===</tt> no element, +false+ otherwise: * ['food', 'drink'].none?(/bar/) # => true * ['food', 'drink'].none?(/foo/) # => false - * [].none?(/foo/) # => true - * [0, 1, 2].none?(3) # => true - * [0, 1, 2].none?(1) # => false + * [].none?(/foo/) # => true + * [0, 1, 2].none?(3) # => true + * [0, 1, 2].none?(1) # => false + * + * With a block given, calls the block with each element in +self+; + * returns +true+ if the block returns no truthy value, +false+ otherwise: + * + * [0, 1, 2].none? {|element| element > 3 } # => true + * [0, 1, 2].none? {|element| element > 1 } # => false * - * Related: Enumerable#none? + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ static VALUE @@ -7554,27 +8069,30 @@ rb_ary_none_p(int argc, VALUE *argv, VALUE ary) /* * call-seq: - * array.one? -> true or false - * array.one? {|element| ... } -> true or false - * array.one?(obj) -> true or false + * one? -> true or false + * one? {|element| ... } -> true or false + * one?(object) -> true or false * * Returns +true+ if exactly one element of +self+ meets a given criterion. * * With no block given and no argument, returns +true+ if +self+ has exactly one truthy element, * +false+ otherwise: + * * [nil, 0].one? # => true * [0, 0].one? # => false * [nil, nil].one? # => false * [].one? # => false * - * With a block given and no argument, calls the block with each element in +self+; + * With a block given, calls the block with each element in +self+; * returns +true+ if the block a truthy value for exactly one element, +false+ otherwise: + * * [0, 1, 2].one? {|element| element > 0 } # => false * [0, 1, 2].one? {|element| element > 1 } # => true * [0, 1, 2].one? {|element| element > 2 } # => false * - * If argument +obj+ is given, returns +true+ if <tt>obj.===</tt> exactly one element, + * With argument +object+ given, returns +true+ if for exactly one element +element+, <tt>object === element</tt>; * +false+ otherwise: + * * [0, 1, 2].one?(0) # => true * [0, 0, 1].one?(0) # => false * [1, 1, 2].one?(0) # => false @@ -7582,7 +8100,7 @@ rb_ary_none_p(int argc, VALUE *argv, VALUE ary) * ['food', 'drink'].one?(/foo/) # => true * [].one?(/foo/) # => false * - * Related: Enumerable#one? + * Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. */ static VALUE @@ -7625,19 +8143,22 @@ rb_ary_one_p(int argc, VALUE *argv, VALUE ary) /* * call-seq: - * array.dig(index, *identifiers) -> object + * dig(index, *identifiers) -> object * - * Finds and returns the object in nested objects - * that is specified by +index+ and +identifiers+. - * The nested objects may be instances of various classes. + * Finds and returns the object in nested object + * specified by +index+ and +identifiers+; + * the nested objects may be instances of various classes. * See {Dig Methods}[rdoc-ref:dig_methods.rdoc]. * * Examples: + * * a = [:foo, [:bar, :baz, [:bat, :bam]]] * a.dig(1) # => [:bar, :baz, [:bat, :bam]] * a.dig(1, 2) # => [:bat, :bam] * a.dig(1, 2, 0) # => :bat * a.dig(1, 2, 3) # => nil + * + * Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. */ static VALUE @@ -7655,7 +8176,7 @@ finish_exact_sum(long n, VALUE r, VALUE v, int z) { if (n != 0) v = rb_fix_plus(LONG2FIX(n), v); - if (r != Qundef) { + if (!UNDEF_P(r)) { v = rb_rational_plus(r, v); } else if (!n && z) { @@ -7666,35 +8187,43 @@ finish_exact_sum(long n, VALUE r, VALUE v, int z) /* * call-seq: - * array.sum(init = 0) -> object - * array.sum(init = 0) {|element| ... } -> object + * sum(init = 0) -> object + * sum(init = 0) {|element| ... } -> object + * + * With no block given, returns the sum of +init+ and all elements of +self+; + * for array +array+ and value +init+, equivalent to: * - * When no block is given, returns the object equivalent to: * sum = init * array.each {|element| sum += element } * sum - * For example, <tt>[e1, e2, e3].sum</tt> returns <tt>init + e1 + e2 + e3</tt>. + * + * For example, <tt>[e0, e1, e2].sum</tt> returns <tt>init + e0 + e1 + e2</tt>. * * Examples: - * a = [0, 1, 2, 3] - * a.sum # => 6 - * a.sum(100) # => 106 * - * The elements need not be numeric, but must be <tt>+</tt>-compatible - * with each other and with +init+: - * a = ['abc', 'def', 'ghi'] - * a.sum('jkl') # => "jklabcdefghi" + * [0, 1, 2, 3].sum # => 6 + * [0, 1, 2, 3].sum(100) # => 106 + * ['abc', 'def', 'ghi'].sum('jkl') # => "jklabcdefghi" + * [[:foo, :bar], ['foo', 'bar']].sum([2, 3]) + * # => [2, 3, :foo, :bar, "foo", "bar"] + * + * The +init+ value and elements need not be numeric, but must all be <tt>+</tt>-compatible: * - * When a block is given, it is called with each element - * and the block's return value (instead of the element itself) is used as the addend: - * a = ['zero', 1, :two] - * s = a.sum('Coerced and concatenated: ') {|element| element.to_s } - * s # => "Coerced and concatenated: zero1two" + * # Raises TypeError: Array can't be coerced into Integer. + * [[:foo, :bar], ['foo', 'bar']].sum(2) + * + * With a block given, calls the block with each element of +self+; + * the block's return value (instead of the element itself) is used as the addend: + * + * ['zero', 1, :two].sum('Coerced and concatenated: ') {|element| element.to_s } + * # => "Coerced and concatenated: zero1two" * * Notes: + * * - Array#join and Array#flatten may be faster than Array#sum - * for an \Array of Strings or an \Array of Arrays. + * for an array of strings or an array of arrays. * - Array#sum method may not respect method redefinition of "+" methods such as Integer#+. + * */ static VALUE @@ -7713,6 +8242,12 @@ rb_ary_sum(int argc, VALUE *argv, VALUE ary) n = 0; r = Qundef; + + if (!FIXNUM_P(v) && !RB_BIGNUM_TYPE_P(v) && !RB_TYPE_P(v, T_RATIONAL)) { + i = 0; + goto init_is_a_value; + } + for (i = 0; i < RARRAY_LEN(ary); i++) { e = RARRAY_AREF(ary, i); if (block_given) @@ -7727,7 +8262,7 @@ rb_ary_sum(int argc, VALUE *argv, VALUE ary) else if (RB_BIGNUM_TYPE_P(e)) v = rb_big_plus(e, v); else if (RB_TYPE_P(e, T_RATIONAL)) { - if (r == Qundef) + if (UNDEF_P(r)) r = e; else r = rb_rational_plus(r, e); @@ -7797,6 +8332,7 @@ rb_ary_sum(int argc, VALUE *argv, VALUE ary) } goto has_some_value; + init_is_a_value: for (; i < RARRAY_LEN(ary); i++) { e = RARRAY_AREF(ary, i); if (block_given) @@ -7807,6 +8343,7 @@ rb_ary_sum(int argc, VALUE *argv, VALUE ary) return v; } +/* :nodoc: */ static VALUE rb_ary_deconstruct(VALUE ary) { @@ -7814,82 +8351,154 @@ rb_ary_deconstruct(VALUE ary) } /* - * An \Array is an ordered, integer-indexed collection of objects, - * called _elements_. Any object may be an \Array element. + * An \Array object is an ordered, integer-indexed collection of objects, + * called _elements_; + * the object represents + * an {array data structure}[https://en.wikipedia.org/wiki/Array_(data_structure)]. + * + * An element may be any object (even another array); + * elements may be any mixture of objects of different types. + * + * Important data structures that use arrays include: + * + * - {Coordinate vector}[https://en.wikipedia.org/wiki/Coordinate_vector]. + * - {Matrix}[https://en.wikipedia.org/wiki/Matrix_(mathematics)]. + * - {Heap}[https://en.wikipedia.org/wiki/Heap_(data_structure)]. + * - {Hash table}[https://en.wikipedia.org/wiki/Hash_table]. + * - {Deque (double-ended queue)}[https://en.wikipedia.org/wiki/Double-ended_queue]. + * - {Queue}[https://en.wikipedia.org/wiki/Queue_(abstract_data_type)]. + * - {Stack}[https://en.wikipedia.org/wiki/Stack_(abstract_data_type)]. + * + * There are also array-like data structures: + * + * - {Associative array}[https://en.wikipedia.org/wiki/Associative_array] (see Hash). + * - {Directory}[https://en.wikipedia.org/wiki/Directory_(computing)] (see Dir). + * - {Environment}[https://en.wikipedia.org/wiki/Environment_variable] (see ENV). + * - {Set}[https://en.wikipedia.org/wiki/Set_(abstract_data_type)] (see Set). + * - {String}[https://en.wikipedia.org/wiki/String_(computer_science)] (see String). * * == \Array Indexes * * \Array indexing starts at 0, as in C or Java. * - * A positive index is an offset from the first element: + * A non-negative index is an offset from the first element: + * * - Index 0 indicates the first element. * - Index 1 indicates the second element. * - ... * * A negative index is an offset, backwards, from the end of the array: + * * - Index -1 indicates the last element. * - Index -2 indicates the next-to-last element. * - ... * - * A non-negative index is <i>in range</i> if it is smaller than + * + * === In-Range and Out-of-Range Indexes + * + * A non-negative index is <i>in range</i> if and only if it is smaller than * the size of the array. For a 3-element array: + * * - Indexes 0 through 2 are in range. * - Index 3 is out of range. * - * A negative index is <i>in range</i> if its absolute value is + * A negative index is <i>in range</i> if and only if its absolute value is * not larger than the size of the array. For a 3-element array: + * * - Indexes -1 through -3 are in range. * - Index -4 is out of range. * - * == Creating Arrays - * - * You can create an \Array object explicitly with: - * - * - An {array literal}[doc/syntax/literals_rdoc.html#label-Array+Literals]. - * - * You can convert certain objects to Arrays with: - * - * - \Method {Array}[Kernel.html#method-i-Array]. - * - * An \Array can contain different types of objects. For - * example, the array below contains an Integer, a String and a Float: - * - * ary = [1, "two", 3.0] #=> [1, "two", 3.0] - * - * An array can also be created by calling Array.new with zero, one - * (the initial size of the Array) or two arguments (the initial size and a - * default object). - * - * ary = Array.new #=> [] - * Array.new(3) #=> [nil, nil, nil] - * Array.new(3, true) #=> [true, true, true] - * - * Note that the second argument populates the array with references to the - * same object. Therefore, it is only recommended in cases when you need to - * instantiate arrays with natively immutable objects such as Symbols, - * numbers, true or false. - * - * To create an array with separate objects a block can be passed instead. - * This method is safe to use with mutable objects such as hashes, strings or - * other arrays: + * === Effective Index * - * Array.new(4) {Hash.new} #=> [{}, {}, {}, {}] - * Array.new(4) {|i| i.to_s } #=> ["0", "1", "2", "3"] + * Although the effective index into an array is always an integer, + * some methods (both within class \Array and elsewhere) + * accept one or more non-integer arguments that are + * {integer-convertible objects}[rdoc-ref:implicit_conversion.rdoc@Integer-Convertible+Objects]. * - * This is also a quick way to build up multi-dimensional arrays: - * - * empty_table = Array.new(3) {Array.new(3)} - * #=> [[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]] + * == Creating Arrays * - * An array can also be created by using the Array() method, provided by - * Kernel, which tries to call #to_ary, then #to_a on its argument. + * You can create an \Array object explicitly with: * - * Array({:a => "a", :b => "b"}) #=> [[:a, "a"], [:b, "b"]] + * - An {array literal}[rdoc-ref:syntax/literals.rdoc@Array+Literals]: + * + * [1, 'one', :one, [2, 'two', :two]] + * + * - A {%w or %W string-array Literal}[rdoc-ref:syntax/literals.rdoc@25w+and+-25W-3A+String-Array+Literals]: + * + * %w[foo bar baz] # => ["foo", "bar", "baz"] + * %w[1 % *] # => ["1", "%", "*"] + * + * - A {%i or %I symbol-array Literal}[rdoc-ref:syntax/literals.rdoc@25i+and+-25I-3A+Symbol-Array+Literals]: + * + * %i[foo bar baz] # => [:foo, :bar, :baz] + * %i[1 % *] # => [:"1", :%, :*] + * + * - Method Kernel#Array: + * + * Array(["a", "b"]) # => ["a", "b"] + * Array(1..5) # => [1, 2, 3, 4, 5] + * Array(key: :value) # => [[:key, :value]] + * Array(nil) # => [] + * Array(1) # => [1] + * Array({:a => "a", :b => "b"}) # => [[:a, "a"], [:b, "b"]] + * + * - Method Array.new: + * + * Array.new # => [] + * Array.new(3) # => [nil, nil, nil] + * Array.new(4) {Hash.new} # => [{}, {}, {}, {}] + * Array.new(3, true) # => [true, true, true] + * + * Note that the last example above populates the array + * with references to the same object. + * This is recommended only in cases where that object is a natively immutable object + * such as a symbol, a numeric, +nil+, +true+, or +false+. + * + * Another way to create an array with various objects, using a block; + * this usage is safe for mutable objects such as hashes, strings or + * other arrays: + * + * Array.new(4) {|i| i.to_s } # => ["0", "1", "2", "3"] + * + * Here is a way to create a multi-dimensional array: + * + * Array.new(3) {Array.new(3)} + * # => [[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]] + * + * A number of Ruby methods, both in the core and in the standard library, + * provide instance method +to_a+, which converts an object to an array. + * + * - ARGF#to_a + * - Array#to_a + * - Enumerable#to_a + * - Hash#to_a + * - MatchData#to_a + * - NilClass#to_a + * - OptionParser#to_a + * - Range#to_a + * - Set#to_a + * - Struct#to_a + * - Time#to_a + * - Benchmark::Tms#to_a + * - CSV::Table#to_a + * - Enumerator::Lazy#to_a + * - Gem::List#to_a + * - Gem::NameTuple#to_a + * - Gem::Platform#to_a + * - Gem::RequestSet::Lockfile::Tokenizer#to_a + * - Gem::SourceList#to_a + * - OpenSSL::X509::Extension#to_a + * - OpenSSL::X509::Name#to_a + * - Racc::ISet#to_a + * - Rinda::RingFinger#to_a + * - Ripper::Lexer::Elem#to_a + * - RubyVM::InstructionSequence#to_a + * - YAML::DBM#to_a * * == Example Usage * - * In addition to the methods it mixes in through the Enumerable module, the - * Array class has proprietary methods for accessing, searching and otherwise + * In addition to the methods it mixes in through the Enumerable module, + * class \Array has proprietary methods for accessing, searching and otherwise * manipulating arrays. * * Some of the more common ones are illustrated below. @@ -7937,9 +8546,9 @@ rb_ary_deconstruct(VALUE ary) * * arr.drop(3) #=> [4, 5, 6] * - * == Obtaining Information about an Array + * == Obtaining Information about an \Array * - * Arrays keep track of their own length at all times. To query an array + * An array keeps track of its own length at all times. To query an array * about the number of elements it contains, use #length, #count or #size. * * browsers = ['Chrome', 'Firefox', 'Safari', 'Opera', 'IE'] @@ -7954,7 +8563,7 @@ rb_ary_deconstruct(VALUE ary) * * browsers.include?('Konqueror') #=> false * - * == Adding Items to Arrays + * == Adding Items to an \Array * * Items can be added to the end of an array by using either #push or #<< * @@ -7975,7 +8584,7 @@ rb_ary_deconstruct(VALUE ary) * arr.insert(3, 'orange', 'pear', 'grapefruit') * #=> [0, 1, 2, "orange", "pear", "grapefruit", "apple", 3, 4, 5, 6] * - * == Removing Items from an Array + * == Removing Items from an \Array * * The method #pop removes the last element in an array and returns it: * @@ -8015,11 +8624,11 @@ rb_ary_deconstruct(VALUE ary) * arr = [2, 5, 6, 556, 6, 6, 8, 9, 0, 123, 556] * arr.uniq #=> [2, 5, 6, 556, 8, 9, 0, 123] * - * == Iterating over Arrays + * == Iterating over an \Array * - * Like all classes that include the Enumerable module, Array has an each + * Like all classes that include the Enumerable module, class \Array has an each * method, which defines what elements should be iterated over and how. In - * case of Array's #each, all elements in the Array instance are yielded to + * case of Array#each, all elements in +self+ are yielded to * the supplied block in sequence. * * Note that this operation leaves the array unchanged. @@ -8045,7 +8654,8 @@ rb_ary_deconstruct(VALUE ary) * arr.map! {|a| a**2} #=> [1, 4, 9, 16, 25] * arr #=> [1, 4, 9, 16, 25] * - * == Selecting Items from an Array + * + * == Selecting Items from an \Array * * Elements can be selected from an array according to criteria defined in a * block. The selection can happen in a destructive or a non-destructive @@ -8078,192 +8688,181 @@ rb_ary_deconstruct(VALUE ary) * * == What's Here * - * First, what's elsewhere. \Class \Array: + * First, what's elsewhere. Class \Array: * - * - Inherits from {class Object}[Object.html#class-Object-label-What-27s+Here]. - * - Includes {module Enumerable}[Enumerable.html#module-Enumerable-label-What-27s+Here], + * - Inherits from {class Object}[rdoc-ref:Object@What-27s+Here]. + * - Includes {module Enumerable}[rdoc-ref:Enumerable@What-27s+Here], * which provides dozens of additional methods. * * Here, class \Array provides methods that are useful for: * - * - {Creating an Array}[#class-Array-label-Methods+for+Creating+an+Array] - * - {Querying}[#class-Array-label-Methods+for+Querying] - * - {Comparing}[#class-Array-label-Methods+for+Comparing] - * - {Fetching}[#class-Array-label-Methods+for+Fetching] - * - {Assigning}[#class-Array-label-Methods+for+Assigning] - * - {Deleting}[#class-Array-label-Methods+for+Deleting] - * - {Combining}[#class-Array-label-Methods+for+Combining] - * - {Iterating}[#class-Array-label-Methods+for+Iterating] - * - {Converting}[#class-Array-label-Methods+for+Converting] - * - {And more....}[#class-Array-label-Other+Methods] + * - {Creating an Array}[rdoc-ref:Array@Methods+for+Creating+an+Array] + * - {Querying}[rdoc-ref:Array@Methods+for+Querying] + * - {Comparing}[rdoc-ref:Array@Methods+for+Comparing] + * - {Fetching}[rdoc-ref:Array@Methods+for+Fetching] + * - {Assigning}[rdoc-ref:Array@Methods+for+Assigning] + * - {Deleting}[rdoc-ref:Array@Methods+for+Deleting] + * - {Combining}[rdoc-ref:Array@Methods+for+Combining] + * - {Iterating}[rdoc-ref:Array@Methods+for+Iterating] + * - {Converting}[rdoc-ref:Array@Methods+for+Converting] + * - {And more....}[rdoc-ref:Array@Other+Methods] + * + * === Methods for Creating an \Array * - * === Methods for Creating an Array + * - ::[]: Returns a new array populated with given objects. + * - ::new: Returns a new array. + * - ::try_convert: Returns a new array created from a given object. * - * ::[]:: Returns a new array populated with given objects. - * ::new:: Returns a new array. - * ::try_convert:: Returns a new array created from a given object. + * See also {Creating Arrays}[rdoc-ref:Array@Creating+Arrays]. * * === Methods for Querying * - * #length, #size:: Returns the count of elements. - * #include?:: Returns whether any element <tt>==</tt> a given object. - * #empty?:: Returns whether there are no elements. - * #all?:: Returns whether all elements meet a given criterion. - * #any?:: Returns whether any element meets a given criterion. - * #none?:: Returns whether no element <tt>==</tt> a given object. - * #one?:: Returns whether exactly one element <tt>==</tt> a given object. - * #count:: Returns the count of elements that meet a given criterion. - * #find_index, #index:: Returns the index of the first element that meets a given criterion. - * #rindex:: Returns the index of the last element that meets a given criterion. - * #hash:: Returns the integer hash code. + * - #all?: Returns whether all elements meet a given criterion. + * - #any?: Returns whether any element meets a given criterion. + * - #count: Returns the count of elements that meet a given criterion. + * - #empty?: Returns whether there are no elements. + * - #find_index (aliased as #index): Returns the index of the first element that meets a given criterion. + * - #hash: Returns the integer hash code. + * - #include?: Returns whether any element <tt>==</tt> a given object. + * - #length (aliased as #size): Returns the count of elements. + * - #none?: Returns whether no element <tt>==</tt> a given object. + * - #one?: Returns whether exactly one element <tt>==</tt> a given object. + * - #rindex: Returns the index of the last element that meets a given criterion. * * === Methods for Comparing - * {#<=>}[#method-i-3C-3D-3E]:: Returns -1, 0, or 1 - * as +self+ is less than, equal to, or greater than a given object. - * {#==}[#method-i-3D-3D]:: Returns whether each element in +self+ is <tt>==</tt> to the - * corresponding element in a given object. - * #eql?:: Returns whether each element in +self+ is <tt>eql?</tt> to the corresponding - * element in a given object. + * + * - #<=>: Returns -1, 0, or 1, as +self+ is less than, equal to, or greater than a given object. + * - #==: Returns whether each element in +self+ is <tt>==</tt> to the corresponding element in a given object. + * - #eql?: Returns whether each element in +self+ is <tt>eql?</tt> to the corresponding element in a given object. * === Methods for Fetching * * These methods do not modify +self+. * - * #[]:: Returns one or more elements. - * #fetch:: Returns the element at a given offset. - * #first:: Returns one or more leading elements. - * #last:: Returns one or more trailing elements. - * #max:: Returns one or more maximum-valued elements, - * as determined by <tt><=></tt> or a given block. - * #max:: Returns one or more minimum-valued elements, - * as determined by <tt><=></tt> or a given block. - * #minmax:: Returns the minimum-valued and maximum-valued elements, - * as determined by <tt><=></tt> or a given block. - * #assoc:: Returns the first element that is an array - * whose first element <tt>==</tt> a given object. - * #rassoc:: Returns the first element that is an array - * whose second element <tt>==</tt> a given object. - * #at:: Returns the element at a given offset. - * #values_at:: Returns the elements at given offsets. - * #dig:: Returns the object in nested objects - * that is specified by a given index and additional arguments. - * #drop:: Returns trailing elements as determined by a given index. - * #take:: Returns leading elements as determined by a given index. - * #drop_while:: Returns trailing elements as determined by a given block. - * #take_while:: Returns leading elements as determined by a given block. - * #slice:: Returns consecutive elements as determined by a given argument. - * #sort:: Returns all elements in an order determined by <tt><=></tt> or a given block. - * #reverse:: Returns all elements in reverse order. - * #compact:: Returns an array containing all non-+nil+ elements. - * #select, #filter:: Returns an array containing elements selected by a given block. - * #uniq:: Returns an array containing non-duplicate elements. - * #rotate:: Returns all elements with some rotated from one end to the other. - * #bsearch:: Returns an element selected via a binary search - * as determined by a given block. - * #bsearch_index:: Returns the index of an element selected via a binary search - * as determined by a given block. - * #sample:: Returns one or more random elements. - * #shuffle:: Returns elements in a random order. + * - #[] (aliased as #slice): Returns consecutive elements as determined by a given argument. + * - #assoc: Returns the first element that is an array whose first element <tt>==</tt> a given object. + * - #at: Returns the element at a given offset. + * - #bsearch: Returns an element selected via a binary search as determined by a given block. + * - #bsearch_index: Returns the index of an element selected via a binary search as determined by a given block. + * - #compact: Returns an array containing all non-+nil+ elements. + * - #dig: Returns the object in nested objects that is specified by a given index and additional arguments. + * - #drop: Returns trailing elements as determined by a given index. + * - #drop_while: Returns trailing elements as determined by a given block. + * - #fetch: Returns the element at a given offset. + * - #fetch_values: Returns elements at given offsets. + * - #first: Returns one or more leading elements. + * - #last: Returns one or more trailing elements. + * - #max: Returns one or more maximum-valued elements, as determined by <tt>#<=></tt> or a given block. + * - #min: Returns one or more minimum-valued elements, as determined by <tt>#<=></tt> or a given block. + * - #minmax: Returns the minimum-valued and maximum-valued elements, as determined by <tt>#<=></tt> or a given block. + * - #rassoc: Returns the first element that is an array whose second element <tt>==</tt> a given object. + * - #reject: Returns an array containing elements not rejected by a given block. + * - #reverse: Returns all elements in reverse order. + * - #rotate: Returns all elements with some rotated from one end to the other. + * - #sample: Returns one or more random elements. + * - #select (aliased as #filter): Returns an array containing elements selected by a given block. + * - #shuffle: Returns elements in a random order. + * - #sort: Returns all elements in an order determined by <tt>#<=></tt> or a given block. + * - #take: Returns leading elements as determined by a given index. + * - #take_while: Returns leading elements as determined by a given block. + * - #uniq: Returns an array containing non-duplicate elements. + * - #values_at: Returns the elements at given offsets. * * === Methods for Assigning * * These methods add, replace, or reorder elements in +self+. * - * #[]=:: Assigns specified elements with a given object. - * #push, #append, #<<:: Appends trailing elements. - * #unshift, #prepend:: Prepends leading elements. - * #insert:: Inserts given objects at a given offset; does not replace elements. - * #concat:: Appends all elements from given arrays. - * #fill:: Replaces specified elements with specified objects. - * #replace:: Replaces the content of +self+ with the content of a given array. - * #reverse!:: Replaces +self+ with its elements reversed. - * #rotate!:: Replaces +self+ with its elements rotated. - * #shuffle!:: Replaces +self+ with its elements in random order. - * #sort!:: Replaces +self+ with its elements sorted, - * as determined by <tt><=></tt> or a given block. - * #sort_by!:: Replaces +self+ with its elements sorted, as determined by a given block. + * - #<<: Appends an element. + * - #[]=: Assigns specified elements with a given object. + * - #concat: Appends all elements from given arrays. + * - #fill: Replaces specified elements with specified objects. + * - #flatten!: Replaces each nested array in +self+ with the elements from that array. + * - #initialize_copy (aliased as #replace): Replaces the content of +self+ with the content of a given array. + * - #insert: Inserts given objects at a given offset; does not replace elements. + * - #push (aliased as #append): Appends elements. + * - #reverse!: Replaces +self+ with its elements reversed. + * - #rotate!: Replaces +self+ with its elements rotated. + * - #shuffle!: Replaces +self+ with its elements in random order. + * - #sort!: Replaces +self+ with its elements sorted, as determined by <tt>#<=></tt> or a given block. + * - #sort_by!: Replaces +self+ with its elements sorted, as determined by a given block. + * - #unshift (aliased as #prepend): Prepends leading elements. * * === Methods for Deleting * * Each of these methods removes elements from +self+: * - * #pop:: Removes and returns the last element. - * #shift:: Removes and returns the first element. - * #compact!:: Removes all non-+nil+ elements. - * #delete:: Removes elements equal to a given object. - * #delete_at:: Removes the element at a given offset. - * #delete_if:: Removes elements specified by a given block. - * #keep_if:: Removes elements not specified by a given block. - * #reject!:: Removes elements specified by a given block. - * #select!, #filter!:: Removes elements not specified by a given block. - * #slice!:: Removes and returns a sequence of elements. - * #uniq!:: Removes duplicates. + * - #clear: Removes all elements. + * - #compact!: Removes all +nil+ elements. + * - #delete: Removes elements equal to a given object. + * - #delete_at: Removes the element at a given offset. + * - #delete_if: Removes elements specified by a given block. + * - #keep_if: Removes elements not specified by a given block. + * - #pop: Removes and returns the last element. + * - #reject!: Removes elements specified by a given block. + * - #select! (aliased as #filter!): Removes elements not specified by a given block. + * - #shift: Removes and returns the first element. + * - #slice!: Removes and returns a sequence of elements. + * - #uniq!: Removes duplicates. * * === Methods for Combining * - * {#&}[#method-i-26]:: Returns an array containing elements found both in +self+ and a given array. - * #intersection:: Returns an array containing elements found both in +self+ - * and in each given array. - * #+:: Returns an array containing all elements of +self+ followed by all elements of a given array. - * #-:: Returns an array containiing all elements of +self+ that are not found in a given array. - * {#|}[#method-i-7C]:: Returns an array containing all elements of +self+ and all elements of a given array, - * duplicates removed. - * #union:: Returns an array containing all elements of +self+ and all elements of given arrays, - * duplicates removed. - * #difference:: Returns an array containing all elements of +self+ that are not found - * in any of the given arrays.. - * #product:: Returns or yields all combinations of elements from +self+ and given arrays. + * - #&: Returns an array containing elements found both in +self+ and a given array. + * - #+: Returns an array containing all elements of +self+ followed by all elements of a given array. + * - #-: Returns an array containing all elements of +self+ that are not found in a given array. + * - #|: Returns an array containing all element of +self+ and all elements of a given array, duplicates removed. + * - #difference: Returns an array containing all elements of +self+ that are not found in any of the given arrays.. + * - #intersection: Returns an array containing elements found both in +self+ and in each given array. + * - #product: Returns or yields all combinations of elements from +self+ and given arrays. + * - #reverse: Returns an array containing all elements of +self+ in reverse order. + * - #union: Returns an array containing all elements of +self+ and all elements of given arrays, duplicates removed. * * === Methods for Iterating * - * #each:: Passes each element to a given block. - * #reverse_each:: Passes each element, in reverse order, to a given block. - * #each_index:: Passes each element index to a given block. - * #cycle:: Calls a given block with each element, then does so again, - * for a specified number of times, or forever. - * #combination:: Calls a given block with combinations of elements of +self+; - * a combination does not use the same element more than once. - * #permutation:: Calls a given block with permutations of elements of +self+; - * a permutation does not use the same element more than once. - * #repeated_combination:: Calls a given block with combinations of elements of +self+; - * a combination may use the same element more than once. - * #repeated_permutation:: Calls a given block with permutations of elements of +self+; - * a permutation may use the same element more than once. + * - #combination: Calls a given block with combinations of elements of +self+; a combination does not use the same element more than once. + * - #cycle: Calls a given block with each element, then does so again, for a specified number of times, or forever. + * - #each: Passes each element to a given block. + * - #each_index: Passes each element index to a given block. + * - #permutation: Calls a given block with permutations of elements of +self+; a permutation does not use the same element more than once. + * - #repeated_combination: Calls a given block with combinations of elements of +self+; a combination may use the same element more than once. + * - #repeated_permutation: Calls a given block with permutations of elements of +self+; a permutation may use the same element more than once. + * - #reverse_each: Passes each element, in reverse order, to a given block. * * === Methods for Converting * - * #map, #collect:: Returns an array containing the block return-value for each element. - * #map!, #collect!:: Replaces each element with a block return-value. - * #flatten:: Returns an array that is a recursive flattening of +self+. - * #flatten!:: Replaces each nested array in +self+ with the elements from that array. - * #inspect, #to_s:: Returns a new String containing the elements. - * #join:: Returns a newsString containing the elements joined by the field separator. - * #to_a:: Returns +self+ or a new array containing all elements. - * #to_ary:: Returns +self+. - * #to_h:: Returns a new hash formed from the elements. - * #transpose:: Transposes +self+, which must be an array of arrays. - * #zip:: Returns a new array of arrays containing +self+ and given arrays; - * follow the link for details. + * - #collect (aliased as #map): Returns an array containing the block return-value for each element. + * - #collect! (aliased as #map!): Replaces each element with a block return-value. + * - #flatten: Returns an array that is a recursive flattening of +self+. + * - #inspect (aliased as #to_s): Returns a new String containing the elements. + * - #join: Returns a new String containing the elements joined by the field separator. + * - #to_a: Returns +self+ or a new array containing all elements. + * - #to_ary: Returns +self+. + * - #to_h: Returns a new hash formed from the elements. + * - #transpose: Transposes +self+, which must be an array of arrays. + * - #zip: Returns a new array of arrays containing +self+ and given arrays. * * === Other Methods * - * #*:: Returns one of the following: - * - With integer argument +n+, a new array that is the concatenation - * of +n+ copies of +self+. - * - With string argument +field_separator+, a new string that is equivalent to - * <tt>join(field_separator)</tt>. - * #abbrev:: Returns a hash of unambiguous abbreviations for elements. - * #pack:: Packs the elements into a binary sequence. - * #sum:: Returns a sum of elements according to either <tt>+</tt> or a given block. + * - #*: Returns one of the following: + * + * - With integer argument +n+, a new array that is the concatenation + * of +n+ copies of +self+. + * - With string argument +field_separator+, a new string that is equivalent to + * <tt>join(field_separator)</tt>. + * + * - #pack: Packs the elements into a binary sequence. + * - #sum: Returns a sum of elements according to either <tt>+</tt> or a given block. */ void Init_Array(void) { + fake_ary_flags = init_fake_ary_flags(); + rb_cArray = rb_define_class("Array", rb_cObject); rb_include_module(rb_cArray, rb_mEnumerable); rb_define_alloc_func(rb_cArray, empty_ary_alloc); + rb_define_singleton_method(rb_cArray, "new", rb_ary_s_new, -1); rb_define_singleton_method(rb_cArray, "[]", rb_ary_s_create, -1); rb_define_singleton_method(rb_cArray, "try_convert", rb_ary_s_try_convert, 1); rb_define_method(rb_cArray, "initialize", rb_ary_initialize, -1); @@ -8283,8 +8882,6 @@ Init_Array(void) rb_define_method(rb_cArray, "[]=", rb_ary_aset, -1); rb_define_method(rb_cArray, "at", rb_ary_at, 1); rb_define_method(rb_cArray, "fetch", rb_ary_fetch, -1); - rb_define_method(rb_cArray, "first", rb_ary_first, -1); - rb_define_method(rb_cArray, "last", rb_ary_last, -1); rb_define_method(rb_cArray, "concat", rb_ary_concat_multi, -1); rb_define_method(rb_cArray, "union", rb_ary_union_multi, -1); rb_define_method(rb_cArray, "difference", rb_ary_difference_multi, -1); @@ -8304,6 +8901,9 @@ Init_Array(void) rb_define_method(rb_cArray, "length", rb_ary_length, 0); rb_define_method(rb_cArray, "size", rb_ary_length, 0); rb_define_method(rb_cArray, "empty?", rb_ary_empty_p, 0); + rb_define_method(rb_cArray, "find", rb_ary_find, -1); + rb_define_method(rb_cArray, "detect", rb_ary_find, -1); + rb_define_method(rb_cArray, "rfind", rb_ary_rfind, -1); rb_define_method(rb_cArray, "find_index", rb_ary_index, -1); rb_define_method(rb_cArray, "index", rb_ary_index, -1); rb_define_method(rb_cArray, "rindex", rb_ary_rindex, -1); @@ -8381,8 +8981,12 @@ Init_Array(void) rb_define_method(rb_cArray, "one?", rb_ary_one_p, -1); rb_define_method(rb_cArray, "dig", rb_ary_dig, -1); rb_define_method(rb_cArray, "sum", rb_ary_sum, -1); + rb_define_method(rb_cArray, "freeze", rb_ary_freeze, 0); rb_define_method(rb_cArray, "deconstruct", rb_ary_deconstruct, 0); + + rb_cArray_empty_frozen = RB_OBJ_SET_SHAREABLE(rb_ary_freeze(rb_ary_new())); + rb_vm_register_global_object(rb_cArray_empty_frozen); } #include "array.rbinc" @@ -1,62 +1,97 @@ class Array # call-seq: - # array.shuffle!(random: Random) -> array + # shuffle!(random: Random) -> self # - # Shuffles the elements of +self+ in place. - # a = [1, 2, 3] #=> [1, 2, 3] - # a.shuffle! #=> [2, 3, 1] - # a #=> [2, 3, 1] + # Shuffles all elements in +self+ into a random order, + # as selected by the object given by the keyword argument +random+. + # Returns +self+: # - # The optional +random+ argument will be used as the random number generator: - # a.shuffle!(random: Random.new(1)) #=> [1, 3, 2] + # a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + # a.shuffle! # => [5, 3, 8, 7, 6, 1, 9, 4, 2, 0] + # a.shuffle! # => [9, 4, 0, 6, 2, 8, 1, 5, 3, 7] + # + # Duplicate elements are included: + # + # a = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] + # a.shuffle! # => [1, 0, 0, 1, 1, 0, 1, 0, 0, 1] + # a.shuffle! # => [0, 1, 0, 1, 1, 0, 1, 0, 1, 0] + # + # The object given with the keyword argument +random+ is used as the random number generator. + # + # Related: see {Methods for Assigning}[rdoc-ref:Array@Methods+for+Assigning]. def shuffle!(random: Random) Primitive.rb_ary_shuffle_bang(random) end # call-seq: - # array.shuffle(random: Random) -> new_ary + # shuffle(random: Random) -> new_array + # + # Returns a new array containing all elements from +self+ in a random order, + # as selected by the object given by the keyword argument +random+: + # + # a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + # a.shuffle # => [0, 8, 1, 9, 6, 3, 4, 7, 2, 5] + # a.shuffle # => [8, 9, 0, 5, 1, 2, 6, 4, 7, 3] # - # Returns a new array with elements of +self+ shuffled. - # a = [1, 2, 3] #=> [1, 2, 3] - # a.shuffle #=> [2, 3, 1] - # a #=> [1, 2, 3] + # Duplicate elements are included: # - # The optional +random+ argument will be used as the random number generator: - # a.shuffle(random: Random.new(1)) #=> [1, 3, 2] + # a = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] + # a.shuffle # => [1, 0, 1, 1, 0, 0, 1, 0, 0, 1] + # a.shuffle # => [1, 1, 0, 0, 0, 1, 1, 0, 0, 1] + # + # The object given with the keyword argument +random+ is used as the random number generator. + # + # Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. def shuffle(random: Random) Primitive.rb_ary_shuffle(random) end # call-seq: - # array.sample(random: Random) -> object - # array.sample(n, random: Random) -> new_ary + # sample(random: Random) -> object + # sample(count, random: Random) -> new_ary # - # Returns random elements from +self+. + # Returns random elements from +self+, + # as selected by the object given by the keyword argument +random+. # - # When no arguments are given, returns a random element from +self+: - # a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - # a.sample # => 3 - # a.sample # => 8 - # If +self+ is empty, returns +nil+. + # With no argument +count+ given, returns one random element from +self+: + # + # a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + # a.sample # => 3 + # a.sample # => 8 + # + # Returns +nil+ if +self+ is empty: + # + # [].sample # => nil + # + # With a non-negative numeric argument +count+ given, + # returns a new array containing +count+ random elements from +self+: + # + # a.sample(3) # => [8, 9, 2] + # a.sample(6) # => [9, 6, 0, 3, 1, 4] + # + # The order of the result array is unrelated to the order of +self+. + # + # Returns a new empty array if +self+ is empty: + # + # [].sample(4) # => [] + # + # May return duplicates in +self+: + # + # a = [1, 1, 1, 2, 2, 3] + # a.sample(a.size) # => [1, 1, 3, 2, 1, 2] # - # When argument +n+ is given, returns a new \Array containing +n+ random - # elements from +self+: - # a.sample(3) # => [8, 9, 2] - # a.sample(6) # => [9, 6, 10, 3, 1, 4] # Returns no more than <tt>a.size</tt> elements # (because no new duplicates are introduced): - # a.sample(a.size * 2) # => [6, 4, 1, 8, 5, 9, 10, 2, 3, 7] - # But +self+ may contain duplicates: - # a = [1, 1, 1, 2, 2, 3] - # a.sample(a.size * 2) # => [1, 1, 3, 2, 1, 2] - # The argument +n+ must be a non-negative numeric value. - # The order of the result array is unrelated to the order of +self+. - # Returns a new empty \Array if +self+ is empty. # - # The optional +random+ argument will be used as the random number generator: - # a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - # a.sample(random: Random.new(1)) #=> 6 - # a.sample(4, random: Random.new(1)) #=> [6, 10, 9, 2] + # a.sample(50) # => [6, 4, 1, 8, 5, 9, 0, 2, 3, 7] + # + # The object given with the keyword argument +random+ is used as the random number generator: + # + # a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + # a.sample(random: Random.new(1)) # => 6 + # a.sample(4, random: Random.new(1)) # => [6, 10, 9, 2] + # + # Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. def sample(n = (ary = false), random: Random) if Primitive.mandatory_only? # Primitive.cexpr! %{ rb_ary_sample(self, rb_cRandom, Qfalse, Qfalse) } @@ -66,4 +101,187 @@ class Array Primitive.ary_sample(random, n, ary) end end + + # call-seq: + # first -> object or nil + # first(count) -> new_array + # + # Returns elements from +self+, or +nil+; does not modify +self+. + # + # With no argument given, returns the first element (if available): + # + # a = [:foo, 'bar', 2] + # a.first # => :foo + # a # => [:foo, "bar", 2] + # + # If +self+ is empty, returns +nil+. + # + # [].first # => nil + # + # With a non-negative integer argument +count+ given, + # returns the first +count+ elements (as available) in a new array: + # + # a.first(0) # => [] + # a.first(2) # => [:foo, "bar"] + # a.first(50) # => [:foo, "bar", 2] + # + # Related: see {Methods for Querying}[rdoc-ref:Array@Methods+for+Querying]. + def first n = unspecified = true + if Primitive.mandatory_only? + Primitive.attr! :leaf + Primitive.cexpr! %q{ ary_first(self) } + else + if unspecified + Primitive.cexpr! %q{ ary_first(self) } + else + Primitive.cexpr! %q{ ary_take_first_or_last_n(self, NUM2LONG(n), ARY_TAKE_FIRST) } + end + end + end + + # call-seq: + # last -> last_object or nil + # last(count) -> new_array + # + # Returns elements from +self+, or +nil+; +self+ is not modified. + # + # With no argument given, returns the last element, or +nil+ if +self+ is empty: + # + # a = [:foo, 'bar', 2] + # a.last # => 2 + # a # => [:foo, "bar", 2] + # [].last # => nil + # + # + # With non-negative integer argument +count+ given, + # returns a new array containing the trailing +count+ elements of +self+, as available: + # + # a = [:foo, 'bar', 2] + # a.last(2) # => ["bar", 2] + # a.last(50) # => [:foo, "bar", 2] + # a.last(0) # => [] + # [].last(3) # => [] + # + # Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. + def last n = unspecified = true + if Primitive.mandatory_only? + Primitive.attr! :leaf + Primitive.cexpr! %q{ ary_last(self) } + else + if unspecified + Primitive.cexpr! %q{ ary_last(self) } + else + Primitive.cexpr! %q{ ary_take_first_or_last_n(self, NUM2LONG(n), ARY_TAKE_LAST) } + end + end + end + + # call-seq: + # fetch_values(*indexes) -> new_array + # fetch_values(*indexes) { |index| ... } -> new_array + # + # With no block given, returns a new array containing the elements of +self+ + # at the offsets specified by +indexes+. Each of the +indexes+ must be an + # {integer-convertible object}[rdoc-ref:implicit_conversion.rdoc@Integer-Convertible+Objects]: + # + # a = [:foo, :bar, :baz] + # a.fetch_values(2, 0) # => [:baz, :foo] + # a.fetch_values(2.1, 0) # => [:baz, :foo] + # a.fetch_values # => [] + # + # For a negative index, counts backwards from the end of the array: + # + # a.fetch_values(-2, -1) # [:bar, :baz] + # + # When no block is given, raises an exception if any index is out of range. + # + # With a block given, for each index: + # + # - If the index is in range, uses an element of +self+ (as above). + # - Otherwise, calls the block with the index and uses the block's return value. + # + # Example: + # + # a = [:foo, :bar, :baz] + # a.fetch_values(1, 0, 42, 777) { |index| index.to_s } + # # => [:bar, :foo, "42", "777"] + # + # Related: see {Methods for Fetching}[rdoc-ref:Array@Methods+for+Fetching]. + def fetch_values(*indexes, &block) + indexes.map! { |i| fetch(i, &block) } + indexes + end + + with_jit do + if Primitive.rb_builtin_basic_definition_p(:each) + undef :each + + def each # :nodoc: + Primitive.attr! :inline_block, :c_trace + + unless defined?(yield) + return Primitive.cexpr! 'SIZED_ENUMERATOR(self, 0, 0, ary_enum_length)' + end + _i = 0 + value = nil + while Primitive.cexpr!(%q{ ary_fetch_next(self, LOCAL_PTR(_i), LOCAL_PTR(value)) }) + yield value + end + self + end + end + + if Primitive.rb_builtin_basic_definition_p(:map) + undef :map + + def map # :nodoc: + Primitive.attr! :inline_block, :c_trace + + unless defined?(yield) + return Primitive.cexpr! 'SIZED_ENUMERATOR(self, 0, 0, ary_enum_length)' + end + + _i = 0 + value = nil + result = Primitive.ary_sized_alloc + while Primitive.cexpr!(%q{ ary_fetch_next(self, LOCAL_PTR(_i), LOCAL_PTR(value)) }) + value = yield(value) + Primitive.cexpr!(%q{ rb_ary_push(result, value) }) + end + result + end + + if Primitive.rb_builtin_basic_definition_p(:collect) + undef :collect + alias collect map + end + end + + if Primitive.rb_builtin_basic_definition_p(:select) + undef :select + + def select # :nodoc: + Primitive.attr! :inline_block, :c_trace + + unless defined?(yield) + return Primitive.cexpr! 'SIZED_ENUMERATOR(self, 0, 0, ary_enum_length)' + end + + _i = 0 + value = nil + result = Primitive.ary_sized_alloc + while Primitive.cexpr!(%q{ ary_fetch_next(self, LOCAL_PTR(_i), LOCAL_PTR(value)) }) + if yield value + Primitive.cexpr!(%q{ rb_ary_push(result, value) }) + end + end + result + end + + if Primitive.rb_builtin_basic_definition_p(:filter) + undef :filter + alias filter select + end + end + end end @@ -1,6 +1,6 @@ /* indent-tabs-mode: nil */ #include "internal.h" -#include "internal/parse.h" +#include "internal/ruby_parser.h" #include "internal/symbol.h" #include "internal/warnings.h" #include "iseq.h" @@ -14,9 +14,10 @@ static VALUE rb_mAST; static VALUE rb_cNode; +static VALUE rb_cLocation; struct ASTNodeData { - rb_ast_t *ast; + VALUE ast_value; const NODE *node; }; @@ -24,14 +25,20 @@ static void node_gc_mark(void *ptr) { struct ASTNodeData *data = (struct ASTNodeData *)ptr; - rb_gc_mark((VALUE)data->ast); + rb_gc_mark(data->ast_value); } static size_t node_memsize(const void *ptr) { struct ASTNodeData *data = (struct ASTNodeData *)ptr; - return rb_ast_memsize(data->ast); + size_t size = sizeof(struct ASTNodeData); + if (data->ast_value) { + rb_ast_t *ast = rb_ruby_ast_data_get(data->ast_value); + size += rb_ast_memsize(ast); + } + + return size; } static const rb_data_type_t rb_node_type = { @@ -41,31 +48,57 @@ static const rb_data_type_t rb_node_type = { RUBY_TYPED_FREE_IMMEDIATELY, }; +struct ASTLocationData { + int first_lineno; + int first_column; + int last_lineno; + int last_column; +}; + +static void +location_gc_mark(void *ptr) +{ +} + +static size_t +location_memsize(const void *ptr) +{ + return sizeof(struct ASTLocationData); +} + +static const rb_data_type_t rb_location_type = { + "AST/location", + {location_gc_mark, RUBY_TYPED_DEFAULT_FREE, location_memsize,}, + 0, 0, + RUBY_TYPED_FREE_IMMEDIATELY, +}; + + static VALUE rb_ast_node_alloc(VALUE klass); static void -setup_node(VALUE obj, rb_ast_t *ast, const NODE *node) +setup_node(VALUE obj, VALUE ast_value, const NODE *node) { struct ASTNodeData *data; TypedData_Get_Struct(obj, struct ASTNodeData, &rb_node_type, data); - data->ast = ast; + data->ast_value = ast_value; data->node = node; } static VALUE -ast_new_internal(rb_ast_t *ast, const NODE *node) +ast_new_internal(VALUE ast_value, const NODE *node) { VALUE obj; obj = rb_ast_node_alloc(rb_cNode); - setup_node(obj, ast, node); + setup_node(obj, ast_value, node); return obj; } -static VALUE rb_ast_parse_str(VALUE str, VALUE keep_script_lines); -static VALUE rb_ast_parse_file(VALUE path, VALUE keep_script_lines); +static VALUE rb_ast_parse_str(VALUE str, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens); +static VALUE rb_ast_parse_file(VALUE path, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens); static VALUE ast_parse_new(void) @@ -74,83 +107,77 @@ ast_parse_new(void) } static VALUE -ast_parse_done(rb_ast_t *ast) +ast_parse_done(VALUE ast_value) { + rb_ast_t *ast = rb_ruby_ast_data_get(ast_value); + if (!ast->body.root) { rb_ast_dispose(ast); rb_exc_raise(GET_EC()->errinfo); } - return ast_new_internal(ast, (NODE *)ast->body.root); + return ast_new_internal(ast_value, (NODE *)ast->body.root); } static VALUE -ast_s_parse(rb_execution_context_t *ec, VALUE module, VALUE str, VALUE keep_script_lines) +setup_vparser(VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens) { - return rb_ast_parse_str(str, keep_script_lines); + VALUE vparser = ast_parse_new(); + if (RTEST(keep_script_lines)) rb_parser_set_script_lines(vparser); + if (RTEST(error_tolerant)) rb_parser_error_tolerant(vparser); + if (RTEST(keep_tokens)) rb_parser_keep_tokens(vparser); + return vparser; } static VALUE -rb_ast_parse_str(VALUE str, VALUE keep_script_lines) +ast_s_parse(rb_execution_context_t *ec, VALUE module, VALUE str, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens) { - rb_ast_t *ast = 0; + return rb_ast_parse_str(str, keep_script_lines, error_tolerant, keep_tokens); +} +static VALUE +rb_ast_parse_str(VALUE str, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens) +{ + VALUE ast_value = Qnil; StringValue(str); - VALUE vparser = ast_parse_new(); - if (RTEST(keep_script_lines)) rb_parser_keep_script_lines(vparser); - ast = rb_parser_compile_string_path(vparser, Qnil, str, 1); - return ast_parse_done(ast); + VALUE vparser = setup_vparser(keep_script_lines, error_tolerant, keep_tokens); + ast_value = rb_parser_compile_string_path(vparser, Qnil, str, 1); + return ast_parse_done(ast_value); } static VALUE -ast_s_parse_file(rb_execution_context_t *ec, VALUE module, VALUE path, VALUE keep_script_lines) +ast_s_parse_file(rb_execution_context_t *ec, VALUE module, VALUE path, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens) { - return rb_ast_parse_file(path, keep_script_lines); + return rb_ast_parse_file(path, keep_script_lines, error_tolerant, keep_tokens); } static VALUE -rb_ast_parse_file(VALUE path, VALUE keep_script_lines) +rb_ast_parse_file(VALUE path, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens) { VALUE f; - rb_ast_t *ast = 0; + VALUE ast_value = Qnil; rb_encoding *enc = rb_utf8_encoding(); - FilePathValue(path); f = rb_file_open_str(path, "r"); rb_funcall(f, rb_intern("set_encoding"), 2, rb_enc_from_encoding(enc), rb_str_new_cstr("-")); - VALUE vparser = ast_parse_new(); - if (RTEST(keep_script_lines)) rb_parser_keep_script_lines(vparser); - ast = rb_parser_compile_file_path(vparser, Qnil, f, 1); + VALUE vparser = setup_vparser(keep_script_lines, error_tolerant, keep_tokens); + ast_value = rb_parser_compile_file_path(vparser, Qnil, f, 1); rb_io_close(f); - return ast_parse_done(ast); + return ast_parse_done(ast_value); } static VALUE -lex_array(VALUE array, int index) +rb_ast_parse_array(VALUE array, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens) { - VALUE str = rb_ary_entry(array, index); - if (!NIL_P(str)) { - StringValue(str); - if (!rb_enc_asciicompat(rb_enc_get(str))) { - rb_raise(rb_eArgError, "invalid source encoding"); - } - } - return str; -} - -static VALUE -rb_ast_parse_array(VALUE array, VALUE keep_script_lines) -{ - rb_ast_t *ast = 0; + VALUE ast_value = Qnil; array = rb_check_array_type(array); - VALUE vparser = ast_parse_new(); - if (RTEST(keep_script_lines)) rb_parser_keep_script_lines(vparser); - ast = rb_parser_compile_generic(vparser, lex_array, Qnil, array, 1); - return ast_parse_done(ast); + VALUE vparser = setup_vparser(keep_script_lines, error_tolerant, keep_tokens); + ast_value = rb_parser_compile_array(vparser, Qnil, array, 1); + return ast_parse_done(ast_value); } -static VALUE node_children(rb_ast_t*, const NODE*); +static VALUE node_children(VALUE, const NODE*); static VALUE node_find(VALUE self, const int node_id) @@ -162,7 +189,7 @@ node_find(VALUE self, const int node_id) if (nd_node_id(data->node) == node_id) return self; - ary = node_children(data->ast, data->node); + ary = node_children(data->ast_value, data->node); for (i = 0; i < RARRAY_LEN(ary); i++) { VALUE child = RARRAY_AREF(ary, i); @@ -179,21 +206,24 @@ node_find(VALUE self, const int node_id) extern VALUE rb_e_script; static VALUE -script_lines(VALUE path) +node_id_for_backtrace_location(rb_execution_context_t *ec, VALUE module, VALUE location) { - VALUE hash, lines; - ID script_lines; - CONST_ID(script_lines, "SCRIPT_LINES__"); - if (!rb_const_defined_at(rb_cObject, script_lines)) return Qnil; - hash = rb_const_get_at(rb_cObject, script_lines); - if (!RB_TYPE_P(hash, T_HASH)) return Qnil; - lines = rb_hash_lookup(hash, path); - if (!RB_TYPE_P(lines, T_ARRAY)) return Qnil; - return lines; + int node_id; + + if (!rb_frame_info_p(location)) { + rb_raise(rb_eTypeError, "Thread::Backtrace::Location object expected"); + } + + node_id = rb_get_node_id_from_frame_info(location); + if (node_id == -1) { + return Qnil; + } + + return INT2NUM(node_id); } static VALUE -ast_s_of(rb_execution_context_t *ec, VALUE module, VALUE body, VALUE keep_script_lines) +ast_s_of(rb_execution_context_t *ec, VALUE module, VALUE body, VALUE keep_script_lines, VALUE error_tolerant, VALUE keep_tokens) { VALUE node, lines = Qnil; const rb_iseq_t *iseq; @@ -215,14 +245,19 @@ ast_s_of(rb_execution_context_t *ec, VALUE module, VALUE body, VALUE keep_script iseq = rb_method_iseq(body); } if (iseq) { - node_id = iseq->body->location.node_id; + node_id = ISEQ_BODY(iseq)->location.node_id; } } if (!iseq) { return Qnil; } - lines = iseq->body->variable.script_lines; + + if (ISEQ_BODY(iseq)->prism) { + rb_raise(rb_eRuntimeError, "cannot get AST for ISEQ compiled by prism"); + } + + lines = ISEQ_BODY(iseq)->variable.script_lines; VALUE path = rb_iseq_path(iseq); int e_option = RSTRING_LEN(path) == 2 && memcmp(RSTRING_PTR(path), "-e", 2) == 0; @@ -231,14 +266,14 @@ ast_s_of(rb_execution_context_t *ec, VALUE module, VALUE body, VALUE keep_script rb_raise(rb_eArgError, "cannot get AST for method defined in eval"); } - if (!NIL_P(lines) || !NIL_P(lines = script_lines(path))) { - node = rb_ast_parse_array(lines, keep_script_lines); + if (!NIL_P(lines)) { + node = rb_ast_parse_array(lines, keep_script_lines, error_tolerant, keep_tokens); } else if (e_option) { - node = rb_ast_parse_str(rb_e_script, keep_script_lines); + node = rb_ast_parse_str(rb_e_script, keep_script_lines, error_tolerant, keep_tokens); } else { - node = rb_ast_parse_file(path, keep_script_lines); + node = rb_ast_parse_file(path, keep_script_lines, error_tolerant, keep_tokens); } return node_find(node, node_id); @@ -277,10 +312,10 @@ ast_node_node_id(rb_execution_context_t *ec, VALUE self) return INT2FIX(nd_node_id(data->node)); } -#define NEW_CHILD(ast, node) node ? ast_new_internal(ast, node) : Qnil +#define NEW_CHILD(ast_value, node) (node ? ast_new_internal(ast_value, node) : Qnil) static VALUE -rb_ary_new_from_node_args(rb_ast_t *ast, long n, ...) +rb_ary_new_from_node_args(VALUE ast_value, long n, ...) { va_list ar; VALUE ary; @@ -292,39 +327,57 @@ rb_ary_new_from_node_args(rb_ast_t *ast, long n, ...) for (i=0; i<n; i++) { NODE *node; node = va_arg(ar, NODE *); - rb_ary_push(ary, NEW_CHILD(ast, node)); + rb_ary_push(ary, NEW_CHILD(ast_value, node)); } va_end(ar); return ary; } static VALUE -dump_block(rb_ast_t *ast, const NODE *node) +dump_block(VALUE ast_value, const struct RNode_BLOCK *node) { VALUE ary = rb_ary_new(); do { - rb_ary_push(ary, NEW_CHILD(ast, node->nd_head)); + rb_ary_push(ary, NEW_CHILD(ast_value, node->nd_head)); } while (node->nd_next && nd_type_p(node->nd_next, NODE_BLOCK) && - (node = node->nd_next, 1)); + (node = RNODE_BLOCK(node->nd_next), 1)); if (node->nd_next) { - rb_ary_push(ary, NEW_CHILD(ast, node->nd_next)); + rb_ary_push(ary, NEW_CHILD(ast_value, node->nd_next)); } return ary; } static VALUE -dump_array(rb_ast_t *ast, const NODE *node) +dump_array(VALUE ast_value, const struct RNode_LIST *node) { VALUE ary = rb_ary_new(); - rb_ary_push(ary, NEW_CHILD(ast, node->nd_head)); + rb_ary_push(ary, NEW_CHILD(ast_value, node->nd_head)); while (node->nd_next && nd_type_p(node->nd_next, NODE_LIST)) { - node = node->nd_next; - rb_ary_push(ary, NEW_CHILD(ast, node->nd_head)); + node = RNODE_LIST(node->nd_next); + rb_ary_push(ary, NEW_CHILD(ast_value, node->nd_head)); + } + rb_ary_push(ary, NEW_CHILD(ast_value, node->nd_next)); + + return ary; +} + +static VALUE +dump_parser_array(VALUE ast_value, rb_parser_ary_t *p_ary) +{ + VALUE ary; + + if (p_ary->data_type != PARSER_ARY_DATA_NODE) { + rb_bug("unexpected rb_parser_ary_data_type: %d", p_ary->data_type); + } + + ary = rb_ary_new(); + + for (long i = 0; i < p_ary->len; i++) { + rb_ary_push(ary, NEW_CHILD(ast_value, p_ary->data[i])); } - rb_ary_push(ary, NEW_CHILD(ast, node->nd_next)); return ary; } @@ -346,305 +399,341 @@ no_name_rest(void) } static VALUE -rest_arg(rb_ast_t *ast, const NODE *rest_arg) +rest_arg(VALUE ast_value, const NODE *rest_arg) { - return NODE_NAMED_REST_P(rest_arg) ? NEW_CHILD(ast, rest_arg) : no_name_rest(); + return NODE_NAMED_REST_P(rest_arg) ? NEW_CHILD(ast_value, rest_arg) : no_name_rest(); } static VALUE -node_children(rb_ast_t *ast, const NODE *node) +node_children(VALUE ast_value, const NODE *node) { - char name[DECIMAL_SIZE_OF_BITS(sizeof(long) * CHAR_BIT) + 2]; /* including '$' */ + char name[sizeof("$") + DECIMAL_SIZE_OF(long)]; enum node_type type = nd_type(node); switch (type) { case NODE_BLOCK: - return dump_block(ast, node); + return dump_block(ast_value, RNODE_BLOCK(node)); case NODE_IF: - return rb_ary_new_from_node_args(ast, 3, node->nd_cond, node->nd_body, node->nd_else); + return rb_ary_new_from_node_args(ast_value, 3, RNODE_IF(node)->nd_cond, RNODE_IF(node)->nd_body, RNODE_IF(node)->nd_else); case NODE_UNLESS: - return rb_ary_new_from_node_args(ast, 3, node->nd_cond, node->nd_body, node->nd_else); + return rb_ary_new_from_node_args(ast_value, 3, RNODE_UNLESS(node)->nd_cond, RNODE_UNLESS(node)->nd_body, RNODE_UNLESS(node)->nd_else); case NODE_CASE: - return rb_ary_new_from_node_args(ast, 2, node->nd_head, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_CASE(node)->nd_head, RNODE_CASE(node)->nd_body); case NODE_CASE2: - return rb_ary_new_from_node_args(ast, 2, node->nd_head, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_CASE2(node)->nd_head, RNODE_CASE2(node)->nd_body); case NODE_CASE3: - return rb_ary_new_from_node_args(ast, 2, node->nd_head, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_CASE3(node)->nd_head, RNODE_CASE3(node)->nd_body); case NODE_WHEN: - return rb_ary_new_from_node_args(ast, 3, node->nd_head, node->nd_body, node->nd_next); + return rb_ary_new_from_node_args(ast_value, 3, RNODE_WHEN(node)->nd_head, RNODE_WHEN(node)->nd_body, RNODE_WHEN(node)->nd_next); case NODE_IN: - return rb_ary_new_from_node_args(ast, 3, node->nd_head, node->nd_body, node->nd_next); + return rb_ary_new_from_node_args(ast_value, 3, RNODE_IN(node)->nd_head, RNODE_IN(node)->nd_body, RNODE_IN(node)->nd_next); case NODE_WHILE: case NODE_UNTIL: - return rb_ary_push(rb_ary_new_from_node_args(ast, 2, node->nd_cond, node->nd_body), - RBOOL(node->nd_state)); + return rb_ary_push(rb_ary_new_from_node_args(ast_value, 2, RNODE_WHILE(node)->nd_cond, RNODE_WHILE(node)->nd_body), + RBOOL(RNODE_WHILE(node)->nd_state)); case NODE_ITER: case NODE_FOR: - return rb_ary_new_from_node_args(ast, 2, node->nd_iter, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_ITER(node)->nd_iter, RNODE_ITER(node)->nd_body); case NODE_FOR_MASGN: - return rb_ary_new_from_node_args(ast, 1, node->nd_var); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_FOR_MASGN(node)->nd_var); case NODE_BREAK: + return rb_ary_new_from_node_args(ast_value, 1, RNODE_BREAK(node)->nd_stts); case NODE_NEXT: + return rb_ary_new_from_node_args(ast_value, 1, RNODE_NEXT(node)->nd_stts); case NODE_RETURN: - return rb_ary_new_from_node_args(ast, 1, node->nd_stts); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_RETURN(node)->nd_stts); case NODE_REDO: - return rb_ary_new_from_node_args(ast, 0); + return rb_ary_new_from_node_args(ast_value, 0); case NODE_RETRY: - return rb_ary_new_from_node_args(ast, 0); + return rb_ary_new_from_node_args(ast_value, 0); case NODE_BEGIN: - return rb_ary_new_from_node_args(ast, 1, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_BEGIN(node)->nd_body); case NODE_RESCUE: - return rb_ary_new_from_node_args(ast, 3, node->nd_head, node->nd_resq, node->nd_else); + return rb_ary_new_from_node_args(ast_value, 3, RNODE_RESCUE(node)->nd_head, RNODE_RESCUE(node)->nd_resq, RNODE_RESCUE(node)->nd_else); case NODE_RESBODY: - return rb_ary_new_from_node_args(ast, 3, node->nd_args, node->nd_body, node->nd_head); + return rb_ary_new_from_node_args(ast_value, 4, RNODE_RESBODY(node)->nd_args, RNODE_RESBODY(node)->nd_exc_var, RNODE_RESBODY(node)->nd_body, RNODE_RESBODY(node)->nd_next); case NODE_ENSURE: - return rb_ary_new_from_node_args(ast, 2, node->nd_head, node->nd_ensr); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_ENSURE(node)->nd_head, RNODE_ENSURE(node)->nd_ensr); case NODE_AND: case NODE_OR: { VALUE ary = rb_ary_new(); while (1) { - rb_ary_push(ary, NEW_CHILD(ast, node->nd_1st)); - if (!node->nd_2nd || !nd_type_p(node->nd_2nd, type)) + rb_ary_push(ary, NEW_CHILD(ast_value, RNODE_AND(node)->nd_1st)); + if (!RNODE_AND(node)->nd_2nd || !nd_type_p(RNODE_AND(node)->nd_2nd, type)) break; - node = node->nd_2nd; + node = RNODE_AND(node)->nd_2nd; } - rb_ary_push(ary, NEW_CHILD(ast, node->nd_2nd)); + rb_ary_push(ary, NEW_CHILD(ast_value, RNODE_AND(node)->nd_2nd)); return ary; } case NODE_MASGN: - if (NODE_NAMED_REST_P(node->nd_args)) { - return rb_ary_new_from_node_args(ast, 3, node->nd_value, node->nd_head, node->nd_args); + if (NODE_NAMED_REST_P(RNODE_MASGN(node)->nd_args)) { + return rb_ary_new_from_node_args(ast_value, 3, RNODE_MASGN(node)->nd_value, RNODE_MASGN(node)->nd_head, RNODE_MASGN(node)->nd_args); } else { - return rb_ary_new_from_args(3, NEW_CHILD(ast, node->nd_value), - NEW_CHILD(ast, node->nd_head), + return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_MASGN(node)->nd_value), + NEW_CHILD(ast_value, RNODE_MASGN(node)->nd_head), no_name_rest()); } case NODE_LASGN: + if (NODE_REQUIRED_KEYWORD_P(RNODE_LASGN(node)->nd_value)) { + return rb_ary_new_from_args(2, var_name(RNODE_LASGN(node)->nd_vid), ID2SYM(rb_intern("NODE_SPECIAL_REQUIRED_KEYWORD"))); + } + return rb_ary_new_from_args(2, var_name(RNODE_LASGN(node)->nd_vid), NEW_CHILD(ast_value, RNODE_LASGN(node)->nd_value)); case NODE_DASGN: + if (NODE_REQUIRED_KEYWORD_P(RNODE_DASGN(node)->nd_value)) { + return rb_ary_new_from_args(2, var_name(RNODE_DASGN(node)->nd_vid), ID2SYM(rb_intern("NODE_SPECIAL_REQUIRED_KEYWORD"))); + } + return rb_ary_new_from_args(2, var_name(RNODE_DASGN(node)->nd_vid), NEW_CHILD(ast_value, RNODE_DASGN(node)->nd_value)); case NODE_IASGN: + return rb_ary_new_from_args(2, var_name(RNODE_IASGN(node)->nd_vid), NEW_CHILD(ast_value, RNODE_IASGN(node)->nd_value)); case NODE_CVASGN: + return rb_ary_new_from_args(2, var_name(RNODE_CVASGN(node)->nd_vid), NEW_CHILD(ast_value, RNODE_CVASGN(node)->nd_value)); case NODE_GASGN: - if (NODE_REQUIRED_KEYWORD_P(node)) { - return rb_ary_new_from_args(2, var_name(node->nd_vid), ID2SYM(rb_intern("NODE_SPECIAL_REQUIRED_KEYWORD"))); - } - return rb_ary_new_from_args(2, var_name(node->nd_vid), NEW_CHILD(ast, node->nd_value)); + return rb_ary_new_from_args(2, var_name(RNODE_GASGN(node)->nd_vid), NEW_CHILD(ast_value, RNODE_GASGN(node)->nd_value)); case NODE_CDECL: - if (node->nd_vid) { - return rb_ary_new_from_args(2, ID2SYM(node->nd_vid), NEW_CHILD(ast, node->nd_value)); + if (RNODE_CDECL(node)->nd_vid) { + return rb_ary_new_from_args(2, ID2SYM(RNODE_CDECL(node)->nd_vid), NEW_CHILD(ast_value, RNODE_CDECL(node)->nd_value)); } - return rb_ary_new_from_args(3, NEW_CHILD(ast, node->nd_else), ID2SYM(node->nd_else->nd_mid), NEW_CHILD(ast, node->nd_value)); + return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_CDECL(node)->nd_else), ID2SYM(RNODE_COLON2(RNODE_CDECL(node)->nd_else)->nd_mid), NEW_CHILD(ast_value, RNODE_CDECL(node)->nd_value)); case NODE_OP_ASGN1: - return rb_ary_new_from_args(4, NEW_CHILD(ast, node->nd_recv), - ID2SYM(node->nd_mid), - NEW_CHILD(ast, node->nd_args->nd_head), - NEW_CHILD(ast, node->nd_args->nd_body)); + return rb_ary_new_from_args(4, NEW_CHILD(ast_value, RNODE_OP_ASGN1(node)->nd_recv), + ID2SYM(RNODE_OP_ASGN1(node)->nd_mid), + NEW_CHILD(ast_value, RNODE_OP_ASGN1(node)->nd_index), + NEW_CHILD(ast_value, RNODE_OP_ASGN1(node)->nd_rvalue)); case NODE_OP_ASGN2: - return rb_ary_new_from_args(5, NEW_CHILD(ast, node->nd_recv), - RBOOL(node->nd_next->nd_aid), - ID2SYM(node->nd_next->nd_vid), - ID2SYM(node->nd_next->nd_mid), - NEW_CHILD(ast, node->nd_value)); + return rb_ary_new_from_args(5, NEW_CHILD(ast_value, RNODE_OP_ASGN2(node)->nd_recv), + RBOOL(RNODE_OP_ASGN2(node)->nd_aid), + ID2SYM(RNODE_OP_ASGN2(node)->nd_vid), + ID2SYM(RNODE_OP_ASGN2(node)->nd_mid), + NEW_CHILD(ast_value, RNODE_OP_ASGN2(node)->nd_value)); case NODE_OP_ASGN_AND: - return rb_ary_new_from_args(3, NEW_CHILD(ast, node->nd_head), ID2SYM(idANDOP), - NEW_CHILD(ast, node->nd_value)); + return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_OP_ASGN_AND(node)->nd_head), ID2SYM(idANDOP), + NEW_CHILD(ast_value, RNODE_OP_ASGN_AND(node)->nd_value)); case NODE_OP_ASGN_OR: - return rb_ary_new_from_args(3, NEW_CHILD(ast, node->nd_head), ID2SYM(idOROP), - NEW_CHILD(ast, node->nd_value)); + return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_OP_ASGN_OR(node)->nd_head), ID2SYM(idOROP), + NEW_CHILD(ast_value, RNODE_OP_ASGN_OR(node)->nd_value)); case NODE_OP_CDECL: - return rb_ary_new_from_args(3, NEW_CHILD(ast, node->nd_head), - ID2SYM(node->nd_aid), - NEW_CHILD(ast, node->nd_value)); + return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_OP_CDECL(node)->nd_head), + ID2SYM(RNODE_OP_CDECL(node)->nd_aid), + NEW_CHILD(ast_value, RNODE_OP_CDECL(node)->nd_value)); case NODE_CALL: + return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_CALL(node)->nd_recv), + ID2SYM(RNODE_CALL(node)->nd_mid), + NEW_CHILD(ast_value, RNODE_CALL(node)->nd_args)); case NODE_OPCALL: + return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_OPCALL(node)->nd_recv), + ID2SYM(RNODE_OPCALL(node)->nd_mid), + NEW_CHILD(ast_value, RNODE_OPCALL(node)->nd_args)); case NODE_QCALL: - return rb_ary_new_from_args(3, NEW_CHILD(ast, node->nd_recv), - ID2SYM(node->nd_mid), - NEW_CHILD(ast, node->nd_args)); + return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_QCALL(node)->nd_recv), + ID2SYM(RNODE_QCALL(node)->nd_mid), + NEW_CHILD(ast_value, RNODE_QCALL(node)->nd_args)); case NODE_FCALL: - return rb_ary_new_from_args(2, ID2SYM(node->nd_mid), - NEW_CHILD(ast, node->nd_args)); + return rb_ary_new_from_args(2, ID2SYM(RNODE_FCALL(node)->nd_mid), + NEW_CHILD(ast_value, RNODE_FCALL(node)->nd_args)); case NODE_VCALL: - return rb_ary_new_from_args(1, ID2SYM(node->nd_mid)); + return rb_ary_new_from_args(1, ID2SYM(RNODE_VCALL(node)->nd_mid)); case NODE_SUPER: - return rb_ary_new_from_node_args(ast, 1, node->nd_args); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_SUPER(node)->nd_args); case NODE_ZSUPER: - return rb_ary_new_from_node_args(ast, 0); + return rb_ary_new_from_node_args(ast_value, 0); case NODE_LIST: - case NODE_VALUES: - return dump_array(ast, node); + return dump_array(ast_value, RNODE_LIST(node)); case NODE_ZLIST: - return rb_ary_new_from_node_args(ast, 0); + return rb_ary_new_from_node_args(ast_value, 0); case NODE_HASH: - return rb_ary_new_from_node_args(ast, 1, node->nd_head); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_HASH(node)->nd_head); case NODE_YIELD: - return rb_ary_new_from_node_args(ast, 1, node->nd_head); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_YIELD(node)->nd_head); case NODE_LVAR: + return rb_ary_new_from_args(1, var_name(RNODE_LVAR(node)->nd_vid)); case NODE_DVAR: - return rb_ary_new_from_args(1, var_name(node->nd_vid)); + return rb_ary_new_from_args(1, var_name(RNODE_DVAR(node)->nd_vid)); case NODE_IVAR: + return rb_ary_new_from_args(1, ID2SYM(RNODE_IVAR(node)->nd_vid)); case NODE_CONST: + return rb_ary_new_from_args(1, ID2SYM(RNODE_CONST(node)->nd_vid)); case NODE_CVAR: + return rb_ary_new_from_args(1, ID2SYM(RNODE_CVAR(node)->nd_vid)); case NODE_GVAR: - return rb_ary_new_from_args(1, ID2SYM(node->nd_vid)); + return rb_ary_new_from_args(1, ID2SYM(RNODE_GVAR(node)->nd_vid)); case NODE_NTH_REF: - snprintf(name, sizeof(name), "$%ld", node->nd_nth); + snprintf(name, sizeof(name), "$%ld", RNODE_NTH_REF(node)->nd_nth); return rb_ary_new_from_args(1, ID2SYM(rb_intern(name))); case NODE_BACK_REF: name[0] = '$'; - name[1] = (char)node->nd_nth; + name[1] = (char)RNODE_BACK_REF(node)->nd_nth; name[2] = '\0'; return rb_ary_new_from_args(1, ID2SYM(rb_intern(name))); + case NODE_MATCH: + return rb_ary_new_from_args(1, rb_node_regx_string_val(node)); case NODE_MATCH2: - if (node->nd_args) { - return rb_ary_new_from_node_args(ast, 3, node->nd_recv, node->nd_value, node->nd_args); + if (RNODE_MATCH2(node)->nd_args) { + return rb_ary_new_from_node_args(ast_value, 3, RNODE_MATCH2(node)->nd_recv, RNODE_MATCH2(node)->nd_value, RNODE_MATCH2(node)->nd_args); } - return rb_ary_new_from_node_args(ast, 2, node->nd_recv, node->nd_value); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_MATCH2(node)->nd_recv, RNODE_MATCH2(node)->nd_value); case NODE_MATCH3: - return rb_ary_new_from_node_args(ast, 2, node->nd_recv, node->nd_value); - case NODE_MATCH: - case NODE_LIT: + return rb_ary_new_from_node_args(ast_value, 2, RNODE_MATCH3(node)->nd_recv, RNODE_MATCH3(node)->nd_value); case NODE_STR: case NODE_XSTR: - return rb_ary_new_from_args(1, node->nd_lit); + return rb_ary_new_from_args(1, rb_node_str_string_val(node)); + case NODE_INTEGER: + return rb_ary_new_from_args(1, rb_node_integer_literal_val(node)); + case NODE_FLOAT: + return rb_ary_new_from_args(1, rb_node_float_literal_val(node)); + case NODE_RATIONAL: + return rb_ary_new_from_args(1, rb_node_rational_literal_val(node)); + case NODE_IMAGINARY: + return rb_ary_new_from_args(1, rb_node_imaginary_literal_val(node)); + case NODE_REGX: + return rb_ary_new_from_args(1, rb_node_regx_string_val(node)); case NODE_ONCE: - return rb_ary_new_from_node_args(ast, 1, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_ONCE(node)->nd_body); case NODE_DSTR: case NODE_DXSTR: case NODE_DREGX: case NODE_DSYM: { - NODE *n = node->nd_next; + struct RNode_LIST *n = RNODE_DSTR(node)->nd_next; VALUE head = Qnil, next = Qnil; if (n) { - head = NEW_CHILD(ast, n->nd_head); - next = NEW_CHILD(ast, n->nd_next); + head = NEW_CHILD(ast_value, n->nd_head); + next = NEW_CHILD(ast_value, n->nd_next); } - return rb_ary_new_from_args(3, node->nd_lit, head, next); + return rb_ary_new_from_args(3, rb_node_dstr_string_val(node), head, next); } + case NODE_SYM: + return rb_ary_new_from_args(1, rb_node_sym_string_val(node)); case NODE_EVSTR: - return rb_ary_new_from_node_args(ast, 1, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_EVSTR(node)->nd_body); case NODE_ARGSCAT: - return rb_ary_new_from_node_args(ast, 2, node->nd_head, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_ARGSCAT(node)->nd_head, RNODE_ARGSCAT(node)->nd_body); case NODE_ARGSPUSH: - return rb_ary_new_from_node_args(ast, 2, node->nd_head, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_ARGSPUSH(node)->nd_head, RNODE_ARGSPUSH(node)->nd_body); case NODE_SPLAT: - return rb_ary_new_from_node_args(ast, 1, node->nd_head); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_SPLAT(node)->nd_head); case NODE_BLOCK_PASS: - return rb_ary_new_from_node_args(ast, 2, node->nd_head, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_BLOCK_PASS(node)->nd_head, RNODE_BLOCK_PASS(node)->nd_body); case NODE_DEFN: - return rb_ary_new_from_args(2, ID2SYM(node->nd_mid), NEW_CHILD(ast, node->nd_defn)); + return rb_ary_new_from_args(2, ID2SYM(RNODE_DEFN(node)->nd_mid), NEW_CHILD(ast_value, RNODE_DEFN(node)->nd_defn)); case NODE_DEFS: - return rb_ary_new_from_args(3, NEW_CHILD(ast, node->nd_recv), ID2SYM(node->nd_mid), NEW_CHILD(ast, node->nd_defn)); + return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_DEFS(node)->nd_recv), ID2SYM(RNODE_DEFS(node)->nd_mid), NEW_CHILD(ast_value, RNODE_DEFS(node)->nd_defn)); case NODE_ALIAS: - return rb_ary_new_from_node_args(ast, 2, node->nd_1st, node->nd_2nd); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_ALIAS(node)->nd_1st, RNODE_ALIAS(node)->nd_2nd); case NODE_VALIAS: - return rb_ary_new_from_args(2, ID2SYM(node->nd_alias), ID2SYM(node->nd_orig)); + return rb_ary_new_from_args(2, ID2SYM(RNODE_VALIAS(node)->nd_alias), ID2SYM(RNODE_VALIAS(node)->nd_orig)); case NODE_UNDEF: - return rb_ary_new_from_node_args(ast, 1, node->nd_undef); + return rb_ary_new_from_args(1, dump_parser_array(ast_value, RNODE_UNDEF(node)->nd_undefs)); case NODE_CLASS: - return rb_ary_new_from_node_args(ast, 3, node->nd_cpath, node->nd_super, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 3, RNODE_CLASS(node)->nd_cpath, RNODE_CLASS(node)->nd_super, RNODE_CLASS(node)->nd_body); case NODE_MODULE: - return rb_ary_new_from_node_args(ast, 2, node->nd_cpath, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_MODULE(node)->nd_cpath, RNODE_MODULE(node)->nd_body); case NODE_SCLASS: - return rb_ary_new_from_node_args(ast, 2, node->nd_recv, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_SCLASS(node)->nd_recv, RNODE_SCLASS(node)->nd_body); case NODE_COLON2: - return rb_ary_new_from_args(2, NEW_CHILD(ast, node->nd_head), ID2SYM(node->nd_mid)); + return rb_ary_new_from_args(2, NEW_CHILD(ast_value, RNODE_COLON2(node)->nd_head), ID2SYM(RNODE_COLON2(node)->nd_mid)); case NODE_COLON3: - return rb_ary_new_from_args(1, ID2SYM(node->nd_mid)); + return rb_ary_new_from_args(1, ID2SYM(RNODE_COLON3(node)->nd_mid)); case NODE_DOT2: case NODE_DOT3: case NODE_FLIP2: case NODE_FLIP3: - return rb_ary_new_from_node_args(ast, 2, node->nd_beg, node->nd_end); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_DOT2(node)->nd_beg, RNODE_DOT2(node)->nd_end); case NODE_SELF: - return rb_ary_new_from_node_args(ast, 0); + return rb_ary_new_from_node_args(ast_value, 0); case NODE_NIL: - return rb_ary_new_from_node_args(ast, 0); + return rb_ary_new_from_node_args(ast_value, 0); case NODE_TRUE: - return rb_ary_new_from_node_args(ast, 0); + return rb_ary_new_from_node_args(ast_value, 0); case NODE_FALSE: - return rb_ary_new_from_node_args(ast, 0); + return rb_ary_new_from_node_args(ast_value, 0); case NODE_ERRINFO: - return rb_ary_new_from_node_args(ast, 0); + return rb_ary_new_from_node_args(ast_value, 0); case NODE_DEFINED: - return rb_ary_new_from_node_args(ast, 1, node->nd_head); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_DEFINED(node)->nd_head); case NODE_POSTEXE: - return rb_ary_new_from_node_args(ast, 1, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_POSTEXE(node)->nd_body); case NODE_ATTRASGN: - return rb_ary_new_from_args(3, NEW_CHILD(ast, node->nd_recv), ID2SYM(node->nd_mid), NEW_CHILD(ast, node->nd_args)); + return rb_ary_new_from_args(3, NEW_CHILD(ast_value, RNODE_ATTRASGN(node)->nd_recv), ID2SYM(RNODE_ATTRASGN(node)->nd_mid), NEW_CHILD(ast_value, RNODE_ATTRASGN(node)->nd_args)); case NODE_LAMBDA: - return rb_ary_new_from_node_args(ast, 1, node->nd_body); + return rb_ary_new_from_node_args(ast_value, 1, RNODE_LAMBDA(node)->nd_body); case NODE_OPT_ARG: - return rb_ary_new_from_node_args(ast, 2, node->nd_body, node->nd_next); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_OPT_ARG(node)->nd_body, RNODE_OPT_ARG(node)->nd_next); case NODE_KW_ARG: - return rb_ary_new_from_node_args(ast, 2, node->nd_body, node->nd_next); + return rb_ary_new_from_node_args(ast_value, 2, RNODE_KW_ARG(node)->nd_body, RNODE_KW_ARG(node)->nd_next); case NODE_POSTARG: - if (NODE_NAMED_REST_P(node->nd_1st)) { - return rb_ary_new_from_node_args(ast, 2, node->nd_1st, node->nd_2nd); + if (NODE_NAMED_REST_P(RNODE_POSTARG(node)->nd_1st)) { + return rb_ary_new_from_node_args(ast_value, 2, RNODE_POSTARG(node)->nd_1st, RNODE_POSTARG(node)->nd_2nd); } return rb_ary_new_from_args(2, no_name_rest(), - NEW_CHILD(ast, node->nd_2nd)); + NEW_CHILD(ast_value, RNODE_POSTARG(node)->nd_2nd)); case NODE_ARGS: { - struct rb_args_info *ainfo = node->nd_ainfo; + struct rb_args_info *ainfo = &RNODE_ARGS(node)->nd_ainfo; return rb_ary_new_from_args(10, INT2NUM(ainfo->pre_args_num), - NEW_CHILD(ast, ainfo->pre_init), - NEW_CHILD(ast, ainfo->opt_args), + NEW_CHILD(ast_value, ainfo->pre_init), + NEW_CHILD(ast_value, (NODE *)ainfo->opt_args), var_name(ainfo->first_post_arg), INT2NUM(ainfo->post_args_num), - NEW_CHILD(ast, ainfo->post_init), + NEW_CHILD(ast_value, ainfo->post_init), (ainfo->rest_arg == NODE_SPECIAL_EXCESSIVE_COMMA ? ID2SYM(rb_intern("NODE_SPECIAL_EXCESSIVE_COMMA")) : var_name(ainfo->rest_arg)), - (ainfo->no_kwarg ? Qfalse : NEW_CHILD(ast, ainfo->kw_args)), - (ainfo->no_kwarg ? Qfalse : NEW_CHILD(ast, ainfo->kw_rest_arg)), + (ainfo->no_kwarg ? Qfalse : NEW_CHILD(ast_value, (NODE *)ainfo->kw_args)), + (ainfo->no_kwarg ? Qfalse : NEW_CHILD(ast_value, ainfo->kw_rest_arg)), var_name(ainfo->block_arg)); } case NODE_SCOPE: { - rb_ast_id_table_t *tbl = node->nd_tbl; + rb_ast_id_table_t *tbl = RNODE_SCOPE(node)->nd_tbl; int i, size = tbl ? tbl->size : 0; VALUE locals = rb_ary_new_capa(size); for (i = 0; i < size; i++) { rb_ary_push(locals, var_name(tbl->ids[i])); } - return rb_ary_new_from_args(3, locals, NEW_CHILD(ast, node->nd_args), NEW_CHILD(ast, node->nd_body)); + return rb_ary_new_from_args(3, locals, NEW_CHILD(ast_value, (NODE *)RNODE_SCOPE(node)->nd_args), NEW_CHILD(ast_value, RNODE_SCOPE(node)->nd_body)); } case NODE_ARYPTN: { - struct rb_ary_pattern_info *apinfo = node->nd_apinfo; - VALUE rest = rest_arg(ast, apinfo->rest_arg); + VALUE rest = rest_arg(ast_value, RNODE_ARYPTN(node)->rest_arg); return rb_ary_new_from_args(4, - NEW_CHILD(ast, node->nd_pconst), - NEW_CHILD(ast, apinfo->pre_args), + NEW_CHILD(ast_value, RNODE_ARYPTN(node)->nd_pconst), + NEW_CHILD(ast_value, RNODE_ARYPTN(node)->pre_args), rest, - NEW_CHILD(ast, apinfo->post_args)); + NEW_CHILD(ast_value, RNODE_ARYPTN(node)->post_args)); } case NODE_FNDPTN: { - struct rb_fnd_pattern_info *fpinfo = node->nd_fpinfo; - VALUE pre_rest = rest_arg(ast, fpinfo->pre_rest_arg); - VALUE post_rest = rest_arg(ast, fpinfo->post_rest_arg); + VALUE pre_rest = rest_arg(ast_value, RNODE_FNDPTN(node)->pre_rest_arg); + VALUE post_rest = rest_arg(ast_value, RNODE_FNDPTN(node)->post_rest_arg); return rb_ary_new_from_args(4, - NEW_CHILD(ast, node->nd_pconst), + NEW_CHILD(ast_value, RNODE_FNDPTN(node)->nd_pconst), pre_rest, - NEW_CHILD(ast, fpinfo->args), + NEW_CHILD(ast_value, RNODE_FNDPTN(node)->args), post_rest); } case NODE_HSHPTN: { - VALUE kwrest = node->nd_pkwrestarg == NODE_SPECIAL_NO_REST_KEYWORD ? ID2SYM(rb_intern("NODE_SPECIAL_NO_REST_KEYWORD")) : - NEW_CHILD(ast, node->nd_pkwrestarg); + VALUE kwrest = RNODE_HSHPTN(node)->nd_pkwrestarg == NODE_SPECIAL_NO_REST_KEYWORD ? ID2SYM(rb_intern("NODE_SPECIAL_NO_REST_KEYWORD")) : + NEW_CHILD(ast_value, RNODE_HSHPTN(node)->nd_pkwrestarg); return rb_ary_new_from_args(3, - NEW_CHILD(ast, node->nd_pconst), - NEW_CHILD(ast, node->nd_pkwargs), + NEW_CHILD(ast_value, RNODE_HSHPTN(node)->nd_pconst), + NEW_CHILD(ast_value, RNODE_HSHPTN(node)->nd_pkwargs), kwrest); } + case NODE_LINE: + return rb_ary_new_from_args(1, rb_node_line_lineno_val(node)); + case NODE_FILE: + return rb_ary_new_from_args(1, rb_node_file_path_val(node)); + case NODE_ENCODING: + return rb_ary_new_from_args(1, rb_node_encoding_val(node)); + case NODE_ERROR: + return rb_ary_new_from_node_args(ast_value, 0); case NODE_ARGS_AUX: case NODE_LAST: break; @@ -659,7 +748,250 @@ ast_node_children(rb_execution_context_t *ec, VALUE self) struct ASTNodeData *data; TypedData_Get_Struct(self, struct ASTNodeData, &rb_node_type, data); - return node_children(data->ast, data->node); + return node_children(data->ast_value, data->node); +} + +static int +null_loc_p(rb_code_location_t *loc) +{ + return (loc->beg_pos.lineno == 0 && loc->beg_pos.column == -1 && loc->end_pos.lineno == 0 && loc->end_pos.column == -1); +} + +static VALUE +location_new(rb_code_location_t *loc) +{ + VALUE obj; + struct ASTLocationData *data; + + if (null_loc_p(loc)) return Qnil; + + obj = TypedData_Make_Struct(rb_cLocation, struct ASTLocationData, &rb_location_type, data); + data->first_lineno = loc->beg_pos.lineno; + data->first_column = loc->beg_pos.column; + data->last_lineno = loc->end_pos.lineno; + data->last_column = loc->end_pos.column; + + return obj; +} + +static VALUE +node_locations(VALUE ast_value, const NODE *node) +{ + enum node_type type = nd_type(node); + switch (type) { + case NODE_ALIAS: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_ALIAS(node)->keyword_loc)); + case NODE_AND: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_AND(node)->operator_loc)); + case NODE_BLOCK_PASS: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_BLOCK_PASS(node)->operator_loc)); + case NODE_BREAK: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_BREAK(node)->keyword_loc)); + case NODE_CASE: + return rb_ary_new_from_args(3, + location_new(nd_code_loc(node)), + location_new(&RNODE_CASE(node)->case_keyword_loc), + location_new(&RNODE_CASE(node)->end_keyword_loc)); + case NODE_CASE2: + return rb_ary_new_from_args(3, + location_new(nd_code_loc(node)), + location_new(&RNODE_CASE2(node)->case_keyword_loc), + location_new(&RNODE_CASE2(node)->end_keyword_loc)); + case NODE_CASE3: + return rb_ary_new_from_args(3, + location_new(nd_code_loc(node)), + location_new(&RNODE_CASE3(node)->case_keyword_loc), + location_new(&RNODE_CASE3(node)->end_keyword_loc)); + case NODE_CLASS: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_CLASS(node)->class_keyword_loc), + location_new(&RNODE_CLASS(node)->inheritance_operator_loc), + location_new(&RNODE_CLASS(node)->end_keyword_loc)); + case NODE_COLON2: + return rb_ary_new_from_args(3, + location_new(nd_code_loc(node)), + location_new(&RNODE_COLON2(node)->delimiter_loc), + location_new(&RNODE_COLON2(node)->name_loc)); + case NODE_COLON3: + return rb_ary_new_from_args(3, + location_new(nd_code_loc(node)), + location_new(&RNODE_COLON3(node)->delimiter_loc), + location_new(&RNODE_COLON3(node)->name_loc)); + case NODE_DEFINED: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_DEFINED(node)->keyword_loc)); + case NODE_DOT2: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_DOT2(node)->operator_loc)); + case NODE_DOT3: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_DOT3(node)->operator_loc)); + case NODE_EVSTR: + return rb_ary_new_from_args(3, + location_new(nd_code_loc(node)), + location_new(&RNODE_EVSTR(node)->opening_loc), + location_new(&RNODE_EVSTR(node)->closing_loc)); + case NODE_FLIP2: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_FLIP2(node)->operator_loc)); + case NODE_FLIP3: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_FLIP3(node)->operator_loc)); + case NODE_FOR: + return rb_ary_new_from_args(5, + location_new(nd_code_loc(node)), + location_new(&RNODE_FOR(node)->for_keyword_loc), + location_new(&RNODE_FOR(node)->in_keyword_loc), + location_new(&RNODE_FOR(node)->do_keyword_loc), + location_new(&RNODE_FOR(node)->end_keyword_loc)); + case NODE_LAMBDA: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_LAMBDA(node)->operator_loc), + location_new(&RNODE_LAMBDA(node)->opening_loc), + location_new(&RNODE_LAMBDA(node)->closing_loc)); + case NODE_IF: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_IF(node)->if_keyword_loc), + location_new(&RNODE_IF(node)->then_keyword_loc), + location_new(&RNODE_IF(node)->end_keyword_loc)); + case NODE_IN: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_IN(node)->in_keyword_loc), + location_new(&RNODE_IN(node)->then_keyword_loc), + location_new(&RNODE_IN(node)->operator_loc)); + case NODE_MODULE: + return rb_ary_new_from_args(3, + location_new(nd_code_loc(node)), + location_new(&RNODE_MODULE(node)->module_keyword_loc), + location_new(&RNODE_MODULE(node)->end_keyword_loc)); + case NODE_NEXT: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_NEXT(node)->keyword_loc)); + case NODE_OR: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_OR(node)->operator_loc)); + case NODE_OP_ASGN1: + return rb_ary_new_from_args(5, + location_new(nd_code_loc(node)), + location_new(&RNODE_OP_ASGN1(node)->call_operator_loc), + location_new(&RNODE_OP_ASGN1(node)->opening_loc), + location_new(&RNODE_OP_ASGN1(node)->closing_loc), + location_new(&RNODE_OP_ASGN1(node)->binary_operator_loc)); + case NODE_OP_ASGN2: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_OP_ASGN2(node)->call_operator_loc), + location_new(&RNODE_OP_ASGN2(node)->message_loc), + location_new(&RNODE_OP_ASGN2(node)->binary_operator_loc)); + case NODE_POSTEXE: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_POSTEXE(node)->keyword_loc), + location_new(&RNODE_POSTEXE(node)->opening_loc), + location_new(&RNODE_POSTEXE(node)->closing_loc)); + case NODE_REDO: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_REDO(node)->keyword_loc)); + case NODE_REGX: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_REGX(node)->opening_loc), + location_new(&RNODE_REGX(node)->content_loc), + location_new(&RNODE_REGX(node)->closing_loc)); + case NODE_RETURN: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_RETURN(node)->keyword_loc)); + + case NODE_SCLASS: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_SCLASS(node)->class_keyword_loc), + location_new(&RNODE_SCLASS(node)->operator_loc), + location_new(&RNODE_SCLASS(node)->end_keyword_loc)); + + case NODE_SPLAT: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_SPLAT(node)->operator_loc)); + case NODE_SUPER: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_SUPER(node)->keyword_loc), + location_new(&RNODE_SUPER(node)->lparen_loc), + location_new(&RNODE_SUPER(node)->rparen_loc)); + case NODE_UNDEF: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_UNDEF(node)->keyword_loc)); + case NODE_UNLESS: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_UNLESS(node)->keyword_loc), + location_new(&RNODE_UNLESS(node)->then_keyword_loc), + location_new(&RNODE_UNLESS(node)->end_keyword_loc)); + case NODE_VALIAS: + return rb_ary_new_from_args(2, + location_new(nd_code_loc(node)), + location_new(&RNODE_VALIAS(node)->keyword_loc)); + case NODE_WHEN: + return rb_ary_new_from_args(3, + location_new(nd_code_loc(node)), + location_new(&RNODE_WHEN(node)->keyword_loc), + location_new(&RNODE_WHEN(node)->then_keyword_loc)); + case NODE_WHILE: + return rb_ary_new_from_args(3, + location_new(nd_code_loc(node)), + location_new(&RNODE_WHILE(node)->keyword_loc), + location_new(&RNODE_WHILE(node)->closing_loc)); + case NODE_UNTIL: + return rb_ary_new_from_args(3, + location_new(nd_code_loc(node)), + location_new(&RNODE_UNTIL(node)->keyword_loc), + location_new(&RNODE_UNTIL(node)->closing_loc)); + case NODE_YIELD: + return rb_ary_new_from_args(4, + location_new(nd_code_loc(node)), + location_new(&RNODE_YIELD(node)->keyword_loc), + location_new(&RNODE_YIELD(node)->lparen_loc), + location_new(&RNODE_YIELD(node)->rparen_loc)); + case NODE_ARGS_AUX: + case NODE_LAST: + break; + default: + return rb_ary_new_from_args(1, location_new(nd_code_loc(node))); + } + + rb_bug("node_locations: unknown node: %s", ruby_node_name(type)); +} + +static VALUE +ast_node_locations(rb_execution_context_t *ec, VALUE self) +{ + struct ASTNodeData *data; + TypedData_Get_Struct(self, struct ASTNodeData, &rb_node_type, data); + + return node_locations(data->ast_value, data->node); } static VALUE @@ -699,6 +1031,42 @@ ast_node_last_column(rb_execution_context_t *ec, VALUE self) } static VALUE +ast_node_all_tokens(rb_execution_context_t *ec, VALUE self) +{ + long i; + struct ASTNodeData *data; + rb_ast_t *ast; + rb_parser_ary_t *parser_tokens; + rb_parser_ast_token_t *parser_token; + VALUE str, loc, token, all_tokens; + + TypedData_Get_Struct(self, struct ASTNodeData, &rb_node_type, data); + ast = rb_ruby_ast_data_get(data->ast_value); + + parser_tokens = ast->node_buffer->tokens; + if (parser_tokens == NULL) { + return Qnil; + } + + all_tokens = rb_ary_new2(parser_tokens->len); + for (i = 0; i < parser_tokens->len; i++) { + parser_token = parser_tokens->data[i]; + str = rb_str_new(parser_token->str->ptr, parser_token->str->len); + loc = rb_ary_new_from_args(4, + INT2FIX(parser_token->loc.beg_pos.lineno), + INT2FIX(parser_token->loc.beg_pos.column), + INT2FIX(parser_token->loc.end_pos.lineno), + INT2FIX(parser_token->loc.end_pos.column) + ); + token = rb_ary_new_from_args(4, INT2FIX(parser_token->id), ID2SYM(rb_intern(parser_token->type_name)), str, loc); + rb_ary_push(all_tokens, token); + } + rb_ary_freeze(all_tokens); + + return all_tokens; +} + +static VALUE ast_node_inspect(rb_execution_context_t *ec, VALUE self) { VALUE str; @@ -722,10 +1090,66 @@ static VALUE ast_node_script_lines(rb_execution_context_t *ec, VALUE self) { struct ASTNodeData *data; + rb_ast_t *ast; TypedData_Get_Struct(self, struct ASTNodeData, &rb_node_type, data); - VALUE ret = data->ast->body.script_lines; - if (!RB_TYPE_P(ret, T_ARRAY)) return Qnil; - return ret; + ast = rb_ruby_ast_data_get(data->ast_value); + rb_parser_ary_t *ret = ast->body.script_lines; + return rb_parser_build_script_lines_from(ret); +} + +static VALUE +ast_location_first_lineno(rb_execution_context_t *ec, VALUE self) +{ + struct ASTLocationData *data; + TypedData_Get_Struct(self, struct ASTLocationData, &rb_location_type, data); + + return INT2NUM(data->first_lineno); +} + +static VALUE +ast_location_first_column(rb_execution_context_t *ec, VALUE self) +{ + struct ASTLocationData *data; + TypedData_Get_Struct(self, struct ASTLocationData, &rb_location_type, data); + + return INT2NUM(data->first_column); +} + +static VALUE +ast_location_last_lineno(rb_execution_context_t *ec, VALUE self) +{ + struct ASTLocationData *data; + TypedData_Get_Struct(self, struct ASTLocationData, &rb_location_type, data); + + return INT2NUM(data->last_lineno); +} + +static VALUE +ast_location_last_column(rb_execution_context_t *ec, VALUE self) +{ + struct ASTLocationData *data; + TypedData_Get_Struct(self, struct ASTLocationData, &rb_location_type, data); + + return INT2NUM(data->last_column); +} + +static VALUE +ast_location_inspect(rb_execution_context_t *ec, VALUE self) +{ + VALUE str; + VALUE cname; + struct ASTLocationData *data; + TypedData_Get_Struct(self, struct ASTLocationData, &rb_location_type, data); + + cname = rb_class_path(rb_obj_class(self)); + str = rb_str_new2("#<"); + + rb_str_append(str, cname); + rb_str_catf(str, ":@%d:%d-%d:%d>", + data->first_lineno, data->first_column, + data->last_lineno, data->last_column); + + return str; } #include "ast.rbinc" @@ -735,5 +1159,7 @@ Init_ast(void) { rb_mAST = rb_define_module_under(rb_cRubyVM, "AbstractSyntaxTree"); rb_cNode = rb_define_class_under(rb_mAST, "Node", rb_cObject); + rb_cLocation = rb_define_class_under(rb_mAST, "Location", rb_cObject); rb_undef_alloc_func(rb_cNode); + rb_undef_alloc_func(rb_cLocation); } @@ -13,28 +13,53 @@ # access children nodes by name, etc. # # If you are looking for a stable API or an API working under multiple Ruby -# implementations, consider using the _parser_ gem or Ripper. If you would -# like to make RubyVM::AbstractSyntaxTree stable, please join the discussion -# at https://bugs.ruby-lang.org/issues/14844. +# implementations, consider using the _prism_ gem, which is the official +# Ruby API to parse Ruby code. # module RubyVM::AbstractSyntaxTree # call-seq: - # RubyVM::AbstractSyntaxTree.parse(string) -> RubyVM::AbstractSyntaxTree::Node + # RubyVM::AbstractSyntaxTree.parse(string, keep_script_lines: RubyVM.keep_script_lines, error_tolerant: false, keep_tokens: false) -> RubyVM::AbstractSyntaxTree::Node # # Parses the given _string_ into an abstract syntax tree, # returning the root node of that tree. # - # SyntaxError is raised if the given _string_ is invalid syntax. - # # RubyVM::AbstractSyntaxTree.parse("x = 1 + 2") # # => #<RubyVM::AbstractSyntaxTree::Node:SCOPE@1:0-1:9> - def self.parse string, keep_script_lines: false - Primitive.ast_s_parse string, keep_script_lines + # + # If <tt>keep_script_lines: true</tt> option is provided, the text of the parsed + # source is associated with nodes and is available via Node#script_lines. + # + # If <tt>keep_tokens: true</tt> option is provided, Node#tokens are populated. + # + # SyntaxError is raised if the given _string_ is invalid syntax. To overwrite this + # behavior, <tt>error_tolerant: true</tt> can be provided. In this case, the parser + # will produce a tree where expressions with syntax errors would be represented by + # Node with <tt>type=:ERROR</tt>. + # + # root = RubyVM::AbstractSyntaxTree.parse("x = 1; p(x; y=2") + # # <internal:ast>:33:in `parse': syntax error, unexpected ';', expecting ')' (SyntaxError) + # # x = 1; p(x; y=2 + # # ^ + # + # root = RubyVM::AbstractSyntaxTree.parse("x = 1; p(x; y=2", error_tolerant: true) + # # (SCOPE@1:0-1:15 + # # tbl: [:x, :y] + # # args: nil + # # body: (BLOCK@1:0-1:15 (LASGN@1:0-1:5 :x (LIT@1:4-1:5 1)) (ERROR@1:7-1:11) (LASGN@1:12-1:15 :y (LIT@1:14-1:15 2)))) + # root.children.last.children + # # [(LASGN@1:0-1:5 :x (LIT@1:4-1:5 1)), + # # (ERROR@1:7-1:11), + # # (LASGN@1:12-1:15 :y (LIT@1:14-1:15 2))] + # + # Note that parsing continues even after the errored expression. + # + def self.parse string, keep_script_lines: RubyVM.keep_script_lines, error_tolerant: false, keep_tokens: false + Primitive.ast_s_parse string, keep_script_lines, error_tolerant, keep_tokens end # call-seq: - # RubyVM::AbstractSyntaxTree.parse_file(pathname) -> RubyVM::AbstractSyntaxTree::Node + # RubyVM::AbstractSyntaxTree.parse_file(pathname, keep_script_lines: RubyVM.keep_script_lines, error_tolerant: false, keep_tokens: false) -> RubyVM::AbstractSyntaxTree::Node # # Reads the file from _pathname_, then parses it like ::parse, # returning the root node of the abstract syntax tree. @@ -44,13 +69,15 @@ module RubyVM::AbstractSyntaxTree # # RubyVM::AbstractSyntaxTree.parse_file("my-app/app.rb") # # => #<RubyVM::AbstractSyntaxTree::Node:SCOPE@1:0-31:3> - def self.parse_file pathname, keep_script_lines: false - Primitive.ast_s_parse_file pathname, keep_script_lines + # + # See ::parse for explanation of keyword argument meaning and usage. + def self.parse_file pathname, keep_script_lines: RubyVM.keep_script_lines, error_tolerant: false, keep_tokens: false + Primitive.ast_s_parse_file pathname, keep_script_lines, error_tolerant, keep_tokens end # call-seq: - # RubyVM::AbstractSyntaxTree.of(proc) -> RubyVM::AbstractSyntaxTree::Node - # RubyVM::AbstractSyntaxTree.of(method) -> RubyVM::AbstractSyntaxTree::Node + # RubyVM::AbstractSyntaxTree.of(proc, keep_script_lines: RubyVM.keep_script_lines, error_tolerant: false, keep_tokens: false) -> RubyVM::AbstractSyntaxTree::Node + # RubyVM::AbstractSyntaxTree.of(method, keep_script_lines: RubyVM.keep_script_lines, error_tolerant: false, keep_tokens: false) -> RubyVM::AbstractSyntaxTree::Node # # Returns AST nodes of the given _proc_ or _method_. # @@ -63,8 +90,25 @@ module RubyVM::AbstractSyntaxTree # # RubyVM::AbstractSyntaxTree.of(method(:hello)) # # => #<RubyVM::AbstractSyntaxTree::Node:SCOPE@1:0-3:3> - def self.of body, keep_script_lines: false - Primitive.ast_s_of body, keep_script_lines + # + # See ::parse for explanation of keyword argument meaning and usage. + def self.of body, keep_script_lines: RubyVM.keep_script_lines, error_tolerant: false, keep_tokens: false + Primitive.ast_s_of body, keep_script_lines, error_tolerant, keep_tokens + end + + # call-seq: + # RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(backtrace_location) -> integer + # + # Returns the node id for the given backtrace location. + # + # begin + # raise + # rescue => e + # loc = e.backtrace_locations.first + # RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(loc) + # end # => 0 + def self.node_id_for_backtrace_location backtrace_location + Primitive.node_id_for_backtrace_location backtrace_location end # RubyVM::AbstractSyntaxTree::Node instances are created by parse methods in @@ -122,6 +166,47 @@ module RubyVM::AbstractSyntaxTree end # call-seq: + # node.tokens -> array + # + # Returns tokens corresponding to the location of the node. + # Returns +nil+ if +keep_tokens+ is not enabled when #parse method is called. + # + # root = RubyVM::AbstractSyntaxTree.parse("x = 1 + 2", keep_tokens: true) + # root.tokens # => [[0, :tIDENTIFIER, "x", [1, 0, 1, 1]], [1, :tSP, " ", [1, 1, 1, 2]], ...] + # root.tokens.map{_1[2]}.join # => "x = 1 + 2" + # + # Token is an array of: + # + # - id + # - token type + # - source code text + # - location [ first_lineno, first_column, last_lineno, last_column ] + def tokens + return nil unless all_tokens + + all_tokens.each_with_object([]) do |token, a| + loc = token.last + if ([first_lineno, first_column] <=> [loc[0], loc[1]]) <= 0 && + ([last_lineno, last_column] <=> [loc[2], loc[3]]) >= 0 + a << token + end + end + end + + # call-seq: + # node.all_tokens -> array + # + # Returns all tokens for the input script regardless the receiver node. + # Returns +nil+ if +keep_tokens+ is not enabled when #parse method is called. + # + # root = RubyVM::AbstractSyntaxTree.parse("x = 1 + 2", keep_tokens: true) + # root.all_tokens # => [[0, :tIDENTIFIER, "x", [1, 0, 1, 1]], [1, :tSP, " ", [1, 1, 1, 2]], ...] + # root.children[-1].all_tokens # => [[0, :tIDENTIFIER, "x", [1, 0, 1, 1]], [1, :tSP, " ", [1, 1, 1, 2]], ...] + def all_tokens + Primitive.ast_node_all_tokens + end + + # call-seq: # node.children -> array # # Returns AST nodes under this one. Each kind of node @@ -179,12 +264,69 @@ module RubyVM::AbstractSyntaxTree lines = script_lines if lines lines = lines[first_lineno - 1 .. last_lineno - 1] - lines[-1] = lines[-1][0...last_column] - lines[0] = lines[0][first_column..-1] + lines[-1] = lines[-1].byteslice(0...last_column) + lines[0] = lines[0].byteslice(first_column..-1) lines.join else nil end end + + # call-seq: + # node.locations -> array + # + # Returns location objects associated with the AST node. + # The returned array contains RubyVM::AbstractSyntaxTree::Location. + def locations + Primitive.ast_node_locations + end + end + + # RubyVM::AbstractSyntaxTree::Location instances are created by + # RubyVM::AbstractSyntaxTree::Node#locations. + # + # This class is MRI specific. + # + class Location + + # call-seq: + # location.first_lineno -> integer + # + # The line number in the source code where this AST's text began. + def first_lineno + Primitive.ast_location_first_lineno + end + + # call-seq: + # location.first_column -> integer + # + # The column number in the source code where this AST's text began. + def first_column + Primitive.ast_location_first_column + end + + # call-seq: + # location.last_lineno -> integer + # + # The line number in the source code where this AST's text ended. + def last_lineno + Primitive.ast_location_last_lineno + end + + # call-seq: + # location.last_column -> integer + # + # The column number in the source code where this AST's text ended. + def last_column + Primitive.ast_location_last_column + end + + # call-seq: + # location.inspect -> string + # + # Returns debugging information about this location as a string. + def inspect + Primitive.ast_location_inspect + end end end diff --git a/autogen.sh b/autogen.sh index f8cdf3c0c1..6cbc5dddab 100755 --- a/autogen.sh +++ b/autogen.sh @@ -1,17 +1,22 @@ #!/bin/sh +# Clear PWD to force commands to recompute working directory PWD= + +# Figure out the source directory for this script +# configure.ac should be in the same place case "$0" in -*/*) srcdir=`dirname $0`;; -*) srcdir="";; + */* ) srcdir=`dirname "$0"` ;; # Called with path + * ) srcdir="";; # Otherwise esac -symlink='--install --symlink' +# If install-only is explicitly requested, disable symlink flags case " $* " in - *" -i "*|*" --install "*) - # reset to copy missing standard auxiliary files, instead of symlinks - symlink= - ;; + *" -i "* | *" --install"* ) symlink_flags="" ;; + * ) symlink_flags="--install --symlink" ;; esac -exec ${AUTORECONF:-autoreconf} ${symlink} "$@" ${srcdir:+"$srcdir"} +exec ${AUTORECONF:-autoreconf} \ + $symlink_flags \ + "$@" \ + $srcdir diff --git a/basictest/test.rb b/basictest/test.rb index 52008b78db..711e4f4ab3 100755 --- a/basictest/test.rb +++ b/basictest/test.rb @@ -879,7 +879,7 @@ $x.sort!{|a,b| b-a} # reverse sort test_ok($x == [7,5,3,2,1]) # split test -$x = "The Book of Mormon" +$x = +"The Book of Mormon" test_ok($x.split(//).reverse!.join == $x.reverse) test_ok($x.reverse == $x.reverse!) test_ok("1 byte string".split(//).reverse.join(":") == "g:n:i:r:t:s: :e:t:y:b: :1") @@ -1643,7 +1643,7 @@ test_ok(/^(?:ab+)+/ =~ "ababb" && $& == "ababb") test_ok(/(\s+\d+){2}/ =~ " 1 2" && $& == " 1 2") test_ok(/(?:\s+\d+){2}/ =~ " 1 2" && $& == " 1 2") -$x = <<END; +$x = +<<END; ABCD ABCD END @@ -1682,12 +1682,12 @@ test_ok(?a == ?a) test_ok(?\C-a == "\1") test_ok(?\M-a == "\341") test_ok(?\M-\C-a == "\201") -test_ok("a".upcase![0] == ?A) -test_ok("A".downcase![0] == ?a) -test_ok("abc".tr!("a-z", "A-Z") == "ABC") -test_ok("aabbcccc".tr_s!("a-z", "A-Z") == "ABC") -test_ok("abcc".squeeze!("a-z") == "abc") -test_ok("abcd".delete!("bc") == "ad") +test_ok("a".dup.upcase![0] == ?A) +test_ok("A".dup.downcase![0] == ?a) +test_ok("abc".dup.tr!("a-z", "A-Z") == "ABC") +test_ok("aabbcccc".dup.tr_s!("a-z", "A-Z") == "ABC") +test_ok("abcc".dup.squeeze!("a-z") == "abc") +test_ok("abcd".dup.delete!("bc") == "ad") $x = "abcdef" $y = [ ?a, ?b, ?c, ?d, ?e, ?f ] @@ -1700,7 +1700,7 @@ $x.each_byte {|i| } test_ok(!$bad) -s = "a string" +s = +"a string" s[0..s.size]="another string" test_ok(s == "another string") @@ -1960,6 +1960,8 @@ test_ok(p1.call == 5) test_ok(i7 == nil) end +# WASI doesn't support spawning a new process for now. +unless /wasi/ =~ RUBY_PLATFORM test_check "system" test_ok(`echo foobar` == "foobar\n") test_ok(`./miniruby -e 'print "foobar"'` == 'foobar') @@ -2010,6 +2012,7 @@ test_ok(done) File.unlink script_tmp or `/bin/rm -f "#{script_tmp}"` File.unlink "#{script_tmp}.bak" or `/bin/rm -f "#{script_tmp}.bak"` +end # not /wasi/ =~ RUBY_PLATFORM test_check "const" TEST1 = 1 diff --git a/benchmark/README.md b/benchmark/README.md index c222164be3..9f9192685e 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -27,16 +27,19 @@ benchmark-driver benchmark/*.yml See also: ```console +benchmark-driver --help Usage: benchmark-driver [options] RUBY|YAML... - -r, --runner TYPE Specify runner type: ips, time, memory, once (default: ips) - -o, --output TYPE Specify output type: compare, simple, markdown, record (default: compare) + -r, --runner TYPE Specify runner type: ips, time, memory, once, block (default: ips) + -o, --output TYPE Specify output type: compare, simple, markdown, record, all (default: compare) -e, --executables EXECS Ruby executables (e1::path1 arg1; e2::path2 arg2;...) --rbenv VERSIONS Ruby executables in rbenv (x.x.x arg1;y.y.y arg2;...) --repeat-count NUM Try benchmark NUM times and use the fastest result or the worst memory usage --repeat-result TYPE Yield "best", "average" or "worst" result with --repeat-count (default: best) + --alternate Alternate executables instead of running the same executable in a row with --repeat-count --bundler Install and use gems specified in Gemfile --filter REGEXP Filter out benchmarks with given regexp --run-duration SECONDS Warmup estimates loop_count to run for this duration (default: 3) + --timeout SECONDS Timeout ruby command execution with timeout(1) -v, --verbose Verbose mode. Multiple -v options increase visibility (max: 2) ``` diff --git a/benchmark/app_aobench.rb b/benchmark/app_aobench.rb index 16296af12b..c1546e08ab 100644 --- a/benchmark/app_aobench.rb +++ b/benchmark/app_aobench.rb @@ -151,7 +151,7 @@ def clamp(f) i.to_i end -def otherBasis(basis, n) +def orthoBasis(basis, n) basis[2] = Vec.new(n.x, n.y, n.z) basis[1] = Vec.new(0.0, 0.0, 0.0) @@ -183,7 +183,7 @@ class Scene def ambient_occlusion(isect) basis = Array.new - otherBasis(basis, isect.n) + orthoBasis(basis, isect.n) ntheta = NAO_SAMPLES nphi = NAO_SAMPLES diff --git a/benchmark/app_fib.rb b/benchmark/app_fib.rb index 34a7b2e725..e61bc8aa32 100644 --- a/benchmark/app_fib.rb +++ b/benchmark/app_fib.rb @@ -1,4 +1,4 @@ -def fib n +def fib(n) if n < 3 1 else diff --git a/benchmark/array_large_literal.yml b/benchmark/array_large_literal.yml new file mode 100644 index 0000000000..423d68391f --- /dev/null +++ b/benchmark/array_large_literal.yml @@ -0,0 +1,19 @@ +prelude: | + def def_array(size) + Object.class_eval(<<-END) + def array_#{size} + x = 1 + [#{(['x'] * size).join(',')}] + end + END + end + def_array(100) + def_array(1000) + def_array(10000) + def_array(100000) +benchmark: + array_100: array_100 + array_1000: array_1000 + array_10000: array_10000 + array_100000: array_100000 + diff --git a/benchmark/array_sort_int.yml b/benchmark/array_sort_int.yml new file mode 100644 index 0000000000..7b9027ebf7 --- /dev/null +++ b/benchmark/array_sort_int.yml @@ -0,0 +1,15 @@ +prelude: | + ary2 = 2.times.to_a.shuffle + ary10 = 10.times.to_a.shuffle + ary100 = 100.times.to_a.shuffle + ary1000 = 1000.times.to_a.shuffle + ary10000 = 10000.times.to_a.shuffle + +benchmark: + ary2.sort: ary2.sort + ary10.sort: ary10.sort + ary100.sort: ary100.sort + ary1000.sort: ary1000.sort + ary10000.sort: ary10000.sort + +loop_count: 10000 diff --git a/benchmark/buffer_each.yml b/benchmark/buffer_each.yml new file mode 100644 index 0000000000..417941104e --- /dev/null +++ b/benchmark/buffer_each.yml @@ -0,0 +1,27 @@ +prelude: | + # frozen_string_literal: true + Warning[:experimental] = false + string = "The quick brown fox jumped over the lazy dog." + array = string.bytes + buffer = IO::Buffer.for(string) +benchmark: + string.each_byte: | + upcased = String.new + string.each_byte do |byte| + upcased << (byte ^ 32) + end + array.each: | + upcased = String.new + array.each do |byte| + upcased << (byte ^ 32) + end + buffer.each: | + upcased = String.new + buffer.each(:U8) do |offset, byte| + upcased << (byte ^ 32) + end + buffer.each_byte: | + upcased = String.new + buffer.each_byte do |byte| + upcased << (byte ^ 32) + end diff --git a/benchmark/buffer_get.yml b/benchmark/buffer_get.yml index e375dcf85d..9e1f99d64e 100644 --- a/benchmark/buffer_get.yml +++ b/benchmark/buffer_get.yml @@ -1,9 +1,25 @@ +prelude: | + # frozen_string_literal: true + Warning[:experimental] = false + string = "The quick brown fox jumped over the lazy dog." + buffer = IO::Buffer.for(string) + format = [:U32, :U32, :U32, :U32] benchmark: - - name: buffer.get - prelude: buffer = IO::Buffer.new(32, IO::Buffer::MAPPED) - script: buffer.get(:U32, 0) - loop_count: 20000000 - - name: string.unpack - prelude: string = "\0" * 32 - script: string.unpack("C") - loop_count: 20000000 + string.unpack1: | + [ + string.unpack1("N"), + string.unpack1("N", offset: 4), + string.unpack1("N", offset: 8), + string.unpack1("N", offset: 12), + ] + buffer.get_value: | + [ + buffer.get_value(:U32, 0), + buffer.get_value(:U32, 4), + buffer.get_value(:U32, 8), + buffer.get_value(:U32, 12), + ] + buffer.get_values: | + buffer.get_values(format, 0) + string.unpack: | + string.unpack("NNNN") diff --git a/benchmark/cgi_escape_html.yml b/benchmark/cgi_escape_html.yml index af6abd08ac..655be9d7d8 100644 --- a/benchmark/cgi_escape_html.yml +++ b/benchmark/cgi_escape_html.yml @@ -1,32 +1,23 @@ -prelude: require 'cgi/escape' +prelude: | + # frozen_string_literal: true + require 'cgi/escape' benchmark: - - name: escape_html_blank - prelude: str = "" - script: CGI.escapeHTML(str) + - script: CGI.escapeHTML("") loop_count: 20000000 - - name: escape_html_short_none - prelude: str = "abcde" - script: CGI.escapeHTML(str) + - script: CGI.escapeHTML("abcde") loop_count: 20000000 - - name: escape_html_short_one - prelude: str = "abcd<" - script: CGI.escapeHTML(str) + - script: CGI.escapeHTML("abcd<") loop_count: 20000000 - - name: escape_html_short_all - prelude: str = "'&\"<>" - script: CGI.escapeHTML(str) + - script: CGI.escapeHTML("'&\"<>") loop_count: 5000000 - - name: escape_html_long_none - prelude: str = "abcde" * 300 - script: CGI.escapeHTML(str) + - prelude: long_no_escape = "abcde" * 300 + script: CGI.escapeHTML(long_no_escape) loop_count: 1000000 - - name: escape_html_long_all - prelude: str = "'&\"<>" * 10 - script: CGI.escapeHTML(str) + - prelude: long_all_escape = "'&\"<>" * 10 + script: CGI.escapeHTML(long_all_escape) loop_count: 1000000 - - name: escape_html_real - prelude: | # http://example.com/ - str = <<~HTML + - prelude: | # http://example.com/ + example_html = <<~HTML <body> <div> <h1>Example Domain</h1> @@ -36,5 +27,5 @@ benchmark: </div> </body> HTML - script: CGI.escapeHTML(str) + script: CGI.escapeHTML(example_html) loop_count: 1000000 diff --git a/benchmark/class_superclass.yml b/benchmark/class_superclass.yml new file mode 100644 index 0000000000..847ff811f1 --- /dev/null +++ b/benchmark/class_superclass.yml @@ -0,0 +1,23 @@ +prelude: | + class SimpleClass; end + class OneModuleClass + 1.times { include Module.new } + end + class MediumClass + 10.times { include Module.new } + end + class LargeClass + 100.times { include Module.new } + end +benchmark: + object_class_superclass: | + Object.superclass + simple_class_superclass: | + SimpleClass.superclass + one_module_class: | + OneModuleClass.superclass + medium_class_superclass: | + MediumClass.superclass + large_class_superclass: | + LargeClass.superclass +loop_count: 20000000 diff --git a/benchmark/constant_invalidation.rb b/benchmark/constant_invalidation.rb new file mode 100644 index 0000000000..a95ec6f37e --- /dev/null +++ b/benchmark/constant_invalidation.rb @@ -0,0 +1,22 @@ +$VERBOSE = nil + +CONSTANT1 = 1 +CONSTANT2 = 1 +CONSTANT3 = 1 +CONSTANT4 = 1 +CONSTANT5 = 1 + +def constants + [CONSTANT1, CONSTANT2, CONSTANT3, CONSTANT4, CONSTANT5] +end + +500_000.times do + constants + + # With previous behavior, this would cause all of the constant caches + # associated with the constant lookups listed above to invalidate, meaning + # they would all have to be fetched again. With current behavior, it only + # invalidates when a name matches, so the following constant set shouldn't + # impact the constant lookups listed above. + INVALIDATE = true +end diff --git a/benchmark/enum_minmax.yml b/benchmark/enum_minmax.yml new file mode 100644 index 0000000000..9d01731abb --- /dev/null +++ b/benchmark/enum_minmax.yml @@ -0,0 +1,25 @@ +prelude: | + set2 = 2.times.to_a.shuffle.to_set + set10 = 10.times.to_a.shuffle.to_set + set100 = 100.times.to_a.shuffle.to_set + set1000 = 1000.times.to_a.shuffle.to_set + set10000 = 10000.times.to_a.shuffle.to_set + +benchmark: + set2.min: set2.min + set10.min: set10.min + set100.min: set100.min + set1000.min: set1000.min + set10000.min: set10000.min + set2.max: set2.max + set10.max: set10.max + set100.max: set100.max + set1000.max: set1000.max + set10000.max: set10000.max + set2.minmax: set2.minmax + set10.minmax: set10.minmax + set100.minmax: set100.minmax + set1000.minmax: set1000.minmax + set10000.minmax: set10000.minmax + +loop_count: 10000 diff --git a/benchmark/enum_sort.yml b/benchmark/enum_sort.yml new file mode 100644 index 0000000000..6f26e748c6 --- /dev/null +++ b/benchmark/enum_sort.yml @@ -0,0 +1,15 @@ +prelude: | + set2 = 2.times.to_a.shuffle.to_set + set10 = 10.times.to_a.shuffle.to_set + set100 = 100.times.to_a.shuffle.to_set + set1000 = 1000.times.to_a.shuffle.to_set + set10000 = 10000.times.to_a.shuffle.to_set + +benchmark: + set2.sort_by: set2.sort_by { 0 } + set10.sort_by: set10.sort_by { 0 } + set100.sort_by: set100.sort_by { 0 } + set1000.sort_by: set1000.sort_by { 0 } + set10000.sort_by: set10000.sort_by { 0 } + +loop_count: 10000 diff --git a/benchmark/enum_sort_by.yml b/benchmark/enum_sort_by.yml new file mode 100644 index 0000000000..d386353888 --- /dev/null +++ b/benchmark/enum_sort_by.yml @@ -0,0 +1,53 @@ +prelude: | + array_length = 2 + fixnum_array2 = array_length.times.to_a.map {rand(10000)} + float_array2 = array_length.times.to_a.map {rand(10000.0).to_f} + string_array2 = array_length.times.to_a.map {"r" * rand(1..10000)} + mix_array2 = array_length.times.to_a.map {if rand(1..100) <= 50 then rand(1..10000).to_f else rand(1..10000) end} + all_zero_array2 =array_length.times.to_a.map {0} + + array_length = 10 + fixnum_array10 = array_length.times.to_a.map {rand(10000)} + float_array10 = array_length.times.to_a.map {rand(10000.0).to_f} + string_array10 = array_length.times.to_a.map {"r" * rand(1..10000)} + mix_array10 = array_length.times.to_a.map {if rand(1..100) <= 50 then rand(1..10000).to_f else rand(1..10000) end} + all_zero_array10 =array_length.times.to_a.map {0} + + array_length = 1000 + fixnum_array1000 = array_length.times.to_a.map {rand(10000)} + float_array1000 = array_length.times.to_a.map {rand(10000.0).to_f} + string_array1000 = array_length.times.to_a.map {"r" * rand(1..10000)} + mix_array1000 = array_length.times.to_a.map {if rand(1..100) <= 50 then rand(1..10000).to_f else rand(1..10000) end} + all_zero_array1000 =array_length.times.to_a.map {0} + + array_length = 100000 + fixnum_array100000 = array_length.times.to_a.map {rand(10000)} + float_array100000 = array_length.times.to_a.map {rand(10000.0).to_f} + string_array100000 = array_length.times.to_a.map {"r" * rand(1..10000)} + mix_array100000 = array_length.times.to_a.map {if rand(1..100) <= 50 then rand(1..10000).to_f else rand(1..10000) end} + all_zero_array100000 =array_length.times.to_a.map {0} + +benchmark: + fixnum_array2.sort_by: fixnum_array2.sort_by {|a| a} + float_array2.sort_by: float_array2.sort_by {|a| a} + string_length2.sort_by: string_array2.sort_by {|a| a.length} + mix_array2.sort_by: mix_array2.sort_by {|a| a} + all_zero2.sort_by: all_zero_array2.sort_by{|a| a} + + fixnum_array10.sort_by: fixnum_array10.sort_by {|a| a} + float_array10.sort_by: float_array10.sort_by {|a| a} + string_length10.sort_by: string_array10.sort_by {|a| a.length} + mix_array10.sort_by: mix_array10.sort_by {|a| a} + all_zero10.sort_by: all_zero_array10.sort_by{|a| a} + + fixnum_array1000.sort_by: fixnum_array1000.sort_by {|a| a} + float_array1000.sort_by: float_array1000.sort_by {|a| a} + string_length1000.sort_by: string_array1000.sort_by {|a| a.length} + mix_array1000.sort_by: mix_array1000.sort_by {|a| a} + all_zero1000.sort_by: all_zero_array1000.sort_by{|a| a} + + fixnum_array100000.sort_by: fixnum_array100000.sort_by {|a| a} + float_array100000.sort_by: float_array100000.sort_by {|a| a} + string_length100000.sort_by: string_array100000.sort_by {|a| a.length} + mix_array100000.sort_by: mix_array100000.sort_by {|a| a} + all_zero100000.sort_by: all_zero_array100000.sort_by{|a| a} diff --git a/benchmark/erb_escape_html.yml b/benchmark/erb_escape_html.yml new file mode 100644 index 0000000000..ca28d756e7 --- /dev/null +++ b/benchmark/erb_escape_html.yml @@ -0,0 +1,31 @@ +prelude: | + # frozen_string_literal: true + require 'erb' +benchmark: + - script: ERB::Util.html_escape("") + loop_count: 20000000 + - script: ERB::Util.html_escape("abcde") + loop_count: 20000000 + - script: ERB::Util.html_escape("abcd<") + loop_count: 20000000 + - script: ERB::Util.html_escape("'&\"<>") + loop_count: 5000000 + - prelude: long_no_escape = "abcde" * 300 + script: ERB::Util.html_escape(long_no_escape) + loop_count: 1000000 + - prelude: long_all_escape = "'&\"<>" * 10 + script: ERB::Util.html_escape(long_all_escape) + loop_count: 1000000 + - prelude: | # http://example.com/ + example_html = <<~HTML + <body> + <div> + <h1>Example Domain</h1> + <p>This domain is established to be used for illustrative examples in documents. You may use this + domain in examples without prior coordination or asking for permission.</p> + <p><a href="http://www.iana.org/domains/example">More information...</a></p> + </div> + </body> + HTML + script: ERB::Util.html_escape(example_html) + loop_count: 1000000 diff --git a/benchmark/file_dirname.yml b/benchmark/file_dirname.yml new file mode 100644 index 0000000000..43a81c9371 --- /dev/null +++ b/benchmark/file_dirname.yml @@ -0,0 +1,6 @@ +prelude: | + # frozen_string_literal: true +benchmark: + long: File.dirname("/Users/george/src/github.com/ruby/ruby/benchmark/file_dirname.yml") + short: File.dirname("foo/bar") + n_4: File.dirname("/Users/george/src/github.com/ruby/ruby/benchmark/file_dirname.yml", 4) diff --git a/benchmark/file_extname.yml b/benchmark/file_extname.yml new file mode 100644 index 0000000000..fb16e55840 --- /dev/null +++ b/benchmark/file_extname.yml @@ -0,0 +1,6 @@ +prelude: | + # frozen_string_literal: true +benchmark: + long: File.extname("/Users/george/src/github.com/ruby/ruby/benchmark/file_dirname.yml") + long_name: File.extname("Users_george_src_github.com_ruby_ruby_benchmark_file_dirname.yml") + short: File.extname("foo/bar") diff --git a/benchmark/file_join.yml b/benchmark/file_join.yml new file mode 100644 index 0000000000..845257cf1e --- /dev/null +++ b/benchmark/file_join.yml @@ -0,0 +1,7 @@ +prelude: | + # frozen_string_literal: true +benchmark: + two_strings: File.join(__FILE__, "path") + many_strings: File.join(__FILE__, "path", "a", "b", "c", "d") + array: File.join([__FILE__, "path", "a", "b", "c", "d"]) + mixed: File.join(__FILE__, "path", "a", "b", ["c", "d"]) diff --git a/benchmark/hash_aref_str_lit.yml b/benchmark/hash_aref_str_lit.yml new file mode 100644 index 0000000000..ed8142bcf1 --- /dev/null +++ b/benchmark/hash_aref_str_lit.yml @@ -0,0 +1,20 @@ +prelude: | + # frozen_string_literal: true + hash = 10.times.to_h do |i| + [i, i] + end + dyn_sym = "dynamic_symbol".to_sym + binary = RubyVM::InstructionSequence.compile("# frozen_string_literal: true\n'iseq_load'").to_binary + iseq_literal_string = RubyVM::InstructionSequence.load_from_binary(binary).eval + + hash[:some_symbol] = 1 + hash[dyn_sym] = 2 + hash["small"] = 3 + hash["frozen_string_literal"] = 4 + hash[iseq_literal_string] = 5 +benchmark: + symbol: hash[:some_symbol] + dyn_symbol: hash[dyn_sym] + small_lit: hash["small"] + frozen_lit: hash["frozen_string_literal"] + iseq_lit: hash[iseq_literal_string] diff --git a/benchmark/hash_key.yml b/benchmark/hash_key.yml new file mode 100644 index 0000000000..cab4cf9ca4 --- /dev/null +++ b/benchmark/hash_key.yml @@ -0,0 +1,5 @@ +prelude: | + obj = Object.new + hash = { obj => true } +benchmark: hash.key?(obj) +loop_count: 30000000 diff --git a/benchmark/hash_new.yml b/benchmark/hash_new.yml new file mode 100644 index 0000000000..9d8e34187f --- /dev/null +++ b/benchmark/hash_new.yml @@ -0,0 +1,16 @@ +prelude: | + has_hash_with_capa = Hash.instance_method(:initialize).parameters.include?([:key, :capacity]) + strings_1k = 1_000.times.map { |i| -i.to_s.freeze } + strings_100k = 100_000.times.map { |i| -i.to_s.freeze } +benchmark: + new: Hash.new + new_with_capa_1k: | + h = has_hash_with_capa ? Hash.new(capacity: strings_1k.size) : {} + strings_1k.each do |x| + h[x] = true + end + new_with_capa_100k: | + h = has_hash_with_capa ? Hash.new(capacity: strings_100k.size) : {} + strings_100k.each do |x| + h[x] = true + end diff --git a/benchmark/io_close.yml b/benchmark/io_close.yml new file mode 100644 index 0000000000..a552872884 --- /dev/null +++ b/benchmark/io_close.yml @@ -0,0 +1,13 @@ +prelude: | + ios = 1000.times.map do + 100.times.map{IO.pipe} + end +benchmark: + # Close IO + io_close: | + # Process each batch of ios per iteration of the benchmark. + ios.pop.each do |r, w| + r.close + w.close + end +loop_count: 100 diff --git a/benchmark/io_close_contended.yml b/benchmark/io_close_contended.yml new file mode 100644 index 0000000000..1d9e4e0d0f --- /dev/null +++ b/benchmark/io_close_contended.yml @@ -0,0 +1,21 @@ +prelude: | + ios = 100.times.map do + 10.times.map do + pipe = IO.pipe.tap do |r, w| + Thread.new do + r.read + rescue IOError + # Ignore + end + end + end + end +benchmark: + # Close IO + io_close_contended: | + # Process each batch of ios per iteration of the benchmark. + ios.pop.each do |r, w| + r.close + w.close + end +loop_count: 10 diff --git a/benchmark/io_write.rb b/benchmark/io_write.rb new file mode 100644 index 0000000000..cdb409948b --- /dev/null +++ b/benchmark/io_write.rb @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby + +require 'benchmark' + +i, o = IO.pipe +o.sync = true + +DOT = ".".freeze + +chunks = 100_000.times.collect{DOT} + +thread = Thread.new do + while i.read(1024) + end +end + +100.times do + o.write(*chunks) +end + +o.close +thread.join diff --git a/benchmark/lib/benchmark_driver/runner/mjit.rb b/benchmark/lib/benchmark_driver/runner/mjit.rb deleted file mode 100644 index 1d4693e8be..0000000000 --- a/benchmark/lib/benchmark_driver/runner/mjit.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'benchmark_driver/struct' -require 'benchmark_driver/metric' -require 'erb' - -# A runner to measure after-JIT performance easily -class BenchmarkDriver::Runner::Mjit < BenchmarkDriver::Runner::Ips - # JobParser returns this, `BenchmarkDriver::Runner.runner_for` searches "*::Job" - Job = Class.new(BenchmarkDriver::DefaultJob) - - # Dynamically fetched and used by `BenchmarkDriver::JobParser.parse` - JobParser = BenchmarkDriver::DefaultJobParser.for(klass: Job, metrics: [METRIC]).extend(Module.new{ - def parse(**) - jobs = super - jobs.map do |job| - job = job.dup - job.prelude = "#{job.prelude}\n#{<<~EOS}" - if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? - __bmdv_ruby_i = 0 - while __bmdv_ruby_i < 10000 # jit_min_calls - #{job.script} - __bmdv_ruby_i += 1 - end - RubyVM::MJIT.pause # compile - #{job.script} - RubyVM::MJIT.resume; RubyVM::MJIT.pause # recompile - #{job.script} - RubyVM::MJIT.resume; RubyVM::MJIT.pause # recompile 2 - end - EOS - job - end - end - }) -end diff --git a/benchmark/lib/benchmark_driver/runner/mjit_exec.rb b/benchmark/lib/benchmark_driver/runner/mjit_exec.rb deleted file mode 100644 index eac3dfba84..0000000000 --- a/benchmark/lib/benchmark_driver/runner/mjit_exec.rb +++ /dev/null @@ -1,237 +0,0 @@ -require 'benchmark_driver/struct' -require 'benchmark_driver/metric' -require 'erb' - -# A special runner dedicated for measuring mjit_exec overhead. -class BenchmarkDriver::Runner::MjitExec - METRIC = BenchmarkDriver::Metric.new(name: 'Iteration per second', unit: 'i/s') - - # JobParser returns this, `BenchmarkDriver::Runner.runner_for` searches "*::Job" - Job = ::BenchmarkDriver::Struct.new( - :name, # @param [String] name - This is mandatory for all runner - :metrics, # @param [Array<BenchmarkDriver::Metric>] - :num_methods, # @param [Integer] num_methods - The number of methods to be defined - :loop_count, # @param [Integer] loop_count - :from_jit, # @param [TrueClass,FalseClass] from_jit - Whether the mjit_exec() is from JIT or not - :to_jit, # @param [TrueClass,FalseClass] to_jit - Whether the mjit_exec() is to JIT or not - ) - # Dynamically fetched and used by `BenchmarkDriver::JobParser.parse` - class << JobParser = Module.new - # @param [Array,String] num_methods - # @param [Integer] loop_count - # @param [TrueClass,FalseClass] from_jit - # @param [TrueClass,FalseClass] to_jit - def parse(num_methods:, loop_count:, from_jit:, to_jit:) - if num_methods.is_a?(String) - num_methods = eval(num_methods) - end - - num_methods.map do |num| - if num_methods.size > 1 - suffix = "[#{'%4d' % num}]" - else - suffix = "_#{num}" - end - Job.new( - name: "mjit_exec_#{from_jit ? 'JT' : 'VM'}2#{to_jit ? 'JT' : 'VM'}#{suffix}", - metrics: [METRIC], - num_methods: num, - loop_count: loop_count, - from_jit: from_jit, - to_jit: to_jit, - ) - end - end - end - - # @param [BenchmarkDriver::Config::RunnerConfig] config - # @param [BenchmarkDriver::Output] output - # @param [BenchmarkDriver::Context] contexts - def initialize(config:, output:, contexts:) - @config = config - @output = output - @contexts = contexts - end - - # This method is dynamically called by `BenchmarkDriver::JobRunner.run` - # @param [Array<BenchmarkDriver::Runner::Peak::Job>] jobs - def run(jobs) - @output.with_benchmark do - jobs.each do |job| - @output.with_job(name: job.name) do - @contexts.each do |context| - result = BenchmarkDriver::Repeater.with_repeat(config: @config, larger_better: true, rest_on_average: :average) do - run_benchmark(job, context: context) - end - value, duration = result.value - @output.with_context(name: context.name, executable: context.executable, gems: context.gems, prelude: context.prelude) do - @output.report(values: { METRIC => value }, duration: duration, loop_count: job.loop_count) - end - end - end - end - end - end - - private - - # @param [BenchmarkDriver::Runner::Ips::Job] job - loop_count is not nil - # @param [BenchmarkDriver::Context] context - # @return [BenchmarkDriver::Metrics] - def run_benchmark(job, context:) - if job.from_jit - if job.to_jit - benchmark = BenchmarkJT2JT.new(num_methods: job.num_methods, loop_count: job.loop_count) - else - raise NotImplementedError, "JT2VM is not implemented yet" - end - else - if job.to_jit - benchmark = BenchmarkVM2JT.new(num_methods: job.num_methods, loop_count: job.loop_count) - else - benchmark = BenchmarkVM2VM.new(num_methods: job.num_methods, loop_count: job.loop_count) - end - end - - duration = Tempfile.open(['benchmark_driver-result', '.txt']) do |f| - with_script(benchmark.render(result: f.path)) do |path| - opt = [] - if context.executable.command.any? { |c| c.start_with?('--jit') } - opt << '--jit-min-calls=2' - end - IO.popen([*context.executable.command, '--disable-gems', *opt, path], &:read) - if $?.success? - Float(f.read) - else - BenchmarkDriver::Result::ERROR - end - end - end - - [job.loop_count.to_f / duration, duration] - end - - def with_script(script) - if @config.verbose >= 2 - sep = '-' * 30 - $stdout.puts "\n\n#{sep}[Script begin]#{sep}\n#{script}#{sep}[Script end]#{sep}\n\n" - end - - Tempfile.open(['benchmark_driver-', '.rb']) do |f| - f.puts script - f.close - return yield(f.path) - end - end - - # @param [Integer] num_methods - # @param [Integer] loop_count - BenchmarkVM2VM = ::BenchmarkDriver::Struct.new(:num_methods, :loop_count) do - # @param [String] result - A file to write result - def render(result:) - ERB.new(<<~EOS, trim_mode: '%').result(binding) - % num_methods.times do |i| - def a<%= i %> - nil - end - % end - RubyVM::MJIT.pause if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? - - def vm - t = Process.clock_gettime(Process::CLOCK_MONOTONIC) - i = 0 - while i < <%= loop_count / 1000 %> - % 1000.times do |i| - a<%= i % num_methods %> - % end - i += 1 - end - % (loop_count % 1000).times do |i| - a<%= i % num_methods %> - % end - Process.clock_gettime(Process::CLOCK_MONOTONIC) - t - end - - vm # warmup call cache - File.write(<%= result.dump %>, vm) - EOS - end - end - private_constant :BenchmarkVM2VM - - # @param [Integer] num_methods - # @param [Integer] loop_count - BenchmarkVM2JT = ::BenchmarkDriver::Struct.new(:num_methods, :loop_count) do - # @param [String] result - A file to write result - def render(result:) - ERB.new(<<~EOS, trim_mode: '%').result(binding) - % num_methods.times do |i| - def a<%= i %> - nil - end - a<%= i %> - a<%= i %> # --jit-min-calls=2 - % end - RubyVM::MJIT.pause if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? - - def vm - t = Process.clock_gettime(Process::CLOCK_MONOTONIC) - i = 0 - while i < <%= loop_count / 1000 %> - % 1000.times do |i| - a<%= i % num_methods %> - % end - i += 1 - end - % (loop_count % 1000).times do |i| - a<%= i % num_methods %> - % end - Process.clock_gettime(Process::CLOCK_MONOTONIC) - t - end - - vm # warmup call cache - File.write(<%= result.dump %>, vm) - EOS - end - end - private_constant :BenchmarkVM2JT - - # @param [Integer] num_methods - # @param [Integer] loop_count - BenchmarkJT2JT = ::BenchmarkDriver::Struct.new(:num_methods, :loop_count) do - # @param [String] result - A file to write result - def render(result:) - ERB.new(<<~EOS, trim_mode: '%').result(binding) - % num_methods.times do |i| - def a<%= i %> - nil - end - % end - - # You may need to: - # * Increase `JIT_ISEQ_SIZE_THRESHOLD` to 10000000 in mjit.h - # * Always return false in `inlinable_iseq_p()` of mjit_compile.c - def jit - t = Process.clock_gettime(Process::CLOCK_MONOTONIC) - i = 0 - while i < <%= loop_count / 1000 %> - % 1000.times do |i| - a<%= i % num_methods %> - % end - i += 1 - end - % (loop_count % 1000).times do |i| - a<%= i % num_methods %> - % end - Process.clock_gettime(Process::CLOCK_MONOTONIC) - t - end - - jit - jit - RubyVM::MJIT.pause if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? - File.write(<%= result.dump %>, jit) - EOS - end - end - private_constant :BenchmarkJT2JT -end diff --git a/benchmark/lib/benchmark_driver/runner/ractor.rb b/benchmark/lib/benchmark_driver/runner/ractor.rb index c730b8e4a5..fd9c2dd4db 100644 --- a/benchmark/lib/benchmark_driver/runner/ractor.rb +++ b/benchmark/lib/benchmark_driver/runner/ractor.rb @@ -87,7 +87,7 @@ __bmdv_ractors << Ractor.new(__bmdv_loop_after - __bmdv_loop_before) { |__bmdv_l <% end %> # Wait for all Ractors before executing code to write results -__bmdv_ractors.map!(&:take) +__bmdv_ractors.map!(&:value) <% results.each do |result| %> File.write(<%= result.dump %>, __bmdv_ractors.shift) diff --git a/benchmark/loop_each.yml b/benchmark/loop_each.yml new file mode 100644 index 0000000000..1c757185a8 --- /dev/null +++ b/benchmark/loop_each.yml @@ -0,0 +1,4 @@ +prelude: | + arr = [nil] * 30_000_000 +benchmark: + loop_each: arr.each{|e|} diff --git a/benchmark/loop_generator.rb b/benchmark/loop_generator.rb index d3375c744c..6a3194b670 100644 --- a/benchmark/loop_generator.rb +++ b/benchmark/loop_generator.rb @@ -1,4 +1,4 @@ -max = 600000 +max = 6000000 if defined? Fiber gen = (1..max).each diff --git a/benchmark/loop_times_megamorphic.yml b/benchmark/loop_times_megamorphic.yml new file mode 100644 index 0000000000..f9343ba897 --- /dev/null +++ b/benchmark/loop_times_megamorphic.yml @@ -0,0 +1,7 @@ +prelude: | + eval(<<~EOS) + def loop_times_megamorphic + #{"1.times {|i|};" * 1000} + end + EOS +benchmark: loop_times_megamorphic diff --git a/benchmark/marshal_dump_load_integer.yml b/benchmark/marshal_dump_load_integer.yml new file mode 100644 index 0000000000..78ebf823d2 --- /dev/null +++ b/benchmark/marshal_dump_load_integer.yml @@ -0,0 +1,22 @@ +prelude: | + smallint_array = 1000.times.map { |x| x } + bigint32_array = 1000.times.map { |x| x + 2**32 } + bigint64_array = 1000.times.map { |x| x + 2**64 } + + smallint_dump = Marshal.dump(smallint_array) + bigint32_dump = Marshal.dump(bigint32_array) + bigint64_dump = Marshal.dump(bigint64_array) +benchmark: + marshal_dump_integer_small: | + Marshal.dump(smallint_array) + marshal_dump_integer_over_32_bit: | + Marshal.dump(bigint32_array) + marshal_dump_integer_over_64_bit: | + Marshal.dump(bigint64_array) + marshal_load_integer_small: | + Marshal.load(smallint_dump) + marshal_load_integer_over_32_bit: | + Marshal.load(bigint32_dump) + marshal_load_integer_over_64_bit: | + Marshal.load(bigint64_dump) +loop_count: 4000 diff --git a/benchmark/masgn.yml b/benchmark/masgn.yml index 4be9333e23..31cb8ee4a3 100644 --- a/benchmark/masgn.yml +++ b/benchmark/masgn.yml @@ -1,7 +1,7 @@ prelude: | a = [nil] * 3 b = Class.new{attr_writer :a, :b, :c}.new - c, d, e, f = nil, nil, nil, nil + c = d = e = f = g = h = i = nil benchmark: array2_2: "c = (a[0], a[1] = 1, 2)" array2_3: "c = (a[0], a[1] = 1, 2, 3)" @@ -27,3 +27,27 @@ benchmark: lvar2_3p: "(d, e = 1, 2, 3; nil)" lvar3_2p: "(d, e, f = 1, 2; nil)" lvar3_3p: "(d, e, f = 1, 2, 3; nil)" + array2_2lv: "c = (a[0], a[1] = g, h)" + array2_ilv: "c = (a[0], a[1] = g, h, i)" + arrayi_2lv: "c = (a[0], a[1], a[2] = g, h)" + arrayi_ilv: "c = (a[0], a[1], a[2] = g, h, i)" + attr2_2lv: "c = (b.a, b.b = g, h)" + attr2_ilv: "c = (b.a, b.b = g, h, i)" + attri_2lv: "c = (b.a, b.b, b.c = g, h)" + attri_ilv: "c = (b.a, b.b, b.c = g, h, i)" + lvar2_2lv: "c = (d, e = g, h)" + lvar2_ilv: "c = (d, e = g, h, i)" + lvari_2lv: "c = (d, e, f = g, h)" + lvari_ilv: "c = (d, e, f = g, h, i)" + array2_2plv: "(a[0], a[1] = g, h; nil)" + array2_iplv: "(a[0], a[1] = g, h, i; nil)" + arrayi_2plv: "(a[0], a[1], a[2] = g, h; nil)" + arrayi_iplv: "(a[0], a[1], a[2] = g, h, i; nil)" + attr2_2plv: "(b.a, b.b = g, h; nil)" + attr2_iplv: "(b.a, b.b = g, h, i; nil)" + attri_2plv: "(b.a, b.b, b.c = g, h; nil)" + attri_iplv: "(b.a, b.b, b.c = g, h, i; nil)" + lvar2_2plv: "(d, e = g, h; nil)" + lvar2_iplv: "(d, e = g, h, i; nil)" + lvari_2plv: "(d, e, f = g, h; nil)" + lvari_iplv: "(d, e, f = g, h, i; nil)" diff --git a/benchmark/mjit_exec_jt2jt.yml b/benchmark/mjit_exec_jt2jt.yml deleted file mode 100644 index 6c303c7a44..0000000000 --- a/benchmark/mjit_exec_jt2jt.yml +++ /dev/null @@ -1,6 +0,0 @@ -type: lib/benchmark_driver/runner/mjit_exec -num_methods: [1] -#num_methods: (1..100).to_a + [200, 300, 400, 500, 600, 700, 800, 900, 1000] -loop_count: 50000000 -from_jit: true -to_jit: true diff --git a/benchmark/mjit_exec_vm2jt.yml b/benchmark/mjit_exec_vm2jt.yml deleted file mode 100644 index 764883f070..0000000000 --- a/benchmark/mjit_exec_vm2jt.yml +++ /dev/null @@ -1,6 +0,0 @@ -type: lib/benchmark_driver/runner/mjit_exec -num_methods: [1] -#num_methods: (1..100).to_a + [200, 300, 400, 500, 600, 700, 800, 900, 1000] -loop_count: 50000000 -from_jit: false -to_jit: true diff --git a/benchmark/mjit_exec_vm2vm.yml b/benchmark/mjit_exec_vm2vm.yml deleted file mode 100644 index 030aa76c1c..0000000000 --- a/benchmark/mjit_exec_vm2vm.yml +++ /dev/null @@ -1,6 +0,0 @@ -type: lib/benchmark_driver/runner/mjit_exec -num_methods: [1] -#num_methods: (1..100).to_a + [200, 300, 400, 500, 600, 700, 800, 900, 1000] -loop_count: 50000000 -from_jit: false -to_jit: false diff --git a/benchmark/mjit_exivar.yml b/benchmark/mjit_exivar.yml deleted file mode 100644 index 2584fa6410..0000000000 --- a/benchmark/mjit_exivar.yml +++ /dev/null @@ -1,18 +0,0 @@ -type: lib/benchmark_driver/runner/mjit -prelude: | - class Bench < Hash - def initialize - @exivar = nil - end - - def exivar - @exivar - end - end - - bench = Bench.new - -benchmark: - mjit_exivar: bench.exivar - -loop_count: 200000000 diff --git a/benchmark/mjit_integer.yml b/benchmark/mjit_integer.yml deleted file mode 100644 index a6b5c9ee16..0000000000 --- a/benchmark/mjit_integer.yml +++ /dev/null @@ -1,32 +0,0 @@ -type: lib/benchmark_driver/runner/mjit -prelude: | - def mjit_abs(int) int.abs end - def mjit_bit_length(int) int.bit_length end - def mjit_comp(int) ~int end - def mjit_even?(int) int.even? end - def mjit_integer?(int) int.integer? end - def mjit_magnitude(int) int.magnitude end - def mjit_odd?(int) int.odd? end - def mjit_ord(int) int.ord end - def mjit_size(int) int.size end - def mjit_to_i(int) int.to_i end - def mjit_to_int(int) int.to_int end - def mjit_uminus(int) -int end - def mjit_zero?(int) int.zero? end - -benchmark: - - mjit_abs(-1) - - mjit_bit_length(100) - - mjit_comp(1) - - mjit_even?(2) - - mjit_integer?(0) - - mjit_magnitude(-1) - - mjit_odd?(1) - - mjit_ord(1) - - mjit_size(1) - - mjit_to_i(1) - - mjit_to_int(1) - - mjit_uminus(1) - - mjit_zero?(0) - -loop_count: 40000000 diff --git a/benchmark/mjit_kernel.yml b/benchmark/mjit_kernel.yml deleted file mode 100644 index 7720e65c2c..0000000000 --- a/benchmark/mjit_kernel.yml +++ /dev/null @@ -1,20 +0,0 @@ -type: lib/benchmark_driver/runner/mjit -prelude: | - def mjit_class(obj) - obj.class - end - - def mjit_frozen?(obj) - obj.frozen? - end - - str = "" - fstr = "".freeze - -benchmark: - - mjit_class(self) - - mjit_class(1) - - mjit_frozen?(str) - - mjit_frozen?(fstr) - -loop_count: 40000000 diff --git a/benchmark/mjit_leave.yml b/benchmark/mjit_leave.yml deleted file mode 100644 index 9ac68b164b..0000000000 --- a/benchmark/mjit_leave.yml +++ /dev/null @@ -1,8 +0,0 @@ -type: lib/benchmark_driver/runner/mjit -prelude: | - def leave - nil - end -benchmark: - mjit_leave: leave -loop_count: 200000000 diff --git a/benchmark/mjit_opt_cc_insns.yml b/benchmark/mjit_opt_cc_insns.yml deleted file mode 100644 index fed6d34bd5..0000000000 --- a/benchmark/mjit_opt_cc_insns.yml +++ /dev/null @@ -1,27 +0,0 @@ -# opt_* insns using vm_method_cfunc_is with send-compatible operands: -# * opt_nil_p -# * opt_not -# * opt_eq -type: lib/benchmark_driver/runner/mjit -prelude: | - def mjit_nil?(obj) - obj.nil? - end - - def mjit_not(obj) - !obj - end - - def mjit_eq(a, b) - a == b - end - -benchmark: - - script: mjit_nil?(1) - loop_count: 40000000 - - script: mjit_not(1) - loop_count: 40000000 - - script: mjit_eq(1, nil) - loop_count: 8000000 - - script: mjit_eq(nil, 1) - loop_count: 8000000 diff --git a/benchmark/mjit_struct_aref.yml b/benchmark/mjit_struct_aref.yml deleted file mode 100644 index bfba1323f2..0000000000 --- a/benchmark/mjit_struct_aref.yml +++ /dev/null @@ -1,10 +0,0 @@ -type: lib/benchmark_driver/runner/mjit -prelude: | - def mjit_struct_aref(struct) - struct.aa - end - struct = Struct.new(:a0, :a1, :a2, :a3, :a4, :a5, :a6, :a7, :a8, :a9, :aa).new - -benchmark: mjit_struct_aref(struct) - -loop_count: 40000000 diff --git a/benchmark/module_eqq.yml b/benchmark/module_eqq.yml new file mode 100644 index 0000000000..2f9c490d92 --- /dev/null +++ b/benchmark/module_eqq.yml @@ -0,0 +1,32 @@ +prelude: | + module SomeModule; end + class SimpleClass; end + class MediumClass + 10.times { include Module.new } + end + class LargeClass + 100.times { include Module.new } + end + class HugeClass + 300.times { include Module.new } + end + SimpleObj = SimpleClass.new + MediumObj = MediumClass.new + LargeObj = LargeClass.new + HugeObj = HugeClass.new +benchmark: + simple_class_eqq_simple_obj: | + SimpleClass === SimpleObj + medium_class_eqq_simple_obj: | + MediumClass === SimpleObj + simple_class_eqq_medium_obj: | + SimpleClass === MediumObj + simple_class_eqq_large_obj: | + SimpleClass === LargeObj + simple_class_eqq_huge_obj: | + SimpleClass === HugeObj + simple_class_eqq_module: | + SimpleClass === HugeObj + module_eqq_module: | + SomeModule === HugeObj +loop_count: 10000000 diff --git a/benchmark/nilclass.yml b/benchmark/nilclass.yml index fba67a5f6a..66234c4cdf 100644 --- a/benchmark/nilclass.yml +++ b/benchmark/nilclass.yml @@ -1,6 +1,16 @@ +prelude: | + def a = nil benchmark: + rationalize: + nil.rationalize + to_c: | + nil.to_c to_i: | nil.to_i to_f: | nil.to_f + to_r: | + nil.to_r + splat: | + a(*nil) loop_count: 100000 diff --git a/benchmark/numeric_methods.yml b/benchmark/numeric_methods.yml index 433c2268a3..1384902935 100644 --- a/benchmark/numeric_methods.yml +++ b/benchmark/numeric_methods.yml @@ -10,4 +10,20 @@ benchmark: int.finite? infinite?: | int.infinite? + integer_real: | + int.real + float_real: | + flo.real + integr_imag: | + int.imag + float_imag: | + flo.imag + integer_conj: | + int.conj + float_conj: | + flo.conj + integer_numerator: | + int.numerator + integer_denominator: | + int.denominator loop_count: 20000000 diff --git a/benchmark/object_allocate.yml b/benchmark/object_allocate.yml index 93ff463e41..c6269923f0 100644 --- a/benchmark/object_allocate.yml +++ b/benchmark/object_allocate.yml @@ -11,6 +11,26 @@ prelude: | class OneTwentyEight 128.times { include(Module.new) } end + class OnePositional + def initialize a; end + end + class TwoPositional + def initialize a, b; end + end + class ThreePositional + def initialize a, b, c; end + end + class FourPositional + def initialize a, b, c, d; end + end + class KWArg + def initialize a:, b:, c:, d: + end + end + class Mixed + def initialize a, b, c:, d: + end + end # Disable GC to see raw throughput: GC.disable benchmark: @@ -18,4 +38,12 @@ benchmark: allocate_32_deep: ThirtyTwo.new allocate_64_deep: SixtyFour.new allocate_128_deep: OneTwentyEight.new + allocate_1_positional_params: OnePositional.new(1) + allocate_2_positional_params: TwoPositional.new(1, 2) + allocate_3_positional_params: ThreePositional.new(1, 2, 3) + allocate_4_positional_params: FourPositional.new(1, 2, 3, 4) + allocate_kwarg_params: "KWArg.new(a: 1, b: 2, c: 3, d: 4)" + allocate_mixed_params: "Mixed.new(1, 2, c: 3, d: 4)" + allocate_no_params: "Object.new" + allocate_allocate: "Object.allocate" loop_count: 100000 diff --git a/benchmark/object_class.yml b/benchmark/object_class.yml new file mode 100644 index 0000000000..1e5409d1e2 --- /dev/null +++ b/benchmark/object_class.yml @@ -0,0 +1,40 @@ +prelude: | + def get_class(obj) + i = 10_000 + while i > 0 + i -= 1 + # 100 times per loop + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; obj.class; + end + end + + class Obj + end + obj = Obj.new + + singleton = Obj.new + def singleton.bar + end + + extended = Obj.new + 2.times do + extended.extend Module.new + end + + immediate = 1.4 +benchmark: + obj: get_class(obj) + extended: get_class(extended) + singleton: get_class(singleton) + immediate: get_class(immediate) +loop_count: 1000 diff --git a/benchmark/object_id.yml b/benchmark/object_id.yml new file mode 100644 index 0000000000..2bd52b923f --- /dev/null +++ b/benchmark/object_id.yml @@ -0,0 +1,4 @@ +benchmark: + baseline: "Object.new" + object_id: "Object.new.object_id" +# loop_count: 100000 diff --git a/benchmark/ractor_string_fstring.yml b/benchmark/ractor_string_fstring.yml new file mode 100644 index 0000000000..14b92d8fd8 --- /dev/null +++ b/benchmark/ractor_string_fstring.yml @@ -0,0 +1,18 @@ +type: lib/benchmark_driver/runner/ractor +benchmark: + ractor_fstring_random: | + i = 0 + str = "same".dup + while i < 2000000 + -(i.to_s.freeze) + i += 1 + end + ractor_fstring_same: | + i = 0 + str = "same".dup + while i < 2000000 + -str + i += 1 + end +loop_count: 1 +ractor: 4 diff --git a/benchmark/range_bsearch_bignum.yml b/benchmark/range_bsearch_bignum.yml new file mode 100644 index 0000000000..5730c93fcf --- /dev/null +++ b/benchmark/range_bsearch_bignum.yml @@ -0,0 +1,10 @@ +prelude: | + first = 2**100 + last = 2**1000 + mid = (first + last) / 2 + r = first..last + +benchmark: + first: r.bsearch { |x| x >= first } + mid: r.bsearch { |x| x >= mid } + last: r.bsearch { |x| x >= last } diff --git a/benchmark/range_bsearch_endpointless.yml b/benchmark/range_bsearch_endpointless.yml new file mode 100644 index 0000000000..8d7bedb662 --- /dev/null +++ b/benchmark/range_bsearch_endpointless.yml @@ -0,0 +1,21 @@ +prelude: | + re = (1..) + rb = (..0) + +benchmark: + 'endless 10**0': re.bsearch { |x| x >= 1 } + 'endless 10**1': re.bsearch { |x| x >= 10 } + 'endless 10**2': re.bsearch { |x| x >= 100 } + 'endless 10**3': re.bsearch { |x| x >= 1000 } + 'endless 10**4': re.bsearch { |x| x >= 10000 } + 'endless 10**5': re.bsearch { |x| x >= 100000 } + 'endless 10**10': re.bsearch { |x| x >= 10000000000 } + 'endless 10**100': re.bsearch { |x| x >= 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 } + 'beginless -10**0': rb.bsearch { |x| x >= -1 } + 'beginless -10**1': rb.bsearch { |x| x >= -10 } + 'beginless -10**2': rb.bsearch { |x| x >= -100 } + 'beginless -10**3': rb.bsearch { |x| x >= -1000 } + 'beginless -10**4': rb.bsearch { |x| x >= -10000 } + 'beginless -10**5': rb.bsearch { |x| x >= -100000 } + 'beginless -10**10': rb.bsearch { |x| x >= -10000000000 } + 'beginless -10**100': rb.bsearch { |x| x >= -10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 } diff --git a/benchmark/range_bsearch_fixnum.yml b/benchmark/range_bsearch_fixnum.yml new file mode 100644 index 0000000000..59416531b9 --- /dev/null +++ b/benchmark/range_bsearch_fixnum.yml @@ -0,0 +1,10 @@ +prelude: | + first = 1 + last = 10000 + mid = (first + last) / 2 + r = first..last + +benchmark: + first: r.bsearch { |x| x >= first } + mid: r.bsearch { |x| x >= mid } + last: r.bsearch { |x| x >= last } diff --git a/benchmark/range_count.yml b/benchmark/range_count.yml new file mode 100644 index 0000000000..58f53a0236 --- /dev/null +++ b/benchmark/range_count.yml @@ -0,0 +1,11 @@ +prelude: | + r_1 = 1..1 + r_1k = 1..1000 + r_1m = 1..1000000 + r_str = 'a'..'z' + +benchmark: + 'int 1': r_1.count + 'int 1K': r_1k.count + 'int 1M': r_1m.count + string: r_str.count diff --git a/benchmark/range_min.yml b/benchmark/range_min.yml new file mode 100644 index 0000000000..9e60dd7308 --- /dev/null +++ b/benchmark/range_min.yml @@ -0,0 +1,2 @@ +benchmark: + - (1..10).min diff --git a/benchmark/range_overlap.yml b/benchmark/range_overlap.yml new file mode 100644 index 0000000000..700a00053c --- /dev/null +++ b/benchmark/range_overlap.yml @@ -0,0 +1,19 @@ +prelude: | + class Range + unless method_defined?(:overlap?) + def overlap?(other) + other.begin == self.begin || cover?(other.begin) || other.cover?(self.begin) + end + end + end + +benchmark: + - (2..3).overlap?(1..1) + - (2..3).overlap?(2..4) + - (2..3).overlap?(4..5) + - (2..3).overlap?(2..1) + - (2..3).overlap?(0..1) + - (2..3).overlap?(...1) + - (2...3).overlap?(..2) + - (2...3).overlap?(3...) + - (2..3).overlap?('a'..'d') diff --git a/benchmark/range_reverse_each.yml b/benchmark/range_reverse_each.yml new file mode 100644 index 0000000000..a32efeccc6 --- /dev/null +++ b/benchmark/range_reverse_each.yml @@ -0,0 +1,16 @@ +prelude: | + rf_1 = 0..1 + rf_1k = 0..1000 + rf_1m = 0..1000000 + big = 2**1000 + rb_1 = big..big+1 + rb_1k = big..big+1000 + rb_1m = big..big+1000000 + +benchmark: + "Fixnum 1": rf_1.reverse_each { _1 } + "Fixnum 1K": rf_1k.reverse_each { _1 } + "Fixnum 1M": rf_1m.reverse_each { _1 } + "Bignum 1": rb_1.reverse_each { _1 } + "Bignum 1K": rb_1k.reverse_each { _1 } + "Bignum 1M": rb_1m.reverse_each { _1 } diff --git a/benchmark/realpath.yml b/benchmark/realpath.yml index 90a029d5b9..6b6a4836b0 100644 --- a/benchmark/realpath.yml +++ b/benchmark/realpath.yml @@ -12,6 +12,9 @@ prelude: | relative_dir = 'b/c' absolute_dir = File.join(pwd, relative_dir) file_dir = 'c' +teardown: | + require 'fileutils' + FileUtils.rm_rf('b') benchmark: relative_nil: "f.realpath(relative, nil)" absolute_nil: "f.realpath(absolute, nil)" diff --git a/benchmark/regexp_dup.yml b/benchmark/regexp_dup.yml new file mode 100644 index 0000000000..52f89991cd --- /dev/null +++ b/benchmark/regexp_dup.yml @@ -0,0 +1,6 @@ +prelude: | + str = "a" * 1000 + re = Regexp.new(str) + +benchmark: + dup: re.dup diff --git a/benchmark/regexp_new.yml b/benchmark/regexp_new.yml new file mode 100644 index 0000000000..bc9ab3ca21 --- /dev/null +++ b/benchmark/regexp_new.yml @@ -0,0 +1,7 @@ +prelude: | + str = "a" * 1000 + re = Regexp.new(str) + +benchmark: + string: Regexp.new(str) + regexp: Regexp.new(re) diff --git a/benchmark/scan.yaml b/benchmark/scan.yaml new file mode 100644 index 0000000000..62ad1d6862 --- /dev/null +++ b/benchmark/scan.yaml @@ -0,0 +1,16 @@ +prelude: | + $LOAD_PATH.unshift(File.expand_path("lib")) + require "strscan" + str = "test string" + scanner = StringScanner.new(str) + str = "test" + reg = /test/ +benchmark: + check(reg): | + scanner.check(reg) + check(str): | + scanner.check(str) + match?(reg): | + scanner.match?(reg) + match?(str): | + scanner.match?(str) diff --git a/benchmark/search.yaml b/benchmark/search.yaml new file mode 100644 index 0000000000..42a50c90e6 --- /dev/null +++ b/benchmark/search.yaml @@ -0,0 +1,16 @@ +prelude: | + $LOAD_PATH.unshift(File.expand_path("lib")) + require "strscan" + str = "test string" + scanner = StringScanner.new(str) + str = "string" + reg = /string/ +benchmark: + check_until(reg): | + scanner.check_until(reg) + check_until(str): | + scanner.check_until(str) + exist?(reg): | + scanner.exist?(reg) + exist?(str): | + scanner.exist?(str) diff --git a/benchmark/set.yml b/benchmark/set.yml new file mode 100644 index 0000000000..061509cb1f --- /dev/null +++ b/benchmark/set.yml @@ -0,0 +1,261 @@ +prelude: | + # First 1000 digits of pi + pi = <<~END.gsub(/\D/, '') + 31415926535897932384626433832795028841971693993751058209749445923078164062862089 + 98628034825342117067982148086513282306647093844609550582231725359408128481117450 + 28410270193852110555964462294895493038196442881097566593344612847564823378678316 + 52712019091456485669234603486104543266482133936072602491412737245870066063155881 + 74881520920962829254091715364367892590360011330530548820466521384146951941511609 + 43305727036575959195309218611738193261179310511854807446237996274956735188575272 + 48912279381830119491298336733624406566430860213949463952247371907021798609437027 + 70539217176293176752384674818467669405132000568127145263560827785771342757789609 + 17363717872146844090122495343014654958537105079227968925892354201995611212902196 + 08640344181598136297747713099605187072113499999983729780499510597317328160963185 + 95024459455346908302642522308253344685035261931188171010003137838752886587533208 + 38142061717766914730359825349042875546873115956286388235378759375195778185778053 + 21712268066130019278766111959092164201989380952572010654505906988788448549 + END + array1 = 10.times.flat_map do |i| + pi[i...].chars.each_slice(10).map(&:join) + end + array2 = array1.map(&:reverse) + array1.map!(&:to_i) + array2.map!(&:to_i) + a1 = array1[...10] + a2 = array1[...100] + a3 = array1 + oa1 = array2[...10] + oa2 = array2[...100] + oa3 = array2 + s0 = Set.new + s0 = Set.new + s1 = Set.new(a1) + s2 = Set.new(a2) + s3 = Set.new(a3) + o0 = Set.new + o1 = Set.new(array2[...10]) + o2 = Set.new(array2[...100]) + o3 = Set.new(array2) + d0 = s0.dup + d1 = s1.dup + d2 = s2.dup + d3 = s3.dup + ss1 = s1 - a1[-1..-1] + ss2 = s2 - a2[-1..-1] + ss3 = s3 - a3[-1..-1] + os1 = o1 - oa1[-1..-1] + os2 = o2 - oa2[-1..-1] + os3 = o3 - oa3[-1..-1] + member = a1.first + cbi = s0.dup.compare_by_identity + ns = Set[s3, o3, d3] + set_subclass = Class.new(Set) + +benchmark: + new_0: Set.new + new_10: Set.new(a1) + new_100: Set.new(a2) + new_1000: Set.new(a3) + aref_0: Set[] + aref_10: Set[*a1] + aref_100: Set[*a2] + aref_1000: Set[*a3] + amp_0: s0 & o0 + amp_10: s1 & o1 + amp_100: s2 & o2 + amp_1000: s3 & o3 + amp_same_0: s0 & d0 + amp_same_10: s1 & d1 + amp_same_100: s2 & d2 + amp_same_1000: s3 & d3 + minus_0: s0 - o0 + minus_10: s1 - o1 + minus_100: s2 - o2 + minus_1000: s3 - o3 + minus_same_0: s0 - d0 + minus_same_10: s1 - d1 + minus_same_100: s2 - d2 + minus_same_1000: s3 - d3 + spaceship_0: s0 <=> o0 + spaceship_diff_10: s1 <=> o1 + spaceship_diff_100: s2 <=> o2 + spaceship_diff_1000: s2 <=> o3 + spaceship_sub_10: s1 <=> ss1 + spaceship_sub_100: s2 <=> ss2 + spaceship_sub_1000: s2 <=> ss3 + spaceship_sup_10: ss1 <=> s1 + spaceship_sup_100: ss2 <=> s2 + spaceship_sup_1000: ss2 <=> s3 + eq_0: s0 == o0 + eq_10: s1 == o1 + eq_100: s2 == o2 + eq_1000: s3 == o3 + eq_same_0: s0 == d0 + eq_same_10: s1 == d1 + eq_same_100: s2 == d2 + eq_same_1000: s3 == d3 + xor_0: s0 ^ o0 + xor_10: s1 ^ o1 + xor_100: s2 ^ o2 + xor_1000: s3 ^ o3 + xor_same_0: s0 ^ d0 + xor_same_10: s1 ^ d1 + xor_same_100: s2 ^ d2 + xor_same_1000: s3 ^ d3 + pipe_0: s0 | o0 + pipe_10: s1 | o1 + pipe_100: s2 | o2 + pipe_1000: s3 | o3 + pipe_same_0: s0 | d0 + pipe_same_10: s1 | d1 + pipe_same_100: s2 | d2 + pipe_same_1000: s3 | d3 + add: a3.each { s0.add(it) } + add_exist: a3.each { s3.add(it) } + addq: a3.each { s0.add?(it) } + addq_exist: a3.each { s3.add?(it) } + classify_0: s0.classify { it } + classify_10: s1.classify { it & 2 } + classify_100: s2.classify { it & 8 } + classify_1000: s3.classify { it & 32 } + clear: s0.clear + collect_0: s0.collect! { it } + collect_10: s1.collect! { it } + collect_100: s2.collect! { it } + collect_1000: s3.collect! { it } + compare_by_identity_0: s0.dup.compare_by_identity + compare_by_identity_10: s1.dup.compare_by_identity + compare_by_identity_100: s2.dup.compare_by_identity + compare_by_identity_1000: s3.dup.compare_by_identity + compare_by_identityq_false: s0.compare_by_identity? + compare_by_identityq_true: cbi.compare_by_identity? + clone_0: s0.clone + clone_10: s1.clone + clone_100: s2.clone + clone_1000: s3.clone + delete: a3.each { s3.delete(it) } + delete_not_exist: a3.each { o3.delete(it) } + deleteq: a3.each { s3.delete?(it) } + deleteq_not_exist: a3.each { o3.delete?(it) } + delete_if_0: s0.delete_if { it } + delete_if_10: s1.delete_if { it & 2 == 0 } + delete_if_100: s2.delete_if { it & 2 == 0 } + delete_if_1000: s3.delete_if { it & 2 == 0 } + disjoint_0: s0.disjoint? o0 + disjoint_10: s1.disjoint? o1 + disjoint_100: s2.disjoint? o2 + disjoint_1000: s3.disjoint? o3 + disjoint_same_0: s0.disjoint? d0 + disjoint_same_10: s1.disjoint? d1 + disjoint_same_100: s2.disjoint? d2 + disjoint_same_1000: s3.disjoint? d3 + divide_1arity_0: s0.divide { true } + divide_1arity_10: s1.divide { it & 2 } + divide_1arity_100: s2.divide { it & 8 } + divide_1arity_1000: s3.divide { it & 32 } + divide_2arity_0: s0.divide { true } + divide_2arity_10: s1.divide { (_1 & 2) == (_2 & 2) } + divide_2arity_100: s2.divide { (_1 & 8) == (_2 & 8) } + divide_2arity_1000: s3.divide { (_1 & 32) == (_2 & 32) } + dup_0: s0.dup + dup_10: s1.dup + dup_100: s2.dup + dup_1000: s3.dup + each_0: s0.each { it } + each_10: s1.each { it } + each_100: s2.each { it } + each_1000: s3.each { it } + empty_true: s0.empty? + empty_false: s3.empty? + flatten: ns.flatten + flattenb: ns.flatten! + include_true_0: s0.include? member + include_true_10: s1.include? member + include_true_100: s2.include? member + include_true_1000: s3.include? member + include_false_0: s0.include?(-1) + include_false_10: s1.include?(-1) + include_false_100: s2.include?(-1) + include_false_1000: s3.include?(-1) + intersect_0: s0.intersect? o0 + intersect_10: s1.intersect? o1 + intersect_100: s2.intersect? o2 + intersect_1000: s3.intersect? o3 + intersect_same_0: s0.intersect? d0 + intersect_same_10: s1.intersect? d1 + intersect_same_100: s2.intersect? d2 + intersect_same_1000: s3.intersect? d3 + join_0: s0.join + join_10: s1.join + join_100: s2.join + join_1000: s3.join + join_arg_0: s0.join "" + join_arg_10: s1.join "" + join_arg_100: s2.join "" + join_arg_1000: s3.join "" + keep_if_0: s0.keep_if { it } + keep_if_10: s1.keep_if { it & 2 == 0 } + keep_if_100: s2.keep_if { it & 2 == 0 } + keep_if_1000: s3.keep_if { it & 2 == 0 } + merge_set: s0.dup.merge(s3, o3) + merge_enum: s0.dup.merge(array1, array2) + proper_subset_0: s0.proper_subset? s0 + proper_subset_10: s1.proper_subset? ss1 + proper_subset_100: s2.proper_subset? ss2 + proper_subset_1000: s3.proper_subset? ss3 + proper_subset_false_10: s1.proper_subset? os1 + proper_subset_false_100: s2.proper_subset? os2 + proper_subset_false_1000: s3.proper_subset? os3 + proper_superset_0: s0.proper_superset? s0 + proper_superset_10: ss1.proper_superset? s1 + proper_superset_100: ss2.proper_superset? s2 + proper_superset_1000: ss3.proper_superset? s3 + proper_superset_false_10: os1.proper_superset? s1 + proper_superset_false_100: os2.proper_superset? s2 + proper_superset_false_1000: os3.proper_superset? s3 + reject_0: s0.reject! { it } + reject_10: s1.reject! { it & 2 == 0 } + reject_100: s2.reject! { it & 2 == 0 } + reject_1000: s3.reject! { it & 2 == 0 } + replace_0: s = Set.new; array1.each { s.replace(s0) } + replace_10: s = Set.new; array1.each { s.replace(s1) } + replace_100: s = Set.new; array1.each { s.replace(s2) } + replace_1000: s = Set.new; array1.each { s.replace(s3) } + reset_0: s0.reset + reset_10: s1.reset + reset_100: s2.reset + reset_1000: s3.reset + select_0: s0.select! { it } + select_10: s1.select! { it & 2 == 0 } + select_100: s2.select! { it & 2 == 0 } + select_1000: s3.select! { it & 2 == 0 } + size_0: s0.size + size_10: s1.size + size_100: s2.size + size_1000: s3.size + subtract_set: s3.dup.subtract(os3) + subtract_enum: s3.dup.subtract(oa3) + subtract_same_set: s3.dup.subtract(s3) + subtract_same_enum: s3.dup.subtract(a3) + subset_0: s0.subset? s0 + subset_10: s1.subset? ss1 + subset_100: s2.subset? ss2 + subset_1000: s3.subset? ss3 + subset_false_10: s1.subset? os1 + subset_false_100: s2.subset? os2 + subset_false_1000: s3.subset? os3 + superset_0: s0.superset? s0 + superset_10: ss1.superset? s1 + superset_100: ss2.superset? s2 + superset_1000: ss3.superset? s3 + superset_false_10: os1.superset? s1 + superset_false_100: os2.superset? s2 + superset_false_1000: os3.superset? s3 + to_a_0: s0.to_a + to_a_10: s1.to_a + to_a_100: s2.to_a + to_a_1000: s3.to_a + to_set_0: s0.to_set + to_set_10: s1.to_set + to_set_100: s2.to_set + to_set_1000: s3.to_set diff --git a/benchmark/so_count_words.yml b/benchmark/so_count_words.yml index 99683505f9..f7322a8541 100644 --- a/benchmark/so_count_words.yml +++ b/benchmark/so_count_words.yml @@ -15,13 +15,13 @@ prelude: | Newsgroups: rec.games.roguelike.nethack X-Mailer: Mozilla 1.1N (Macintosh; I; 68K) - Hello there, Izchak Miller was my father. When I was younger I spent - many a night, hunched over the keyboard with a cup of tea, playing - nethack with him and my brother. my dad was a philosopher with a strong - weakness for fantasy/sci fi. I remember when he started to get involved - with the Nethack team- my brother's Dungeons and Dragons monster book - found a regular place beside my dad's desk. it's nice to see him living - on in the game he loved so much :-). + Hello there, Izchak Miller was my father. When I was younger I spent + many a night, hunched over the keyboard with a cup of tea, playing + nethack with him and my brother. my dad was a philosopher with a strong + weakness for fantasy/sci fi. I remember when he started to get involved + with the Nethack team- my brother's Dungeons and Dragons monster book + found a regular place beside my dad's desk. it's nice to see him living + on in the game he loved so much :-). Tamar Miller The following is a really long word of 5000 characters: @@ -38,8 +38,9 @@ prelude: | 13.times{ data << data } - open(wcinput, 'w'){|f| f.write data} + File.write(wcinput, data) end + at_exit {File.unlink(wcinput) rescue nil} end prepare_wc_input(wc_input_base) @@ -49,16 +50,16 @@ benchmark: # $Id: wc-ruby.code,v 1.4 2004/11/13 07:43:32 bfulgham Exp $ # http://www.bagley.org/~doug/shootout/ # with help from Paul Brannan - input = open(File.join(File.dirname($0), 'wc.input'), 'rb') nl = nw = nc = 0 - while true - tmp = input.read(4096) or break - data = tmp << (input.gets || "") - nc += data.length - nl += data.count("\n") - ((data.strip! || data).tr!("\n", " ") || data).squeeze! - nw += data.count(" ") + 1 + File.open(File.join(File.dirname($0), 'wc.input'), 'rb') do |input| + while tmp = input.read(4096) + data = tmp << (input.gets || "") + nc += data.length + nl += data.count("\n") + ((data.strip! || data).tr!("\n", " ") || data).squeeze! + nw += data.count(" ") + 1 + end end # STDERR.puts "#{nl} #{nw} #{nc}" diff --git a/benchmark/so_meteor_contest.rb b/benchmark/so_meteor_contest.rb index 8c136baa6c..d8c8e3ab9c 100644 --- a/benchmark/so_meteor_contest.rb +++ b/benchmark/so_meteor_contest.rb @@ -447,7 +447,7 @@ end # as an inverse. The inverse will ALWAYS be 3 one of the piece configurations that is exactly 3 rotations away # (an odd number). Checking even vs odd then produces a higher probability of finding more pieces earlier # in the cycle. We still need to keep checking all the permutations, but our probability of finding one will -# diminsh over time. Since we are TOLD how many to search for this lets us exit before checking all pieces +# diminish over time. Since we are TOLD how many to search for this lets us exit before checking all pieces # this bennifit is very great when seeking small numbers of solutions and is 0 when looking for more than the # maximum number def find_top( rotation_skip) diff --git a/benchmark/so_nbody.rb b/benchmark/so_nbody.rb index d6c5bb9e61..9884fc4edc 100644 --- a/benchmark/so_nbody.rb +++ b/benchmark/so_nbody.rb @@ -12,38 +12,38 @@ def _puts *args end class Planet - attr_accessor :x, :y, :z, :vx, :vy, :vz, :mass + attr_accessor :x, :y, :z, :vx, :vy, :vz, :mass - def initialize(x, y, z, vx, vy, vz, mass) - @x, @y, @z = x, y, z - @vx, @vy, @vz = vx * DAYS_PER_YEAR, vy * DAYS_PER_YEAR, vz * DAYS_PER_YEAR - @mass = mass * SOLAR_MASS - end - - def move_from_i(bodies, nbodies, dt, i) - while i < nbodies - b2 = bodies[i] - dx = @x - b2.x - dy = @y - b2.y - dz = @z - b2.z - - distance = Math.sqrt(dx * dx + dy * dy + dz * dz) - mag = dt / (distance * distance * distance) - b_mass_mag, b2_mass_mag = @mass * mag, b2.mass * mag - - @vx -= dx * b2_mass_mag - @vy -= dy * b2_mass_mag - @vz -= dz * b2_mass_mag - b2.vx += dx * b_mass_mag - b2.vy += dy * b_mass_mag - b2.vz += dz * b_mass_mag - i += 1 + def initialize(x, y, z, vx, vy, vz, mass) + @x, @y, @z = x, y, z + @vx, @vy, @vz = vx * DAYS_PER_YEAR, vy * DAYS_PER_YEAR, vz * DAYS_PER_YEAR + @mass = mass * SOLAR_MASS end - @x += dt * @vx - @y += dt * @vy - @z += dt * @vz - end + def move_from_i(bodies, nbodies, dt, i) + while i < nbodies + b2 = bodies[i] + dx = @x - b2.x + dy = @y - b2.y + dz = @z - b2.z + + distance = Math.sqrt(dx * dx + dy * dy + dz * dz) + mag = dt / (distance * distance * distance) + b_mass_mag, b2_mass_mag = @mass * mag, b2.mass * mag + + @vx -= dx * b2_mass_mag + @vy -= dy * b2_mass_mag + @vz -= dz * b2_mass_mag + b2.vx += dx * b_mass_mag + b2.vy += dy * b_mass_mag + b2.vz += dz * b_mass_mag + i += 1 + end + + @x += dt * @vx + @y += dt * @vy + @z += dt * @vz + end end def energy(bodies) diff --git a/benchmark/string_casecmp.yml b/benchmark/string_casecmp.yml index 2354040a04..88a3555c8a 100644 --- a/benchmark/string_casecmp.yml +++ b/benchmark/string_casecmp.yml @@ -20,7 +20,9 @@ benchmark: casecmp-10: lstr10.casecmp(ustr10) casecmp-100: lstr100.casecmp(ustr100) casecmp-1000: lstr1000.casecmp(ustr1000) + casecmp-1000vs10: lstr1000.casecmp(ustr10) casecmp-nonascii1: lnonascii1.casecmp(unonascii1) casecmp-nonascii10: lnonascii10.casecmp(unonascii10) casecmp-nonascii100: lnonascii100.casecmp(unonascii100) casecmp-nonascii1000: lnonascii1000.casecmp(unonascii1000) + casecmp-nonascii1000vs10: lnonascii1000.casecmp(unonascii10) diff --git a/benchmark/string_concat.yml b/benchmark/string_concat.yml new file mode 100644 index 0000000000..f11f95ee9a --- /dev/null +++ b/benchmark/string_concat.yml @@ -0,0 +1,51 @@ +prelude: | + CHUNK = "a" * 64 + UCHUNK = "é" * 32 + SHORT = "a" * (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] / 2) + LONG = "a" * (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] * 2) + GC.disable # GC causes a lot of variance +benchmark: + binary_concat_7bit: | + buffer = String.new(capacity: 4096, encoding: Encoding::BINARY) + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + utf8_concat_7bit: | + buffer = String.new(capacity: 4096, encoding: Encoding::UTF_8) + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + buffer << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK << CHUNK + utf8_concat_UTF8: | + buffer = String.new(capacity: 4096, encoding: Encoding::UTF_8) + buffer << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK + buffer << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK + buffer << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK + buffer << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK + buffer << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK + buffer << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK + buffer << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK + buffer << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK << UCHUNK + interpolation: | + buffer = "#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}" \ + "#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}" \ + "#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}" \ + "#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}" \ + "#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}" \ + "#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}" \ + "#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}" \ + "#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}" \ + "#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}#{CHUNK}" + interpolation_same_heap: | + buffer = "#{SHORT}#{SHORT}" + interpolation_switching_heaps: | + buffer = "#{SHORT}#{LONG}" diff --git a/benchmark/string_dup.yml b/benchmark/string_dup.yml new file mode 100644 index 0000000000..90793f9f2a --- /dev/null +++ b/benchmark/string_dup.yml @@ -0,0 +1,7 @@ +prelude: | + # frozen_string_literal: true +benchmark: + uplus: | + +"A" + dup: | + "A".dup diff --git a/benchmark/string_fstring.yml b/benchmark/string_fstring.yml new file mode 100644 index 0000000000..cafef1f3fe --- /dev/null +++ b/benchmark/string_fstring.yml @@ -0,0 +1,16 @@ +benchmark: + fstring_random: | + i = 0 + str = "same".dup + while i < 5_000_000 + -(i.to_s.freeze) + i += 1 + end + fstring_same: | + i = 0 + str = "same".dup + while i < 10_000_000 + -str + i += 1 + end +loop_count: 1 diff --git a/benchmark/string_gsub.yml b/benchmark/string_gsub.yml new file mode 100644 index 0000000000..0f964337dd --- /dev/null +++ b/benchmark/string_gsub.yml @@ -0,0 +1,43 @@ +prelude: | + # frozen_string_literal: true + STR = ((("a" * 31) + "<") * 1000).freeze + STR_UNICODE = ((("a" * 30) + "\u2028") * 1000).freeze + ESCAPED_CHARS_BINARY = { + "\u2028".b => '\u2028'.b, + "\u2029".b => '\u2029'.b, + ">".b => '\u003e'.b.freeze, + "<".b => '\u003c'.b.freeze, + "&".b => '\u0026'.b.freeze, + } + BINARY_PATTERN = Regexp.union(ESCAPED_CHARS_BINARY.keys) + + ESCAPED_CHARS = { + "\u2028" => '\u2028', + "\u2029" => '\u2029', + ">" => '\u003e', + "<" => '\u003c', + "&" => '\u0026', + } + ESCAPE_PATTERN = Regexp.union(ESCAPED_CHARS.keys) + + +benchmark: + escape: | + str = STR.dup + str.gsub!(ESCAPE_PATTERN, ESCAPED_CHARS) + str + + escape_bin: | + str = STR.b + str.gsub!(BINARY_PATTERN, ESCAPED_CHARS_BINARY) + str.force_encoding(Encoding::UTF_8) + + escape_utf8: | + str = STR_UNICODE.dup + str.gsub!(ESCAPE_PATTERN, ESCAPED_CHARS) + str + + escape_utf8_bin: | + str = STR_UNICODE.b + str.gsub!(BINARY_PATTERN, ESCAPED_CHARS_BINARY) + str.force_encoding(Encoding::UTF_8) diff --git a/benchmark/string_rpartition.yml b/benchmark/string_rpartition.yml new file mode 100644 index 0000000000..37e9d1b071 --- /dev/null +++ b/benchmark/string_rpartition.yml @@ -0,0 +1,18 @@ +prelude: | + str1 = [*"a".."z",*"0".."9"].join("") + str10 = str1 * 10 + ":" + str100 = str1 * 100 + ":" + str1000 = str1 * 1000 + ":" + nonascii1 = [*"\u{e0}".."\u{ff}"].join("") + nonascii10 = nonascii1 * 10 + ":" + nonascii100 = nonascii1 * 100 + ":" + nonascii1000 = nonascii1 * 1000 + ":" +benchmark: + rpartition-1: str1.rpartition(":") + rpartition-10: str10.rpartition(":") + rpartition-100: str100.rpartition(":") + rpartition-1000: str1000.rpartition(":") + rpartition-nonascii1: nonascii1.rpartition(":") + rpartition-nonascii10: nonascii10.rpartition(":") + rpartition-nonascii100: nonascii100.rpartition(":") + rpartition-nonascii1000: nonascii1000.rpartition(":") diff --git a/benchmark/struct_accessor.yml b/benchmark/struct_accessor.yml new file mode 100644 index 0000000000..d95240e2dd --- /dev/null +++ b/benchmark/struct_accessor.yml @@ -0,0 +1,37 @@ +prelude: | + C = Struct.new(:x) do + def initialize(...) + super + @ivar = 42 + end + + attr_accessor :ivar + + class_eval <<-END + def r + #{'x;'*256} + end + def w + #{'self.x = nil;'*256} + end + def rm + m = method(:x) + #{'m.call;'*256} + end + def wm + m = method(:x=) + #{'m.call(nil);'*256} + end + def r_ivar + #{'ivar;'*256} + end + END + end + C.new(nil) # ensure common shape is known + obj = C.new(nil) +benchmark: + member_reader: "obj.r" + member_writer: "obj.w" + member_reader_method: "obj.rm" + member_writer_method: "obj.wm" + ivar_reader: "obj.r_ivar" diff --git a/benchmark/time_now.yml b/benchmark/time_now.yml new file mode 100644 index 0000000000..9336877cd4 --- /dev/null +++ b/benchmark/time_now.yml @@ -0,0 +1,4 @@ +benchmark: + - 'Time.now' + - 'Time.now(in: "+09:00")' + - 'Time.now.year' diff --git a/benchmark/time_parse.yml b/benchmark/time_parse.yml index a6d6948b9c..6060b58bc6 100644 --- a/benchmark/time_parse.yml +++ b/benchmark/time_parse.yml @@ -6,3 +6,5 @@ benchmark: - Time.iso8601(iso8601) - Time.parse(iso8601) - Time.parse(inspect) + - Time.new(iso8601) rescue Time.iso8601(iso8601) + - Time.new(inspect) rescue Time.parse(inspect) diff --git a/benchmark/time_strftime.yml b/benchmark/time_strftime.yml new file mode 100644 index 0000000000..28f62aec87 --- /dev/null +++ b/benchmark/time_strftime.yml @@ -0,0 +1,7 @@ +prelude: | + # frozen_string_literal: true + time = Time.now +benchmark: + - time.strftime("%FT%T") # 19B + - time.strftime("%FT%T.%3N") # 23B + - time.strftime("%FT%T.%6N") # 26B diff --git a/benchmark/time_xmlschema.yml b/benchmark/time_xmlschema.yml new file mode 100644 index 0000000000..654e5cfcbc --- /dev/null +++ b/benchmark/time_xmlschema.yml @@ -0,0 +1,27 @@ +prelude: | + # frozen_string_literal + unless Time.method_defined?(:xmlschema) + class Time + def xmlschema(fraction_digits=0) + fraction_digits = fraction_digits.to_i + s = strftime("%FT%T") + if fraction_digits > 0 + s << strftime(".%#{fraction_digits}N") + end + s << (utc? ? 'Z' : strftime("%:z")) + end + end + end + time = Time.now + utc_time = Time.now.utc + fraction_sec = Time.at(123456789.quo(9999999999)).getlocal("+09:00") + future_time = Time.utc(10000) +benchmark: + - time.xmlschema + - utc_time.xmlschema + - time.xmlschema(6) + - utc_time.xmlschema(6) + - time.xmlschema(9) + - utc_time.xmlschema(9) + - fraction_sec.xmlschema(10) + - future_time.xmlschema diff --git a/benchmark/vm_call_bmethod.yml b/benchmark/vm_call_bmethod.yml new file mode 100644 index 0000000000..40136e5aa4 --- /dev/null +++ b/benchmark/vm_call_bmethod.yml @@ -0,0 +1,37 @@ +prelude: | + define_method(:a0){} + define_method(:a1){|a| a} + define_method(:s){|*a| a} + define_method(:b){|kw: 1| kw} + + t0 = 0.times.to_a + t1 = 1.times.to_a + t10 = 10.times.to_a + t100 = 100.times.to_a + kw = {kw: 2} +benchmark: + bmethod_simple_0: | + a0 + bmethod_simple_1: | + a1(1) + bmethod_simple_0_splat: | + a0(*t0) + bmethod_simple_1_splat: | + a1(*t1) + bmethod_no_splat: | + s + bmethod_0_splat: | + s(*t0) + bmethod_1_splat: | + s(*t1) + bmethod_10_splat: | + s(*t10) + bmethod_100_splat: | + s(*t100) + bmethod_kw: | + b(kw: 1) + bmethod_no_kw: | + b + bmethod_kw_splat: | + b(**kw) +loop_count: 6000000 diff --git a/benchmark/vm_call_kw_and_kw_splat.yml b/benchmark/vm_call_kw_and_kw_splat.yml new file mode 100644 index 0000000000..aa6e549e0c --- /dev/null +++ b/benchmark/vm_call_kw_and_kw_splat.yml @@ -0,0 +1,25 @@ +prelude: | + h1, h10, h100, h1000 = [1, 10, 100, 1000].map do |n| + h = {kw: 1} + n.times{|i| h[i.to_s.to_sym] = i} + h + end + eh = {} + def kw(kw: nil, **kws) end +benchmark: + 1: | + kw(**h1) + 1_mutable: | + kw(**eh, **h1) + 10: | + kw(**h10) + 10_mutable: | + kw(**eh, **h10) + 100: | + kw(**h100) + 100_mutable: | + kw(**eh, **h100) + 1000: | + kw(**h1000) + 1000_mutable: | + kw(**eh, **h1000) diff --git a/benchmark/vm_call_method_missing.yml b/benchmark/vm_call_method_missing.yml new file mode 100644 index 0000000000..f890796f11 --- /dev/null +++ b/benchmark/vm_call_method_missing.yml @@ -0,0 +1,62 @@ +prelude: | + class A0 + def method_missing(m); m end + end + class A1 + def method_missing(m, a) a; end + end + class S + def method_missing(m, *a) a; end + end + class B + def method_missing(m, kw: 1) kw end + end + class SB + def method_missing(m, *a, kw: 1) kw end + end + + t0 = 0.times.to_a + t1 = 1.times.to_a + t10 = 10.times.to_a + t200 = 200.times.to_a + kw = {kw: 2} + + a0 = A0.new + a1 = A1.new + s = S.new + b = B.new + sb = SB.new +benchmark: + method_missing_simple_0: | + a0.() + method_missing_simple_1: | + a1.x(1) + method_missing_simple_0_splat: | + a0.(*t0) + method_missing_simple_1_splat: | + a1.(*t1) + method_missing_no_splat: | + s.() + method_missing_0_splat: | + s.(*t0) + method_missing_1_splat: | + s.(*t1) + method_missing_10_splat: | + s.(*t10) + method_missing_200_splat: | + s.(*t200) + method_missing_kw: | + b.(kw: 1) + method_missing_no_kw: | + b.() + method_missing_kw_splat: | + b.(**kw) + method_missing_0_splat_kw: | + sb.(*t0, **kw) + method_missing_1_splat_kw: | + sb.(*t1, **kw) + method_missing_10_splat_kw: | + sb.(*t10, **kw) + method_missing_200_splat_kw: | + sb.(*t200, **kw) +loop_count: 1000000 diff --git a/benchmark/vm_call_send_iseq.yml b/benchmark/vm_call_send_iseq.yml new file mode 100644 index 0000000000..60ff23c475 --- /dev/null +++ b/benchmark/vm_call_send_iseq.yml @@ -0,0 +1,77 @@ +prelude: | + def a0; end + def a1(a) a; end + def s(*a) a; end + def b(kw: 1) kw end + def sb(*a, kw: 1) kw end + + t0 = 0.times.to_a + t1 = 1.times.to_a + t10 = 10.times.to_a + t200 = 200.times.to_a + + a0_t0 = [:a0, *t0] + a1_t1 = [:a1, *t1] + s_t0 = [:s, *t0] + s_t1 = [:s, *t1] + s_t10 = [:s, *t10] + s_t200 = [:s, *t200] + sb_t0 = [:sb, *t0] + sb_t1 = [:sb, *t1] + sb_t10 = [:sb, *t10] + sb_t200 = [:sb, *t200] + kw = {kw: 2} +benchmark: + send_simple_0: | + send(:a0) + send_simple_1: | + send(:a1, 1) + send_simple_0_splat: | + send(:a0, *t0) + send_simple_1_splat: | + send(:a1, *t1) + send_simple_0_splat_comb: | + send(*a0_t0) + send_simple_1_splat_comb: | + send(*a1_t1) + send_no_splat: | + send(:s) + send_0_splat: | + send(:s, *t0) + send_1_splat: | + send(:s, *t1) + send_10_splat: | + send(:s, *t10) + send_200_splat: | + send(:s, *t200) + send_0_splat_comb: | + send(*s_t0) + send_1_splat_comb: | + send(*s_t1) + send_10_splat_comb: | + send(*s_t10) + send_200_splat_comb: | + send(*s_t200) + send_kw: | + send(:b, kw: 1) + send_no_kw: | + send(:b) + send_kw_splat: | + send(:b, **kw) + send_0_splat_kw: | + send(:sb, *t0, **kw) + send_1_splat_kw: | + send(:sb, *t1, **kw) + send_10_splat_kw: | + send(:sb, *t10, **kw) + send_200_splat_kw: | + send(:sb, *t200, **kw) + send_0_splat_comb_kw: | + send(*sb_t0, **kw) + send_1_splat_comb_kw: | + send(*sb_t1, **kw) + send_10_splat_comb_kw: | + send(*sb_t10, **kw) + send_200_splat_comb_kw: | + send(*sb_t200, **kw) +loop_count: 3000000 diff --git a/benchmark/vm_call_symproc.yml b/benchmark/vm_call_symproc.yml new file mode 100644 index 0000000000..16e0ac579e --- /dev/null +++ b/benchmark/vm_call_symproc.yml @@ -0,0 +1,83 @@ +prelude: | + def self.a0; end + def self.a1(a) a; end + def self.s(*a) a; end + def self.b(kw: 1) kw end + def self.sb(*a, kw: 1) kw end + + t0 = 0.times.to_a + t1 = 1.times.to_a + t10 = 10.times.to_a + t200 = 200.times.to_a + + a0_t0 = [self, *t0] + a1_t1 = [self, *t1] + s_t0 = [self, *t0] + s_t1 = [self, *t1] + s_t10 = [self, *t10] + s_t200 = [self, *t200] + sb_t0 = [self, *t0] + sb_t1 = [self, *t1] + sb_t10 = [self, *t10] + sb_t200 = [self, *t200] + kw = {kw: 2} + + a0 = :a0.to_proc + a1 = :a1.to_proc + s = :s.to_proc + b = :b.to_proc + sb = :sb.to_proc +benchmark: + symproc_simple_0: | + a0.(self) + symproc_simple_1: | + a1.(self, 1) + symproc_simple_0_splat: | + a0.(self, *t0) + symproc_simple_1_splat: | + a1.(self, *t1) + symproc_simple_0_splat_comb: | + a0.(*a0_t0) + symproc_simple_1_splat_comb: | + a1.(*a1_t1) + symproc_no_splat: | + s.(self) + symproc_0_splat: | + s.(self, *t0) + symproc_1_splat: | + s.(self, *t1) + symproc_10_splat: | + s.(self, *t10) + symproc_200_splat: | + s.(self, *t200) + symproc_0_splat_comb: | + s.(*s_t0) + symproc_1_splat_comb: | + s.(*s_t1) + symproc_10_splat_comb: | + s.(*s_t10) + symproc_200_splat_comb: | + s.(*s_t200) + symproc_kw: | + b.(self, kw: 1) + symproc_no_kw: | + b.(self) + symproc_kw_splat: | + b.(self, **kw) + symproc_0_splat_kw: | + sb.(self, *t0, **kw) + symproc_1_splat_kw: | + sb.(self, *t1, **kw) + symproc_10_splat_kw: | + sb.(self, *t10, **kw) + symproc_200_splat_kw: | + sb.(self, *t200, **kw) + symproc_0_splat_comb_kw: | + sb.(*sb_t0, **kw) + symproc_1_splat_comb_kw: | + sb.(*sb_t1, **kw) + symproc_10_splat_comb_kw: | + sb.(*sb_t10, **kw) + symproc_200_splat_comb_kw: | + sb.(*sb_t200, **kw) +loop_count: 1000000 diff --git a/benchmark/vm_const.yml b/benchmark/vm_const.yml index 6064d4eed0..8939ca0cd3 100644 --- a/benchmark/vm_const.yml +++ b/benchmark/vm_const.yml @@ -1,7 +1,13 @@ prelude: | Const = 1 + A = B = C = D = E = F = G = H = I = J = K = L = M = N = O = P = Q = R = S = T = U = V = W = X = Y = Z = 1 + def foo + A; B; C; D; E; F; G; H; I; J; K; L; M; N; O; P; Q; R; S; T; U; V; W; X; Y; Z + end benchmark: vm_const: | j = Const k = Const + vm_const_many: | + foo loop_count: 30000000 diff --git a/benchmark/vm_freezeobj.yml b/benchmark/vm_freezeobj.yml new file mode 100644 index 0000000000..69a795a354 --- /dev/null +++ b/benchmark/vm_freezeobj.yml @@ -0,0 +1,6 @@ +prelude: | + objs = 100000.times.map { Object.new } +benchmark: + vm_freeze_obj: | + objs.map(&:freeze) +loop_count: 600 diff --git a/benchmark/vm_ivar_embedded_obj_init.yml b/benchmark/vm_ivar_embedded_obj_init.yml new file mode 100644 index 0000000000..74fe20a630 --- /dev/null +++ b/benchmark/vm_ivar_embedded_obj_init.yml @@ -0,0 +1,14 @@ +prelude: | + class C + def set_ivars + @a = nil + @b = nil + @c = nil + end + end + + c = C.new +benchmark: + vm_ivar_embedded_obj_init: | + c.set_ivars +loop_count: 30000000 diff --git a/benchmark/vm_ivar_extended_obj_init.yml b/benchmark/vm_ivar_extended_obj_init.yml new file mode 100644 index 0000000000..f054bab282 --- /dev/null +++ b/benchmark/vm_ivar_extended_obj_init.yml @@ -0,0 +1,16 @@ +prelude: | + class C + def set_ivars + @a = nil + @b = nil + @c = nil + @d = nil + @e = nil + end + end + + c = C.new +benchmark: + vm_ivar_extended_obj_init: | + c.set_ivars +loop_count: 30000000 diff --git a/benchmark/vm_ivar_generic_get.yml b/benchmark/vm_ivar_generic_get.yml new file mode 100644 index 0000000000..dae2d37671 --- /dev/null +++ b/benchmark/vm_ivar_generic_get.yml @@ -0,0 +1,17 @@ +prelude: | + class C < Array + attr_reader :a, :b, :c + def initialize + @a = nil + @b = nil + @c = nil + end + end + + c = C.new +benchmark: + vm_ivar_generic_get: | + c.a + c.b + c.c +loop_count: 30000000 diff --git a/benchmark/vm_ivar_generic_set.yml b/benchmark/vm_ivar_generic_set.yml new file mode 100644 index 0000000000..102a6577fb --- /dev/null +++ b/benchmark/vm_ivar_generic_set.yml @@ -0,0 +1,14 @@ +prelude: | + class C < Array + def set_ivars + @a = nil + @b = nil + @c = nil + end + end + + c = C.new +benchmark: + vm_ivar_generic_set: | + c.set_ivars +loop_count: 30000000 diff --git a/benchmark/vm_ivar_get.yml b/benchmark/vm_ivar_get.yml new file mode 100644 index 0000000000..1e0dad665f --- /dev/null +++ b/benchmark/vm_ivar_get.yml @@ -0,0 +1,100 @@ +prelude: | + class Example + def initialize + @levar = 1 + @v0 = 1 + @v1 = 2 + @v3 = 3 + end + + def get_value_loop + sum = 0 + + i = 0 + while i < 100_000 + # 10 times to de-emphasize loop overhead + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + i += 1 + end + + return sum + end + + @levar = 1 + @v0 = 1 + @v1 = 2 + @v3 = 3 + + def self.get_value_loop + sum = 0 + + i = 0 + while i < 100_000 + # 10 times to de-emphasize loop overhead + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + i += 1 + end + + return sum + end + end + + class GenExample < Time + def initialize + @levar = 1 + @v0 = 1 + @v1 = 2 + @v3 = 3 + end + + def get_value_loop + sum = 0 + + i = 0 + while i < 100_000 + # 10 times to de-emphasize loop overhead + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + sum += @levar + i += 1 + end + + return sum + end + end + + obj = Example.new + gen = GenExample.new +benchmark: + vm_ivar_get_on_obj: | + obj.get_value_loop + vm_ivar_get_on_class: | + Example.get_value_loop + vm_ivar_get_on_generic: | + gen.get_value_loop +loop_count: 100 diff --git a/benchmark/vm_ivar_get_unintialized.yml b/benchmark/vm_ivar_get_unintialized.yml new file mode 100644 index 0000000000..a1ccfb06ce --- /dev/null +++ b/benchmark/vm_ivar_get_unintialized.yml @@ -0,0 +1,12 @@ +prelude: | + class Example + def read + @uninitialized + end + end + + obj = Example.new +benchmark: + vm_ivar_get_uninitialized: | + obj.read +loop_count: 30000000 diff --git a/benchmark/vm_ivar_ic_miss.yml b/benchmark/vm_ivar_ic_miss.yml new file mode 100644 index 0000000000..944fb1a9e6 --- /dev/null +++ b/benchmark/vm_ivar_ic_miss.yml @@ -0,0 +1,20 @@ +prelude: | + class Foo + def initialize diverge + if diverge + @a = 1 + end + + @a0 = @a1 = @a2 = @a3 = @a4 = @a5 = @a6 = @a7 = @a8 = @a9 = @a10 = @a11 = @a12 = @a13 = @a14 = @a15 = @a16 = @a17 = @a18 = @a19 = @a20 = @a21 = @a22 = @a23 = @a24 = @a25 = @a26 = @a27 = @a28 = @a29 = @a30 = @a31 = @a32 = @a33 = @a34 = @a35 = @a36 = @a37 = @a38 = @a39 = @a40 = @a41 = @a42 = @a43 = @a44 = @a45 = @a46 = @a47 = @a48 = @a49 = @a50 = @a51 = @a52 = @a53 = @a54 = @a55 = @a56 = @a57 = @a58 = @a59 = @a60 = @a61 = @a62 = @a63 = @a64 = @a65 = @a66 = @a67 = @a68 = @a69 = @a70 = @a71 = @a72 = @a73 = @a74 = @b = 1 + end + + def b; @b; end + end + + a = Foo.new false + b = Foo.new true +benchmark: + vm_ivar_ic_miss: | + a.b + b.b +loop_count: 30000000 diff --git a/benchmark/vm_ivar_init.yml b/benchmark/vm_ivar_init.yml deleted file mode 100644 index c6f1633907..0000000000 --- a/benchmark/vm_ivar_init.yml +++ /dev/null @@ -1,14 +0,0 @@ -prelude: | - class C - def initialize - @a = nil - @b = nil - @c = nil - @d = nil - @e = nil - end - end -benchmark: - vm_ivar_init: | - C.new -loop_count: 30000000 diff --git a/benchmark/vm_ivar_lazy_set.yml b/benchmark/vm_ivar_lazy_set.yml new file mode 100644 index 0000000000..7372ffcfbc --- /dev/null +++ b/benchmark/vm_ivar_lazy_set.yml @@ -0,0 +1,12 @@ +prelude: | + class Example + def lazy_set + @uninitialized ||= 123 + end + end + + objs = 10000000.times.map { Example.new } +benchmark: + vm_ivar_lazy_set: | + objs.each(&:lazy_set) +loop_count: 1 diff --git a/benchmark/vm_ivar_memoize.yml b/benchmark/vm_ivar_memoize.yml new file mode 100644 index 0000000000..90f6b07f05 --- /dev/null +++ b/benchmark/vm_ivar_memoize.yml @@ -0,0 +1,85 @@ +prelude: | + IVARS = 60 + class Record + def initialize(offset = false) + @offset = 1 if offset + @first = 0 + IVARS.times do |i| + instance_variable_set("@ivar_#{i}", i) + end + end + + def first + @first + end + + def lazy_set + @lazy_set ||= 123 + end + + def undef + @undef + end + end + + Record.new # Need one alloc to right size + + BASE = Record.new + LAZY = Record.new + LAZY.lazy_set + + class Miss < Record + @first = 0 + IVARS.times do |i| + instance_variable_set("@i_#{i}", i) + end + end + + Miss.new # Need one alloc to right size + MISS = Miss.new + + DIVERGENT = Record.new(true) + +benchmark: + vm_ivar_stable_shape: | + BASE.first + BASE.first + BASE.first + BASE.first + BASE.first + BASE.first + vm_ivar_memoize_unstable_shape: | + BASE.first + LAZY.first + BASE.first + LAZY.first + BASE.first + LAZY.first + vm_ivar_memoize_unstable_shape_miss: | + BASE.first + MISS.first + BASE.first + MISS.first + BASE.first + MISS.first + vm_ivar_unstable_undef: | + BASE.undef + LAZY.undef + BASE.undef + LAZY.undef + BASE.undef + LAZY.undef + vm_ivar_divergent_shape: | + BASE.first + DIVERGENT.first + BASE.first + DIVERGENT.first + BASE.first + DIVERGENT.first + vm_ivar_divergent_shape_imbalanced: | + BASE.first + DIVERGENT.first + DIVERGENT.first + DIVERGENT.first + DIVERGENT.first + DIVERGENT.first diff --git a/benchmark/vm_ivar_set_on_instance.yml b/benchmark/vm_ivar_set_on_instance.yml new file mode 100644 index 0000000000..6ce53a86ec --- /dev/null +++ b/benchmark/vm_ivar_set_on_instance.yml @@ -0,0 +1,94 @@ +prelude: | + class TheClass + def initialize + @levar = 1 + @v0 = 1 + @v1 = 2 + @v3 = 3 + end + + def set_value_loop + # 100k + i = 0 + while i < 100_000 + # 10 times to de-emphasize loop overhead + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + i += 1 + end + end + end + + class Generic < Time + def initialize + @levar = 1 + @v0 = 1 + @v1 = 2 + @v3 = 3 + end + + def set_value_loop + # 100k + i = 0 + while i < 100_000 + # 10 times to de-emphasize loop overhead + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + i += 1 + end + end + end + + obj = TheClass.new + gen_obj = Generic.new + + class SomeClass + @levar = 1 + @v0 = 1 + @v1 = 2 + @v3 = 3 + + def self.set_value_loop + # 100k + i = 0 + while i < 100_000 + # 10 times to de-emphasize loop overhead + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + @levar = i + i += 1 + end + end + end + +benchmark: + vm_ivar_set_on_instance: | + obj.set_value_loop + vm_ivar_set_on_generic: | + gen_obj.set_value_loop + vm_ivar_set_on_class: | + SomeClass.set_value_loop +loop_count: 100 diff --git a/benchmark/vm_ivar_set_subclass.yml b/benchmark/vm_ivar_set_subclass.yml index 2653d36ded..bc8bf5bf6b 100644 --- a/benchmark/vm_ivar_set_subclass.yml +++ b/benchmark/vm_ivar_set_subclass.yml @@ -1,6 +1,6 @@ prelude: | class A - def initialize + def set_ivars @a = nil @b = nil @c = nil @@ -10,8 +10,11 @@ prelude: | end class B < A; end class C < A; end + + b = B.new + c = C.new benchmark: vm_ivar_init_subclass: | - B.new - C.new + b.set_ivars + c.set_ivars loop_count: 3000000 diff --git a/benchmark/vm_lvar_cond_set.yml b/benchmark/vm_lvar_cond_set.yml new file mode 100644 index 0000000000..1845f9d12e --- /dev/null +++ b/benchmark/vm_lvar_cond_set.yml @@ -0,0 +1,8 @@ +benchmark: + vm_lvar_cond_set: | + a ||= 1 + b ||= 1 + c ||= 1 + d ||= 1 + nil +loop_count: 30000000 diff --git a/benchmark/vm_method_splat_calls.yml b/benchmark/vm_method_splat_calls.yml new file mode 100644 index 0000000000..f2f366e99c --- /dev/null +++ b/benchmark/vm_method_splat_calls.yml @@ -0,0 +1,13 @@ +prelude: | + def f(x=0, y: 0) end + a = [1] + ea = [] + kw = {y: 1} + b = lambda{} +benchmark: + arg_splat: "f(1, *ea)" + arg_splat_block: "f(1, *ea, &b)" + splat_kw_splat: "f(*a, **kw)" + splat_kw_splat_block: "f(*a, **kw, &b)" + splat_kw: "f(*a, y: 1)" + splat_kw_block: "f(*a, y: 1, &b)" diff --git a/benchmark/vm_method_splat_calls2.yml b/benchmark/vm_method_splat_calls2.yml new file mode 100644 index 0000000000..d33dcd7e8b --- /dev/null +++ b/benchmark/vm_method_splat_calls2.yml @@ -0,0 +1,27 @@ +prelude: | + def named_arg_splat(*a) end + def named_arg_kw_splat(*a, **kw) end + def anon_arg_splat(*) end + def anon_kw_splat(**) end + def anon_arg_kw_splat(*, **) end + def anon_fw_to_named(*, **) named_arg_kw_splat(*, **) end + def fw_to_named(...) named_arg_kw_splat(...) end + def fw_to_anon_to_named(...) anon_fw_to_named(...) end + def fw_no_kw(...) named_arg_splat(...) end + a = [1] + kw = {y: 1} +benchmark: + named_multi_arg_splat: "named_arg_splat(*a, *a)" + named_post_splat: "named_arg_splat(*a, a)" + anon_arg_splat: "anon_arg_splat(*a)" + anon_arg_kw_splat: "anon_arg_kw_splat(*a, **kw)" + anon_multi_arg_splat: "anon_arg_splat(*a, *a)" + anon_post_splat: "anon_arg_splat(*a, a)" + anon_kw_splat: "anon_kw_splat(**kw)" + anon_fw_to_named_splat: "anon_fw_to_named(*a, **kw)" + anon_fw_to_named_no_splat: "anon_fw_to_named(1, y: 1)" + fw_to_named_splat: "fw_to_named(*a, **kw)" + fw_to_named_no_splat: "fw_to_named(1, y: 1)" + fw_to_anon_to_named_splat: "fw_to_anon_to_named(*a, **kw)" + fw_to_anon_to_named_no_splat: "fw_to_anon_to_named(1, y: 1)" + fw_no_kw: "fw_no_kw(1, 2)" diff --git a/benchmark/vm_send_cfunc.yml b/benchmark/vm_send_cfunc.yml index b114ac317d..6f12b65176 100644 --- a/benchmark/vm_send_cfunc.yml +++ b/benchmark/vm_send_cfunc.yml @@ -1,3 +1,14 @@ +prelude: | + ary = [] + kw = {a: 1} + empty_kw = {} + kw_ary = [Hash.ruby2_keywords_hash(a: 1)] + empty_kw_ary = [Hash.ruby2_keywords_hash({})] benchmark: - vm_send_cfunc: self.class -loop_count: 100000000 + vm_send_cfunc: itself + vm_send_cfunc_splat: itself(*ary) + vm_send_cfunc_splat_kw_hash: equal?(*kw_ary) + vm_send_cfunc_splat_empty_kw_hash: itself(*empty_kw_ary) + vm_send_cfunc_splat_kw: equal?(*ary, **kw) + vm_send_cfunc_splat_empty_kw: itself(*ary, **empty_kw) +loop_count: 20000000 diff --git a/benchmark/vm_super_splat_calls.yml b/benchmark/vm_super_splat_calls.yml new file mode 100644 index 0000000000..795e44e4da --- /dev/null +++ b/benchmark/vm_super_splat_calls.yml @@ -0,0 +1,25 @@ +prelude: | + @a = [1].freeze + @ea = [].freeze + @kw = {y: 1}.freeze + @b = lambda{} + extend(Module.new{def arg_splat(x=0, y: 0) end}) + extend(Module.new{def arg_splat_block(x=0, y: 0) end}) + extend(Module.new{def splat_kw_splat(x=0, y: 0) end}) + extend(Module.new{def splat_kw_splat_block(x=0, y: 0) end}) + extend(Module.new{def splat_kw(x=0, y: 0) end}) + extend(Module.new{def splat_kw_block(x=0, y: 0) end}) + + extend(Module.new{def arg_splat; super(1, *@ea) end}) + extend(Module.new{def arg_splat_block; super(1, *@ea, &@b) end}) + extend(Module.new{def splat_kw_splat; super(*@a, **@kw) end}) + extend(Module.new{def splat_kw_splat_block; super(*@a, **@kw, &@b) end}) + extend(Module.new{def splat_kw; super(*@a, y: 1) end}) + extend(Module.new{def splat_kw_block; super(*@a, y: 1, &@b) end}) +benchmark: + arg_splat: "arg_splat" + arg_splat_block: "arg_splat_block" + splat_kw_splat: "splat_kw_splat" + splat_kw_splat_block: "splat_kw_splat_block" + splat_kw: "splat_kw" + splat_kw_block: "splat_kw_block" diff --git a/benchmark/vm_zsuper_splat_calls.yml b/benchmark/vm_zsuper_splat_calls.yml new file mode 100644 index 0000000000..82dc22349d --- /dev/null +++ b/benchmark/vm_zsuper_splat_calls.yml @@ -0,0 +1,28 @@ +prelude: | + a = [1].freeze + ea = [].freeze + kw = {y: 1}.freeze + b = lambda{} + extend(Module.new{def arg_splat(x=0, y: 0) end}) + extend(Module.new{def arg_splat_block(x=0, y: 0) end}) + extend(Module.new{def arg_splat_post(x=0, y: 0) end}) + extend(Module.new{def splat_kw_splat(x=0, y: 0) end}) + extend(Module.new{def splat_kw_splat_block(x=0, y: 0) end}) + extend(Module.new{def splat_kw(x=0, y: 0) end}) + extend(Module.new{def splat_kw_block(x=0, y: 0) end}) + + extend(Module.new{def arg_splat(x, *a) super end}) + extend(Module.new{def arg_splat_block(x, *a, &b) super end}) + extend(Module.new{def arg_splat_post(*a, x) super end}) + extend(Module.new{def splat_kw_splat(*a, **kw) super end}) + extend(Module.new{def splat_kw_splat_block(*a, **kw, &b) super end}) + extend(Module.new{def splat_kw(*a, y: 1) super end}) + extend(Module.new{def splat_kw_block(*a, y: 1, &b) super end}) +benchmark: + arg_splat: "arg_splat(1, *ea)" + arg_splat_block: "arg_splat_block(1, *ea, &b)" + arg_splat_post: "arg_splat_post(1, *ea, &b)" + splat_kw_splat: "splat_kw_splat(*a, **kw)" + splat_kw_splat_block: "splat_kw_splat_block(*a, **kw, &b)" + splat_kw: "splat_kw(*a, y: 1)" + splat_kw_block: "splat_kw_block(*a, y: 1, &b)" @@ -23,9 +23,12 @@ # include <ieeefp.h> #endif +#if !defined(USE_GMP) #if defined(HAVE_LIBGMP) && defined(HAVE_GMP_H) -# define USE_GMP -# include <gmp.h> +# define USE_GMP 1 +#else +# define USE_GMP 0 +#endif #endif #include "id.h" @@ -42,6 +45,23 @@ #include "ruby/util.h" #include "ruby_assert.h" +#if USE_GMP +RBIMPL_WARNING_PUSH() +# ifdef _MSC_VER +RBIMPL_WARNING_IGNORED(4146) /* for mpn_neg() */ +# endif +# include <gmp.h> +RBIMPL_WARNING_POP() +#endif + +static const bool debug_integer_pack = ( +#ifdef DEBUG_INTEGER_PACK + DEBUG_INTEGER_PACK+0 +#else + RUBY_DEBUG +#endif + ) != 0; + const char ruby_digitmap[] = "0123456789abcdefghijklmnopqrstuvwxyz"; #ifndef SIZEOF_BDIGIT_DBL @@ -59,7 +79,6 @@ STATIC_ASSERT(sizeof_bdigit_and_dbl, SIZEOF_BDIGIT*2 <= SIZEOF_BDIGIT_DBL); STATIC_ASSERT(bdigit_signedness, 0 < (BDIGIT)-1); STATIC_ASSERT(bdigit_dbl_signedness, 0 < (BDIGIT_DBL)-1); STATIC_ASSERT(bdigit_dbl_signed_signedness, 0 > (BDIGIT_DBL_SIGNED)-1); -STATIC_ASSERT(rbignum_embed_len_max, BIGNUM_EMBED_LEN_MAX <= (BIGNUM_EMBED_LEN_MASK >> BIGNUM_EMBED_LEN_SHIFT)); #if SIZEOF_BDIGIT < SIZEOF_LONG STATIC_ASSERT(sizeof_long_and_sizeof_bdigit, SIZEOF_LONG % SIZEOF_BDIGIT == 0); @@ -99,8 +118,8 @@ STATIC_ASSERT(sizeof_long_and_sizeof_bdigit, SIZEOF_BDIGIT % SIZEOF_LONG == 0); #endif #define BIGZEROP(x) (BIGNUM_LEN(x) == 0 || \ - (BDIGITS(x)[0] == 0 && \ - (BIGNUM_LEN(x) == 1 || bigzero_p(x)))) + (BDIGITS(x)[0] == 0 && \ + (BIGNUM_LEN(x) == 1 || bigzero_p(x)))) #define BIGSIZE(x) (BIGNUM_LEN(x) == 0 ? (size_t)0 : \ BDIGITS(x)[BIGNUM_LEN(x)-1] ? \ (size_t)(BIGNUM_LEN(x)*SIZEOF_BDIGIT - nlz(BDIGITS(x)[BIGNUM_LEN(x)-1])/CHAR_BIT) : \ @@ -145,7 +164,7 @@ STATIC_ASSERT(sizeof_long_and_sizeof_bdigit, SIZEOF_BDIGIT % SIZEOF_LONG == 0); #define GMP_DIV_DIGITS 20 #define GMP_BIG2STR_DIGITS 20 #define GMP_STR2BIG_DIGITS 20 -#ifdef USE_GMP +#if USE_GMP # define NAIVE_MUL_DIGITS GMP_MUL_DIGITS #else # define NAIVE_MUL_DIGITS KARATSUBA_MUL_DIGITS @@ -335,7 +354,7 @@ maxpow_in_bdigit_dbl(int base, int *exp_ret) BDIGIT_DBL maxpow; int exponent; - assert(2 <= base && base <= 36); + RUBY_ASSERT(2 <= base && base <= 36); { #if SIZEOF_BDIGIT_DBL == 2 @@ -367,7 +386,7 @@ maxpow_in_bdigit_dbl(int base, int *exp_ret) static inline BDIGIT_DBL bary2bdigitdbl(const BDIGIT *ds, size_t n) { - assert(n <= 2); + RUBY_ASSERT(n <= 2); if (n == 2) return ds[0] | BIGUP(ds[1]); @@ -379,7 +398,7 @@ bary2bdigitdbl(const BDIGIT *ds, size_t n) static inline void bdigitdbl2bary(BDIGIT *ds, size_t n, BDIGIT_DBL num) { - assert(n == 2); + RUBY_ASSERT(n == 2); ds[0] = BIGLO(num); ds[1] = (BDIGIT)BIGDN(num); @@ -410,12 +429,12 @@ bary_small_lshift(BDIGIT *zds, const BDIGIT *xds, size_t n, int shift) { size_t i; BDIGIT_DBL num = 0; - assert(0 <= shift && shift < BITSPERDIG); + RUBY_ASSERT(0 <= shift && shift < BITSPERDIG); for (i=0; i<n; i++) { - num = num | (BDIGIT_DBL)*xds++ << shift; - *zds++ = BIGLO(num); - num = BIGDN(num); + num = num | (BDIGIT_DBL)*xds++ << shift; + *zds++ = BIGLO(num); + num = BIGDN(num); } return BIGLO(num); } @@ -426,14 +445,14 @@ bary_small_rshift(BDIGIT *zds, const BDIGIT *xds, size_t n, int shift, BDIGIT hi size_t i; BDIGIT_DBL num = 0; - assert(0 <= shift && shift < BITSPERDIG); + RUBY_ASSERT(0 <= shift && shift < BITSPERDIG); num = BIGUP(higher_bdigit); for (i = 0; i < n; i++) { BDIGIT x = xds[n - i - 1]; - num = (num | x) >> shift; + num = (num | x) >> shift; zds[n - i - 1] = BIGLO(num); - num = BIGUP(x); + num = BIGUP(x); } } @@ -443,7 +462,7 @@ bary_zero_p(const BDIGIT *xds, size_t xn) if (xn == 0) return 1; do { - if (xds[--xn]) return 0; + if (xds[--xn]) return 0; } while (xn); return 1; } @@ -971,7 +990,7 @@ integer_unpack_num_bdigits_small(size_t numwords, size_t wordsize, size_t nails, { /* nlp_bits stands for number of leading padding bits */ size_t num_bits = (wordsize * CHAR_BIT - nails) * numwords; - size_t num_bdigits = (num_bits + BITSPERDIG - 1) / BITSPERDIG; + size_t num_bdigits = roomof(num_bits, BITSPERDIG); *nlp_bits_ret = (int)(num_bdigits * BITSPERDIG - num_bits); return num_bdigits; } @@ -981,7 +1000,7 @@ integer_unpack_num_bdigits_generic(size_t numwords, size_t wordsize, size_t nail { /* BITSPERDIG = SIZEOF_BDIGIT * CHAR_BIT */ /* num_bits = (wordsize * CHAR_BIT - nails) * numwords */ - /* num_bdigits = (num_bits + BITSPERDIG - 1) / BITSPERDIG */ + /* num_bdigits = roomof(num_bits, BITSPERDIG) */ /* num_bits = CHAR_BIT * (wordsize * numwords) - nails * numwords = CHAR_BIT * num_bytes1 - nails * numwords */ size_t num_bytes1 = wordsize * numwords; @@ -1043,15 +1062,13 @@ integer_unpack_num_bdigits(size_t numwords, size_t wordsize, size_t nails, int * if (numwords <= (SIZE_MAX - (BITSPERDIG-1)) / CHAR_BIT / wordsize) { num_bdigits = integer_unpack_num_bdigits_small(numwords, wordsize, nails, nlp_bits_ret); -#ifdef DEBUG_INTEGER_PACK - { + if (debug_integer_pack) { int nlp_bits1; size_t num_bdigits1 = integer_unpack_num_bdigits_generic(numwords, wordsize, nails, &nlp_bits1); - assert(num_bdigits == num_bdigits1); - assert(*nlp_bits_ret == nlp_bits1); + RUBY_ASSERT(num_bdigits == num_bdigits1); + RUBY_ASSERT(*nlp_bits_ret == nlp_bits1); (void)num_bdigits1; } -#endif } else { num_bdigits = integer_unpack_num_bdigits_generic(numwords, wordsize, nails, nlp_bits_ret); @@ -1259,7 +1276,7 @@ bary_unpack_internal(BDIGIT *bdigits, size_t num_bdigits, const void *words, siz } if (dd) *dp++ = (BDIGIT)dd; - assert(dp <= de); + RUBY_ASSERT(dp <= de); while (dp < de) *dp++ = 0; #undef PUSH_BITS @@ -1318,7 +1335,7 @@ bary_unpack(BDIGIT *bdigits, size_t num_bdigits, const void *words, size_t numwo num_bdigits0 = integer_unpack_num_bdigits(numwords, wordsize, nails, &nlp_bits); - assert(num_bdigits0 <= num_bdigits); + RUBY_ASSERT(num_bdigits0 <= num_bdigits); sign = bary_unpack_internal(bdigits, num_bdigits0, words, numwords, wordsize, nails, flags, nlp_bits); @@ -1337,16 +1354,16 @@ bary_subb(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGIT *yd size_t i; size_t sn; - assert(xn <= zn); - assert(yn <= zn); + RUBY_ASSERT(xn <= zn); + RUBY_ASSERT(yn <= zn); sn = xn < yn ? xn : yn; num = borrow ? -1 : 0; for (i = 0; i < sn; i++) { - num += (BDIGIT_DBL_SIGNED)xds[i] - yds[i]; - zds[i] = BIGLO(num); - num = BIGDN(num); + num += (BDIGIT_DBL_SIGNED)xds[i] - yds[i]; + zds[i] = BIGLO(num); + num = BIGDN(num); } if (yn <= xn) { for (; i < xn; i++) { @@ -1365,7 +1382,7 @@ bary_subb(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGIT *yd } if (num == 0) goto num_is_zero; for (; i < zn; i++) { - zds[i] = BDIGMAX; + zds[i] = BDIGMAX; } return 1; @@ -1373,10 +1390,10 @@ bary_subb(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGIT *yd if (xds == zds && xn == zn) return 0; for (; i < xn; i++) { - zds[i] = xds[i]; + zds[i] = xds[i]; } for (; i < zn; i++) { - zds[i] = 0; + zds[i] = 0; } return 0; } @@ -1399,31 +1416,31 @@ bary_addc(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGIT *yd BDIGIT_DBL num; size_t i; - assert(xn <= zn); - assert(yn <= zn); + RUBY_ASSERT(xn <= zn); + RUBY_ASSERT(yn <= zn); if (xn > yn) { - const BDIGIT *tds; - tds = xds; xds = yds; yds = tds; - i = xn; xn = yn; yn = i; + const BDIGIT *tds; + tds = xds; xds = yds; yds = tds; + i = xn; xn = yn; yn = i; } num = carry ? 1 : 0; for (i = 0; i < xn; i++) { - num += (BDIGIT_DBL)xds[i] + yds[i]; - zds[i] = BIGLO(num); - num = BIGDN(num); + num += (BDIGIT_DBL)xds[i] + yds[i]; + zds[i] = BIGLO(num); + num = BIGDN(num); } for (; i < yn; i++) { if (num == 0) goto num_is_zero; - num += yds[i]; - zds[i] = BIGLO(num); - num = BIGDN(num); + num += yds[i]; + zds[i] = BIGLO(num); + num = BIGDN(num); } for (; i < zn; i++) { if (num == 0) goto num_is_zero; - zds[i] = BIGLO(num); - num = BIGDN(num); + zds[i] = BIGLO(num); + num = BIGDN(num); } return num != 0; @@ -1431,10 +1448,10 @@ bary_addc(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGIT *yd if (yds == zds && yn == zn) return 0; for (; i < yn; i++) { - zds[i] = yds[i]; + zds[i] = yds[i]; } for (; i < zn; i++) { - zds[i] = 0; + zds[i] = 0; } return 0; } @@ -1464,7 +1481,7 @@ bary_mul_single(BDIGIT *zds, size_t zn, BDIGIT x, BDIGIT y) { BDIGIT_DBL n; - assert(2 <= zn); + RUBY_ASSERT(2 <= zn); n = (BDIGIT_DBL)x * y; bdigitdbl2bary(zds, 2, n); @@ -1478,7 +1495,7 @@ bary_muladd_1xN(BDIGIT *zds, size_t zn, BDIGIT x, const BDIGIT *yds, size_t yn) BDIGIT_DBL dd; size_t j; - assert(zn > yn); + RUBY_ASSERT(zn > yn); if (x == 0) return 0; @@ -1513,7 +1530,7 @@ bigdivrem_mulsub(BDIGIT *zds, size_t zn, BDIGIT x, const BDIGIT *yds, size_t yn) BDIGIT_DBL t2; BDIGIT_DBL_SIGNED num; - assert(zn == yn + 1); + RUBY_ASSERT(zn == yn + 1); num = 0; t2 = 0; @@ -1538,7 +1555,7 @@ bary_mulsub_1xN(BDIGIT *zds, size_t zn, BDIGIT x, const BDIGIT *yds, size_t yn) { BDIGIT_DBL_SIGNED num; - assert(zn == yn + 1); + RUBY_ASSERT(zn == yn + 1); num = bigdivrem_mulsub(zds, zn, x, yds, yn); zds[yn] = BIGLO(num); @@ -1552,7 +1569,7 @@ bary_mul_normal(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIG { size_t i; - assert(xn + yn <= zn); + RUBY_ASSERT(xn + yn <= zn); BDIGITS_ZERO(zds, zn); for (i = 0; i < xn; i++) { @@ -1573,7 +1590,7 @@ rb_big_mul_normal(VALUE x, VALUE y) /* efficient squaring (2 times faster than normal multiplication) * ref: Handbook of Applied Cryptography, Algorithm 14.16 - * http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf + * https://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf */ static void bary_sq_fast(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn) @@ -1583,7 +1600,7 @@ bary_sq_fast(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn) BDIGIT vl; int vh; - assert(xn * 2 <= zn); + RUBY_ASSERT(xn * 2 <= zn); BDIGITS_ZERO(zds, zn); @@ -1591,30 +1608,30 @@ bary_sq_fast(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn) return; for (i = 0; i < xn-1; i++) { - v = (BDIGIT_DBL)xds[i]; - if (!v) + v = (BDIGIT_DBL)xds[i]; + if (!v) continue; - c = (BDIGIT_DBL)zds[i + i] + v * v; - zds[i + i] = BIGLO(c); - c = BIGDN(c); - v *= 2; + c = (BDIGIT_DBL)zds[i + i] + v * v; + zds[i + i] = BIGLO(c); + c = BIGDN(c); + v *= 2; vl = BIGLO(v); vh = (int)BIGDN(v); - for (j = i + 1; j < xn; j++) { - w = (BDIGIT_DBL)xds[j]; - c += (BDIGIT_DBL)zds[i + j] + vl * w; - zds[i + j] = BIGLO(c); - c = BIGDN(c); - if (vh) + for (j = i + 1; j < xn; j++) { + w = (BDIGIT_DBL)xds[j]; + c += (BDIGIT_DBL)zds[i + j] + vl * w; + zds[i + j] = BIGLO(c); + c = BIGDN(c); + if (vh) c += w; - } - if (c) { - c += (BDIGIT_DBL)zds[i + xn]; - zds[i + xn] = BIGLO(c); - c = BIGDN(c); + } + if (c) { + c += (BDIGIT_DBL)zds[i + xn]; + zds[i + xn] = BIGLO(c); + c = BIGDN(c); if (c) zds[i + xn + 1] += (BDIGIT)c; - } + } } /* i == xn-1 */ @@ -1639,6 +1656,12 @@ rb_big_sq_fast(VALUE x) return z; } +static inline size_t +max_size(size_t a, size_t b) +{ + return (a > b ? a : b); +} + /* balancing multiplication by slicing larger argument */ static void bary_mul_balance_with_mulfunc(BDIGIT *const zds, const size_t zn, @@ -1649,15 +1672,21 @@ bary_mul_balance_with_mulfunc(BDIGIT *const zds, const size_t zn, VALUE work = 0; size_t n; - assert(xn + yn <= zn); - assert(xn <= yn); - assert(!KARATSUBA_BALANCED(xn, yn) || !TOOM3_BALANCED(xn, yn)); + RUBY_ASSERT(xn + yn <= zn); + RUBY_ASSERT(xn <= yn); + RUBY_ASSERT(!KARATSUBA_BALANCED(xn, yn) || !TOOM3_BALANCED(xn, yn)); BDIGITS_ZERO(zds, xn); if (wn < xn) { - const size_t r = (yn % xn) ? (yn % xn) : xn; - if ((2 * xn + yn + r) > zn) { + /* The condition when a new buffer is needed: + * 1. (2(xn+r) > zn-(yn-r)) => (2xn+r > zn-yn), at the last + * iteration (or r == 0) + * 2. (2(xn+xn) > zn-(yn-r-xn)) => (3xn-r > zn-yn), at the + * previous iteration. + */ + const size_t r = yn % xn; + if (2*xn + yn + max_size(xn-r, r) > zn) { wn = xn; wds = ALLOCV_N(BDIGIT, work, wn); } @@ -1692,7 +1721,7 @@ bary_mul_balance_with_mulfunc(BDIGIT *const zds, const size_t zn, zds + n, tn, wds, xn); } - n += r; + n += r; } BDIGITS_ZERO(zds+xn+yn, zn - (xn+yn)); @@ -1727,9 +1756,9 @@ bary_mul_karatsuba(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const B const BDIGIT *xds0, *xds1, *yds0, *yds1; BDIGIT *zds0, *zds1, *zds2, *zds3; - assert(xn + yn <= zn); - assert(xn <= yn); - assert(yn < 2 * xn); + RUBY_ASSERT(xn + yn <= zn); + RUBY_ASSERT(xn <= yn); + RUBY_ASSERT(yn < 2 * xn); sq = xds == yds && xn == yn; @@ -1744,7 +1773,7 @@ bary_mul_karatsuba(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const B n = yn / 2; - assert(n < xn); + RUBY_ASSERT(n < xn); if (wn < n) { /* This function itself needs only n BDIGITs for work area. @@ -1865,7 +1894,7 @@ bary_mul_karatsuba(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const B for (x = 0, i = xn-1; 0 <= i; i--) { x <<= SIZEOF_BDIGIT*CHAR_BIT; x |= xds[i]; } for (y = 0, i = yn-1; 0 <= i; i--) { y <<= SIZEOF_BDIGIT*CHAR_BIT; y |= yds[i]; } for (z = 0, i = zn-1; 0 <= i; i--) { z <<= SIZEOF_BDIGIT*CHAR_BIT; z |= zds[i]; } - assert(z == x * y); + RUBY_ASSERT(z == x * y); } */ @@ -1933,11 +1962,11 @@ bary_mul_toom3(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGI int sq = xds == yds && xn == yn; - assert(xn <= yn); /* assume y >= x */ - assert(xn + yn <= zn); + RUBY_ASSERT(xn <= yn); /* assume y >= x */ + RUBY_ASSERT(xn + yn <= zn); n = (yn + 2) / 3; - assert(2*n < xn); + RUBY_ASSERT(2*n < xn); wnc = 0; @@ -2084,21 +2113,21 @@ bary_mul_toom3(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGI v3n = u3n; v3ds = u3ds; v3p = u3p; } else { - /* v1 <- y0 + y2 */ + /* v1 <- y0 + y2 */ bary_add(v1ds, v1n, y0ds, y0n, y2ds, y2n); v1p = 1; - /* y(-1) : v2 <- v1 - y1 = y0 - y1 + y2 */ + /* y(-1) : v2 <- v1 - y1 = y0 - y1 + y2 */ v2p = 1; if (bary_sub(v2ds, v2n, v1ds, v1n, y1ds, y1n)) { bary_2comp(v2ds, v2n); v2p = 0; } - /* y(1) : v1 <- v1 + y1 = y0 + y1 + y2 */ + /* y(1) : v1 <- v1 + y1 = y0 + y1 + y2 */ bary_add(v1ds, v1n, v1ds, v1n, y1ds, y1n); - /* y(-2) : v3 <- 2 * (v2 + y2) - y0 = y0 - 2 * (y1 - 2 * y2) */ + /* y(-2) : v3 <- 2 * (v2 + y2) - y0 = y0 - 2 * (y1 - 2 * y2) */ v3p = 1; if (v2p) { bary_add(v3ds, v3n, v2ds, v2n, y2ds, y2n); @@ -2124,19 +2153,19 @@ bary_mul_toom3(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGI /* z(1) : t1 <- u1 * v1 */ bary_mul_toom3_start(t1ds, t1n, u1ds, u1n, v1ds, v1n, wds, wn); t1p = u1p == v1p; - assert(t1ds[t1n-1] == 0); + RUBY_ASSERT(t1ds[t1n-1] == 0); t1n--; /* z(-1) : t2 <- u2 * v2 */ bary_mul_toom3_start(t2ds, t2n, u2ds, u2n, v2ds, v2n, wds, wn); t2p = u2p == v2p; - assert(t2ds[t2n-1] == 0); + RUBY_ASSERT(t2ds[t2n-1] == 0); t2n--; /* z(-2) : t3 <- u3 * v3 */ bary_mul_toom3_start(t3ds, t3n, u3ds, u3n, v3ds, v3n, wds, wn); t3p = u3p == v3p; - assert(t3ds[t3n-1] == 0); + RUBY_ASSERT(t3ds[t3n-1] == 0); t3n--; /* z(inf) : t4 <- x2 * y2 */ @@ -2291,7 +2320,7 @@ rb_big_mul_toom3(VALUE x, VALUE y) return z; } -#ifdef USE_GMP +#if USE_GMP static inline void bdigits_to_mpz(mpz_t mp, const BDIGIT *digits, size_t len) { @@ -2312,7 +2341,7 @@ bary_mul_gmp(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGIT mpz_t x, y, z; size_t count; - assert(xn + yn <= zn); + RUBY_ASSERT(xn + yn <= zn); mpz_init(x); mpz_init(y); @@ -2347,7 +2376,7 @@ rb_big_mul_gmp(VALUE x, VALUE y) static void bary_short_mul(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGIT *yds, size_t yn) { - assert(xn + yn <= zn); + RUBY_ASSERT(xn + yn <= zn); if (xn == 1 && yn == 1) { bary_mul_single(zds, zn, xds[0], yds[0]); @@ -2383,7 +2412,7 @@ bary_mul_precheck(BDIGIT **zdsp, size_t *znp, const BDIGIT **xdsp, size_t *xnp, const BDIGIT *yds = *ydsp; size_t yn = *ynp; - assert(xn + yn <= zn); + RUBY_ASSERT(xn + yn <= zn); nlsz = 0; @@ -2429,10 +2458,10 @@ bary_mul_precheck(BDIGIT **zdsp, size_t *znp, const BDIGIT **xdsp, size_t *xnp, if (xn > yn) { const BDIGIT *tds; size_t tn; - tds = xds; xds = yds; yds = tds; - tn = xn; xn = yn; yn = tn; + tds = xds; xds = yds; yds = tds; + tn = xn; xn = yn; yn = tn; } - assert(xn <= yn); + RUBY_ASSERT(xn <= yn); if (xn <= 1) { if (xn == 0) { @@ -2556,7 +2585,7 @@ bary_mul(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGIT *yds } } -#ifdef USE_GMP +#if USE_GMP bary_mul_gmp(zds, zn, xds, xn, yds, yn); #else bary_mul_toom3_start(zds, zn, xds, xn, yds, yn, NULL, 0); @@ -2580,26 +2609,26 @@ bigdivrem1(void *ptr) BDIGIT q; do { - if (bds->stop) { - bds->zn = zn; - return 0; + if (bds->stop) { + bds->zn = zn; + return 0; } - if (zds[zn-1] == yds[yn-1]) q = BDIGMAX; - else q = (BDIGIT)((BIGUP(zds[zn-1]) + zds[zn-2])/yds[yn-1]); - if (q) { + if (zds[zn-1] == yds[yn-1]) q = BDIGMAX; + else q = (BDIGIT)((BIGUP(zds[zn-1]) + zds[zn-2])/yds[yn-1]); + if (q) { num = bigdivrem_mulsub(zds+zn-(yn+1), yn+1, q, yds, yn); - while (num) { /* "add back" required */ - q--; + while (num) { /* "add back" required */ + q--; num = bary_add(zds+zn-(yn+1), yn, zds+zn-(yn+1), yn, yds, yn); num--; - } - } + } + } zn--; - zds[zn] = q; + zds[zn] = q; } while (zn > yn); return 0; } @@ -2615,8 +2644,8 @@ rb_big_stop(void *ptr) static BDIGIT bigdivrem_single1(BDIGIT *qds, const BDIGIT *xds, size_t xn, BDIGIT x_higher_bdigit, BDIGIT y) { - assert(0 < xn); - assert(x_higher_bdigit < y); + RUBY_ASSERT(0 < xn); + RUBY_ASSERT(x_higher_bdigit < y); if (POW2_P(y)) { BDIGIT r; r = xds[0] & (y-1); @@ -2648,9 +2677,9 @@ bigdivrem_restoring(BDIGIT *zds, size_t zn, BDIGIT *yds, size_t yn) struct big_div_struct bds; size_t ynzero; - assert(yn < zn); - assert(BDIGIT_MSB(yds[yn-1])); - assert(zds[zn-1] < yds[yn-1]); + RUBY_ASSERT(yn < zn); + RUBY_ASSERT(BDIGIT_MSB(yds[yn-1])); + RUBY_ASSERT(zds[zn-1] < yds[yn-1]); for (ynzero = 0; !yds[ynzero]; ynzero++); @@ -2668,16 +2697,16 @@ bigdivrem_restoring(BDIGIT *zds, size_t zn, BDIGIT *yds, size_t yn) bds.zn = zn - ynzero; if (bds.zn > 10000 || bds.yn > 10000) { retry: - bds.stop = Qfalse; - rb_nogvl(bigdivrem1, &bds, rb_big_stop, &bds, RB_NOGVL_UBF_ASYNC_SAFE); + bds.stop = Qfalse; + rb_nogvl(bigdivrem1, &bds, rb_big_stop, &bds, RB_NOGVL_UBF_ASYNC_SAFE | RB_NOGVL_OFFLOAD_SAFE); - if (bds.stop == Qtrue) { - /* execute trap handler, but exception was not raised. */ - goto retry; - } + if (bds.stop == Qtrue) { + /* execute trap handler, but exception was not raised. */ + goto retry; + } } else { - bigdivrem1(&bds); + bigdivrem1(&bds); } } @@ -2689,9 +2718,9 @@ bary_divmod_normal(BDIGIT *qds, size_t qn, BDIGIT *rds, size_t rn, const BDIGIT size_t zn; VALUE tmpyz = 0; - assert(yn < xn || (xn == yn && yds[yn - 1] <= xds[xn - 1])); - assert(qds ? (xn - yn + 1) <= qn : 1); - assert(rds ? yn <= rn : 1); + RUBY_ASSERT(yn < xn || (xn == yn && yds[yn - 1] <= xds[xn - 1])); + RUBY_ASSERT(qds ? (xn - yn + 1) <= qn : 1); + RUBY_ASSERT(rds ? yn <= rn : 1); zn = xn + BIGDIVREM_EXTRA_WORDS; @@ -2776,17 +2805,17 @@ rb_big_divrem_normal(VALUE x, VALUE y) return rb_assoc_new(q, r); } -#ifdef USE_GMP +#if USE_GMP static void bary_divmod_gmp(BDIGIT *qds, size_t qn, BDIGIT *rds, size_t rn, const BDIGIT *xds, size_t xn, const BDIGIT *yds, size_t yn) { mpz_t x, y, q, r; size_t count; - assert(yn < xn || (xn == yn && yds[yn - 1] <= xds[xn - 1])); - assert(qds ? (xn - yn + 1) <= qn : 1); - assert(rds ? yn <= rn : 1); - assert(qds || rds); + RUBY_ASSERT(yn < xn || (xn == yn && yds[yn - 1] <= xds[xn - 1])); + RUBY_ASSERT(qds ? (xn - yn + 1) <= qn : 1); + RUBY_ASSERT(rds ? yn <= rn : 1); + RUBY_ASSERT(qds || rds); mpz_init(x); mpz_init(y); @@ -2860,7 +2889,7 @@ rb_big_divrem_gmp(VALUE x, VALUE y) static void bary_divmod_branch(BDIGIT *qds, size_t qn, BDIGIT *rds, size_t rn, const BDIGIT *xds, size_t xn, const BDIGIT *yds, size_t yn) { -#ifdef USE_GMP +#if USE_GMP if (GMP_DIV_DIGITS < xn) { bary_divmod_gmp(qds, qn, rds, rn, xds, xn, yds, yn); return; @@ -2872,8 +2901,8 @@ bary_divmod_branch(BDIGIT *qds, size_t qn, BDIGIT *rds, size_t rn, const BDIGIT static void bary_divmod(BDIGIT *qds, size_t qn, BDIGIT *rds, size_t rn, const BDIGIT *xds, size_t xn, const BDIGIT *yds, size_t yn) { - assert(xn <= qn); - assert(yn <= rn); + RUBY_ASSERT(xn <= qn); + RUBY_ASSERT(yn <= rn); BARY_TRUNC(yds, yn); if (yn == 0) @@ -2935,7 +2964,7 @@ int rb_cmpint(VALUE val, VALUE a, VALUE b) { if (NIL_P(val)) { - rb_cmperr(a, b); + rb_cmperr(a, b); } if (FIXNUM_P(val)) { long l = FIX2LONG(val); @@ -2944,9 +2973,9 @@ rb_cmpint(VALUE val, VALUE a, VALUE b) return 0; } if (RB_BIGNUM_TYPE_P(val)) { - if (BIGZEROP(val)) return 0; - if (BIGNUM_SIGN(val)) return 1; - return -1; + if (BIGZEROP(val)) return 0; + if (BIGNUM_SIGN(val)) return 1; + return -1; } if (RTEST(rb_funcall(val, '>', 1, INT2FIX(0)))) return 1; if (RTEST(rb_funcall(val, '<', 1, INT2FIX(0)))) return -1; @@ -2956,42 +2985,73 @@ rb_cmpint(VALUE val, VALUE a, VALUE b) #define BIGNUM_SET_LEN(b,l) \ (BIGNUM_EMBED_P(b) ? \ (void)(RBASIC(b)->flags = \ - (RBASIC(b)->flags & ~BIGNUM_EMBED_LEN_MASK) | \ - ((l) << BIGNUM_EMBED_LEN_SHIFT)) : \ + (RBASIC(b)->flags & ~BIGNUM_EMBED_LEN_MASK) | \ + ((l) << BIGNUM_EMBED_LEN_SHIFT)) : \ (void)(RBIGNUM(b)->as.heap.len = (l))) +static size_t +big_embed_capa(VALUE big) +{ + size_t size = rb_gc_obj_slot_size(big) - offsetof(struct RBignum, as.ary); + RUBY_ASSERT(size % sizeof(BDIGIT) == 0); + size_t capa = size / sizeof(BDIGIT); + RUBY_ASSERT(capa <= BIGNUM_EMBED_LEN_MAX); + return capa; +} + +static size_t +big_embed_size(size_t capa) +{ + size_t size = offsetof(struct RBignum, as.ary) + (sizeof(BDIGIT) * capa); + if (size < sizeof(struct RBignum)) { + size = sizeof(struct RBignum); + } + return size; +} + +static bool +big_embeddable_p(size_t capa) +{ + if (capa > BIGNUM_EMBED_LEN_MAX) { + return false; + } + return rb_gc_size_allocatable_p(big_embed_size(capa)); +} + static void rb_big_realloc(VALUE big, size_t len) { BDIGIT *ds; + size_t embed_capa = big_embed_capa(big); + if (BIGNUM_EMBED_P(big)) { - if (BIGNUM_EMBED_LEN_MAX < len) { - ds = ALLOC_N(BDIGIT, len); - MEMCPY(ds, RBIGNUM(big)->as.ary, BDIGIT, BIGNUM_EMBED_LEN_MAX); - RBIGNUM(big)->as.heap.len = BIGNUM_LEN(big); - RBIGNUM(big)->as.heap.digits = ds; + if (embed_capa < len) { + ds = ALLOC_N(BDIGIT, len); + MEMCPY(ds, RBIGNUM(big)->as.ary, BDIGIT, embed_capa); + RBIGNUM(big)->as.heap.len = BIGNUM_LEN(big); + RBIGNUM(big)->as.heap.digits = ds; FL_UNSET_RAW(big, BIGNUM_EMBED_FLAG); - } + } } else { - if (len <= BIGNUM_EMBED_LEN_MAX) { - ds = RBIGNUM(big)->as.heap.digits; + if (len <= embed_capa) { + ds = RBIGNUM(big)->as.heap.digits; FL_SET_RAW(big, BIGNUM_EMBED_FLAG); - BIGNUM_SET_LEN(big, len); - (void)VALGRIND_MAKE_MEM_UNDEFINED((void*)RBIGNUM(big)->as.ary, sizeof(RBIGNUM(big)->as.ary)); - if (ds) { - MEMCPY(RBIGNUM(big)->as.ary, ds, BDIGIT, len); - xfree(ds); - } - } - else { - if (BIGNUM_LEN(big) == 0) { - RBIGNUM(big)->as.heap.digits = ALLOC_N(BDIGIT, len); - } - else { - REALLOC_N(RBIGNUM(big)->as.heap.digits, BDIGIT, len); - } - } + BIGNUM_SET_LEN(big, len); + (void)VALGRIND_MAKE_MEM_UNDEFINED((void*)RBIGNUM(big)->as.ary, embed_capa * sizeof(BDIGIT)); + if (ds) { + MEMCPY(RBIGNUM(big)->as.ary, ds, BDIGIT, len); + xfree(ds); + } + } + else { + if (BIGNUM_LEN(big) == 0) { + RBIGNUM(big)->as.heap.digits = ALLOC_N(BDIGIT, len); + } + else if (BIGNUM_LEN(big) < len) { + REALLOC_N(RBIGNUM(big)->as.heap.digits, BDIGIT, len); + } + } } } @@ -3005,15 +3065,24 @@ rb_big_resize(VALUE big, size_t len) static VALUE bignew_1(VALUE klass, size_t len, int sign) { - NEWOBJ_OF(big, struct RBignum, klass, T_BIGNUM | (RGENGC_WB_PROTECTED_BIGNUM ? FL_WB_PROTECTED : 0)); - VALUE bigv = (VALUE)big; - BIGNUM_SET_SIGN(bigv, sign); - if (len <= BIGNUM_EMBED_LEN_MAX) { - FL_SET_RAW(bigv, BIGNUM_EMBED_FLAG); + VALUE bigv; + + if (big_embeddable_p(len)) { + size_t size = big_embed_size(len); + RUBY_ASSERT(rb_gc_size_allocatable_p(size)); + NEWOBJ_OF(big, struct RBignum, klass, + T_BIGNUM | BIGNUM_EMBED_FLAG | (RGENGC_WB_PROTECTED_BIGNUM ? FL_WB_PROTECTED : 0), + size, 0); + bigv = (VALUE)big; + BIGNUM_SET_SIGN(bigv, sign); BIGNUM_SET_LEN(bigv, len); - (void)VALGRIND_MAKE_MEM_UNDEFINED((void*)big->as.ary, sizeof(big->as.ary)); + (void)VALGRIND_MAKE_MEM_UNDEFINED((void*)big->as.ary, len * sizeof(BDIGIT)); } else { + NEWOBJ_OF(big, struct RBignum, klass, + T_BIGNUM | (RGENGC_WB_PROTECTED_BIGNUM ? FL_WB_PROTECTED : 0), sizeof(struct RBignum), 0); + bigv = (VALUE)big; + BIGNUM_SET_SIGN(bigv, sign); big->as.heap.digits = ALLOC_N(BDIGIT, len); big->as.heap.len = len; } @@ -3024,7 +3093,9 @@ bignew_1(VALUE klass, size_t len, int sign) VALUE rb_big_new(size_t len, int sign) { - return bignew(len, sign != 0); + VALUE obj = bignew(len, sign != 0); + memset(BIGNUM_DIGITS(obj), 0, len * sizeof(BDIGIT)); + return obj; } VALUE @@ -3077,7 +3148,7 @@ abs2twocomp(VALUE *xp, long *n_ret) MEMCPY(BDIGITS(z), ds, BDIGIT, n); bary_2comp(BDIGITS(z), n); hibits = BDIGMAX; - *xp = z; + *xp = z; } *n_ret = n; return hibits; @@ -3101,7 +3172,7 @@ bigtrunc(VALUE x) if (len == 0) return x; while (--len && !ds[len]); if (BIGNUM_LEN(x) > len+1) { - rb_big_resize(x, len+1); + rb_big_resize(x, len+1); } return x; } @@ -3154,7 +3225,7 @@ static VALUE bignorm(VALUE x) { if (RB_BIGNUM_TYPE_P(x)) { - x = bigfixize(x); + x = bigfixize(x); } return x; } @@ -3176,8 +3247,8 @@ rb_uint2big(uintptr_t n) digits[0] = n; #else for (i = 0; i < bdigit_roomof(SIZEOF_VALUE); i++) { - digits[i] = BIGLO(n); - n = BIGDN(n); + digits[i] = BIGLO(n); + n = BIGDN(n); } #endif @@ -3196,14 +3267,14 @@ rb_int2big(intptr_t n) if (n < 0) { u = 1 + (VALUE)(-(n + 1)); /* u = -n avoiding overflow */ - neg = 1; + neg = 1; } else { u = n; } big = rb_uint2big(u); if (neg) { - BIGNUM_SET_NEGATIVE_SIGN(big); + BIGNUM_SET_NEGATIVE_SIGN(big); } return big; } @@ -3362,7 +3433,7 @@ absint_numwords_generic(size_t numbytes, int nlz_bits_in_msbyte, size_t word_num if (sign == 2) { #if defined __GNUC__ && (__GNUC__ == 4 && __GNUC_MINOR__ == 4) - *nlz_bits_ret = 0; + *nlz_bits_ret = 0; #endif return (size_t)-1; } @@ -3404,15 +3475,13 @@ rb_absint_numwords(VALUE val, size_t word_numbits, size_t *nlz_bits_ret) if (numbytes <= SIZE_MAX / CHAR_BIT) { numwords = absint_numwords_small(numbytes, nlz_bits_in_msbyte, word_numbits, &nlz_bits); -#ifdef DEBUG_INTEGER_PACK - { + if (debug_integer_pack) { size_t numwords0, nlz_bits0; numwords0 = absint_numwords_generic(numbytes, nlz_bits_in_msbyte, word_numbits, &nlz_bits0); - assert(numwords0 == numwords); - assert(nlz_bits0 == nlz_bits); + RUBY_ASSERT(numwords0 == numwords); + RUBY_ASSERT(nlz_bits0 == nlz_bits); (void)numwords0; } -#endif } else { numwords = absint_numwords_generic(numbytes, nlz_bits_in_msbyte, word_numbits, &nlz_bits); @@ -3682,7 +3751,7 @@ rb_integer_unpack(const void *words, size_t numwords, size_t wordsize, size_t na } else if (num_bdigits == numberof(fixbuf)) { val = bignew((long)num_bdigits+1, 0); - MEMCPY(BDIGITS(val), fixbuf, BDIGIT, num_bdigits); + MEMCPY(BDIGITS(val), fixbuf, BDIGIT, num_bdigits); BDIGITS(val)[num_bdigits++] = 1; } else { @@ -3694,9 +3763,9 @@ rb_integer_unpack(const void *words, size_t numwords, size_t wordsize, size_t na BDIGIT_DBL u = fixbuf[0] + BIGUP(fixbuf[1]); if (u == 0) return LONG2FIX(0); - if (0 < sign && POSFIXABLE(u)) + if (0 < sign && POSFIXABLE(u)) return LONG2FIX((long)u); - if (sign < 0 && BDIGIT_MSB(fixbuf[1]) == 0 && + if (sign < 0 && BDIGIT_MSB(fixbuf[1]) == 0 && NEGFIXABLE(-(BDIGIT_DBL_SIGNED)u)) return LONG2FIX((long)-(BDIGIT_DBL_SIGNED)u); val = bignew((long)num_bdigits, 0 <= sign); @@ -3748,41 +3817,41 @@ str2big_scan_digits(const char *s, const char *str, int base, int badcheck, size int c; if (!len) { - *num_digits_p = 0; - *len_p = 0; - return TRUE; + *num_digits_p = 0; + *len_p = 0; + return TRUE; } if (badcheck && *str == '_') return FALSE; while ((c = *str++) != 0) { - if (c == '_') { - if (nondigit) { + if (c == '_') { + if (nondigit) { if (badcheck) return FALSE; - break; - } - nondigit = (char) c; - } - else if ((c = conv_digit(c)) < 0 || c >= base) { - break; - } - else { - nondigit = 0; - num_digits++; - digits_end = str; - } - if (len > 0 && !--len) break; + break; + } + nondigit = (char) c; + } + else if ((c = conv_digit(c)) < 0 || c >= base) { + break; + } + else { + nondigit = 0; + num_digits++; + digits_end = str; + } + if (len > 0 && !--len) break; } if (badcheck && nondigit) return FALSE; if (badcheck && len) { - str--; - while (*str && ISSPACE(*str)) { - str++; - if (len > 0 && !--len) break; - } - if (len && *str) { - return FALSE; - } + str--; + while (*str && ISSPACE(*str)) { + str++; + if (len > 0 && !--len) break; + } + if (len && *str) { + return FALSE; + } } *num_digits_p = num_digits; *len_p = digits_end - digits_start; @@ -3825,7 +3894,7 @@ str2big_poweroftwo( if (numbits) { *dp++ = BIGLO(dd); } - assert((size_t)(dp - BDIGITS(z)) == num_bdigits); + RUBY_ASSERT((size_t)(dp - BDIGITS(z)) == num_bdigits); return z; } @@ -3868,7 +3937,7 @@ str2big_normal( } break; } - assert(blen <= num_bdigits); + RUBY_ASSERT(blen <= num_bdigits); } return z; @@ -3926,7 +3995,7 @@ str2big_karatsuba( current_base = 1; } } - assert(i == num_bdigits); + RUBY_ASSERT(i == num_bdigits); for (unit = 2; unit < num_bdigits; unit *= 2) { for (i = 0; i < num_bdigits; i += unit*2) { if (2*unit <= num_bdigits - i) { @@ -3957,7 +4026,7 @@ str2big_karatsuba( return z; } -#ifdef USE_GMP +#if USE_GMP static VALUE str2big_gmp( int sign, @@ -4024,8 +4093,8 @@ rb_cstr_to_inum(const char *str, int base, int badcheck) char *end; VALUE ret = rb_cstr_parse_inum(str, -1, (badcheck ? NULL : &end), base); if (NIL_P(ret)) { - if (badcheck) rb_invalid_str(str, "Integer()"); - ret = INT2FIX(0); + if (badcheck) rb_invalid_str(str, "Integer()"); + ret = INT2FIX(0); } return ret; } @@ -4049,7 +4118,7 @@ rb_cstr_to_inum(const char *str, int base, int badcheck) VALUE rb_int_parse_cstr(const char *str, ssize_t len, char **endp, size_t *ndigits, - int base, int flags) + int base, int flags) { const char *const s = str; char sign = 1; @@ -4066,82 +4135,82 @@ rb_int_parse_cstr(const char *str, ssize_t len, char **endp, size_t *ndigits, const int badcheck = !endp; #define ADV(n) do {\ - if (len > 0 && len <= (n)) goto bad; \ - str += (n); \ - len -= (n); \ + if (len > 0 && len <= (n)) goto bad; \ + str += (n); \ + len -= (n); \ } while (0) #define ASSERT_LEN() do {\ - assert(len != 0); \ - if (len0 >= 0) assert(s + len0 == str + len); \ + RUBY_ASSERT(len != 0); \ + if (len0 >= 0) RUBY_ASSERT(s + len0 == str + len); \ } while (0) if (!str) { goto bad; } if (len && (flags & RB_INT_PARSE_SIGN)) { - while (ISSPACE(*str)) ADV(1); + while (ISSPACE(*str)) ADV(1); - if (str[0] == '+') { - ADV(1); - } - else if (str[0] == '-') { - ADV(1); - sign = 0; - } - ASSERT_LEN(); + if (str[0] == '+') { + ADV(1); + } + else if (str[0] == '-') { + ADV(1); + sign = 0; + } + ASSERT_LEN(); } if (base <= 0) { - if (str[0] == '0' && len > 1) { - switch (str[1]) { - case 'x': case 'X': - base = 16; - ADV(2); - break; - case 'b': case 'B': - base = 2; - ADV(2); - break; - case 'o': case 'O': - base = 8; - ADV(2); - break; - case 'd': case 'D': - base = 10; - ADV(2); - break; - default: - base = 8; - } - } - else if (base < -1) { - base = -base; - } - else { - base = 10; - } + if (str[0] == '0' && len > 1) { + switch (str[1]) { + case 'x': case 'X': + base = 16; + ADV(2); + break; + case 'b': case 'B': + base = 2; + ADV(2); + break; + case 'o': case 'O': + base = 8; + ADV(2); + break; + case 'd': case 'D': + base = 10; + ADV(2); + break; + default: + base = 8; + } + } + else if (base < -1) { + base = -base; + } + else { + base = 10; + } } else if (len == 1 || !(flags & RB_INT_PARSE_PREFIX)) { - /* no prefix */ + /* no prefix */ } else if (base == 2) { - if (str[0] == '0' && (str[1] == 'b'||str[1] == 'B')) { - ADV(2); - } + if (str[0] == '0' && (str[1] == 'b'||str[1] == 'B')) { + ADV(2); + } } else if (base == 8) { - if (str[0] == '0' && (str[1] == 'o'||str[1] == 'O')) { - ADV(2); - } + if (str[0] == '0' && (str[1] == 'o'||str[1] == 'O')) { + ADV(2); + } } else if (base == 10) { - if (str[0] == '0' && (str[1] == 'd'||str[1] == 'D')) { - ADV(2); - } + if (str[0] == '0' && (str[1] == 'd'||str[1] == 'D')) { + ADV(2); + } } else if (base == 16) { - if (str[0] == '0' && (str[1] == 'x'||str[1] == 'X')) { - ADV(2); - } + if (str[0] == '0' && (str[1] == 'x'||str[1] == 'X')) { + ADV(2); + } } if (!valid_radix_p(base)) { invalid_radix(base); @@ -4149,80 +4218,79 @@ rb_int_parse_cstr(const char *str, ssize_t len, char **endp, size_t *ndigits, if (!len) goto bad; num_digits = str - s; if (*str == '0' && len != 1) { /* squeeze preceding 0s */ - int us = 0; - const char *end = len < 0 ? NULL : str + len; - ++num_digits; - while ((c = *++str) == '0' || - ((flags & RB_INT_PARSE_UNDERSCORE) && c == '_')) { - if (c == '_') { - if (++us >= 2) - break; - } - else { - ++num_digits; - us = 0; - } - if (str == end) break; - } - if (!c || ISSPACE(c)) --str; - if (end) len = end - str; - ASSERT_LEN(); + int us = 0; + const char *end = len < 0 ? NULL : str + len; + ++num_digits; + while ((c = *++str) == '0' || + ((flags & RB_INT_PARSE_UNDERSCORE) && c == '_')) { + if (c == '_') { + if (++us >= 2) + break; + } + else { + ++num_digits; + us = 0; + } + if (str == end) break; + } + if (!c || ISSPACE(c)) --str; + if (end) len = end - str; } c = *str; c = conv_digit(c); if (c < 0 || c >= base) { - if (!badcheck && num_digits) z = INT2FIX(0); - goto bad; + if (!badcheck && num_digits) z = INT2FIX(0); + goto bad; } if (ndigits) *ndigits = num_digits; val = ruby_scan_digits(str, len, base, &num_digits, &ov); if (!ov) { - const char *end = &str[num_digits]; - if (num_digits > 0 && *end == '_' && (flags & RB_INT_PARSE_UNDERSCORE)) - goto bigparse; - if (endp) *endp = (char *)end; - if (ndigits) *ndigits += num_digits; - if (badcheck) { - if (num_digits == 0) return Qnil; /* no number */ - while (len < 0 ? *end : end < str + len) { - if (!ISSPACE(*end)) return Qnil; /* trailing garbage */ - end++; - } - } - - if (POSFIXABLE(val)) { - if (sign) return LONG2FIX(val); - else { - long result = -(long)val; - return LONG2FIX(result); - } - } - else { - VALUE big = rb_uint2big(val); - BIGNUM_SET_SIGN(big, sign); - return bignorm(big); - } + const char *end = &str[num_digits]; + if (num_digits > 0 && *end == '_' && (flags & RB_INT_PARSE_UNDERSCORE)) + goto bigparse; + if (endp) *endp = (char *)end; + if (ndigits) *ndigits += num_digits; + if (badcheck) { + if (num_digits == 0) return Qnil; /* no number */ + while (len < 0 ? *end : end < str + len) { + if (!ISSPACE(*end)) return Qnil; /* trailing garbage */ + end++; + } + } + + if (POSFIXABLE(val)) { + if (sign) return LONG2FIX(val); + else { + long result = -(long)val; + return LONG2FIX(result); + } + } + else { + VALUE big = rb_uint2big(val); + BIGNUM_SET_SIGN(big, sign); + return bignorm(big); + } } bigparse: digits_start = str; if (!str2big_scan_digits(s, str, base, badcheck, &num_digits, &len)) - goto bad; + goto bad; if (endp) *endp = (char *)(str + len); if (ndigits) *ndigits += num_digits; digits_end = digits_start + len; if (POW2_P(base)) { z = str2big_poweroftwo(sign, digits_start, digits_end, num_digits, - bit_length(base-1)); + bit_length(base-1)); } else { int digits_per_bdigits_dbl; maxpow_in_bdigit_dbl(base, &digits_per_bdigits_dbl); num_bdigits = roomof(num_digits, digits_per_bdigits_dbl)*2; -#ifdef USE_GMP +#if USE_GMP if (GMP_STR2BIG_DIGITS < num_bdigits) { z = str2big_gmp(sign, digits_start, digits_end, num_digits, num_bdigits, base); @@ -4251,7 +4319,7 @@ static VALUE rb_cstr_parse_inum(const char *str, ssize_t len, char **endp, int base) { return rb_int_parse_cstr(str, len, endp, NULL, base, - RB_INT_PARSE_DEFAULT); + RB_INT_PARSE_DEFAULT); } VALUE @@ -4300,14 +4368,14 @@ rb_str2big_poweroftwo(VALUE arg, int base, int badcheck) s = str = StringValueCStr(arg); len = RSTRING_LEN(arg); if (*str == '-') { - len--; + len--; str++; positive_p = 0; } digits_start = str; if (!str2big_scan_digits(s, str, base, badcheck, &num_digits, &len)) - invalid_integer(arg); + invalid_integer(arg); digits_end = digits_start + len; z = str2big_poweroftwo(positive_p, digits_start, digits_end, num_digits, @@ -4339,14 +4407,14 @@ rb_str2big_normal(VALUE arg, int base, int badcheck) s = str = StringValuePtr(arg); len = RSTRING_LEN(arg); if (len > 0 && *str == '-') { - len--; + len--; str++; positive_p = 0; } digits_start = str; if (!str2big_scan_digits(s, str, base, badcheck, &num_digits, &len)) - invalid_integer(arg); + invalid_integer(arg); digits_end = digits_start + len; maxpow_in_bdigit_dbl(base, &digits_per_bdigits_dbl); @@ -4381,14 +4449,14 @@ rb_str2big_karatsuba(VALUE arg, int base, int badcheck) s = str = StringValuePtr(arg); len = RSTRING_LEN(arg); if (len > 0 && *str == '-') { - len--; + len--; str++; positive_p = 0; } digits_start = str; if (!str2big_scan_digits(s, str, base, badcheck, &num_digits, &len)) - invalid_integer(arg); + invalid_integer(arg); digits_end = digits_start + len; maxpow_in_bdigit_dbl(base, &digits_per_bdigits_dbl); @@ -4402,7 +4470,7 @@ rb_str2big_karatsuba(VALUE arg, int base, int badcheck) return bignorm(z); } -#ifdef USE_GMP +#if USE_GMP VALUE rb_str2big_gmp(VALUE arg, int base, int badcheck) { @@ -4424,14 +4492,14 @@ rb_str2big_gmp(VALUE arg, int base, int badcheck) s = str = StringValuePtr(arg); len = RSTRING_LEN(arg); if (len > 0 && *str == '-') { - len--; + len--; str++; positive_p = 0; } digits_start = str; if (!str2big_scan_digits(s, str, base, badcheck, &num_digits, &len)) - invalid_integer(arg); + invalid_integer(arg); digits_end = digits_start + len; maxpow_in_bdigit_dbl(base, &digits_per_bdigits_dbl); @@ -4447,7 +4515,7 @@ rb_str2big_gmp(VALUE arg, int base, int badcheck) #if HAVE_LONG_LONG -static VALUE +VALUE rb_ull2big(unsigned LONG_LONG n) { long i; @@ -4458,8 +4526,8 @@ rb_ull2big(unsigned LONG_LONG n) digits[0] = n; #else for (i = 0; i < bdigit_roomof(SIZEOF_LONG_LONG); i++) { - digits[i] = BIGLO(n); - n = BIGDN(n); + digits[i] = BIGLO(n); + n = BIGDN(n); } #endif @@ -4469,7 +4537,7 @@ rb_ull2big(unsigned LONG_LONG n) return big; } -static VALUE +VALUE rb_ll2big(LONG_LONG n) { long neg = 0; @@ -4478,14 +4546,14 @@ rb_ll2big(LONG_LONG n) if (n < 0) { u = 1 + (unsigned LONG_LONG)(-(n + 1)); /* u = -n avoiding overflow */ - neg = 1; + neg = 1; } else { u = n; } big = rb_ull2big(u); if (neg) { - BIGNUM_SET_NEGATIVE_SIGN(big); + BIGNUM_SET_NEGATIVE_SIGN(big); } return big; } @@ -4507,7 +4575,7 @@ rb_ll2inum(LONG_LONG n) #endif /* HAVE_LONG_LONG */ #ifdef HAVE_INT128_T -static VALUE +VALUE rb_uint128t2big(uint128_t n) { long i; @@ -4515,7 +4583,7 @@ rb_uint128t2big(uint128_t n) BDIGIT *digits = BDIGITS(big); for (i = 0; i < bdigit_roomof(SIZEOF_INT128_T); i++) { - digits[i] = BIGLO(RSHIFT(n ,BITSPERDIG*i)); + digits[i] = BIGLO(RSHIFT(n ,BITSPERDIG*i)); } i = bdigit_roomof(SIZEOF_INT128_T); @@ -4524,7 +4592,7 @@ rb_uint128t2big(uint128_t n) return big; } -MJIT_FUNC_EXPORTED VALUE +VALUE rb_int128t2big(int128_t n) { int neg = 0; @@ -4533,14 +4601,14 @@ rb_int128t2big(int128_t n) if (n < 0) { u = 1 + (uint128_t)(-(n + 1)); /* u = -n avoiding overflow */ - neg = 1; + neg = 1; } else { u = n; } big = rb_uint128t2big(u); if (neg) { - BIGNUM_SET_NEGATIVE_SIGN(big); + BIGNUM_SET_NEGATIVE_SIGN(big); } return big; } @@ -4569,11 +4637,14 @@ big_shift3(VALUE x, int lshift_p, size_t shift_numdigits, int shift_numbits) if (lshift_p) { if (LONG_MAX < shift_numdigits) { - rb_raise(rb_eArgError, "too big number"); + too_big: + rb_raise(rb_eRangeError, "shift width too big"); } s1 = shift_numdigits; s2 = shift_numbits; + if ((size_t)s1 != shift_numdigits) goto too_big; xn = BIGNUM_LEN(x); + if (LONG_MAX/SIZEOF_BDIGIT <= xn+s1) goto too_big; z = bignew(xn+s1+1, BIGNUM_SIGN(x)); zds = BDIGITS(z); BDIGITS_ZERO(zds, s1); @@ -4615,8 +4686,8 @@ big_shift2(VALUE x, int lshift_p, VALUE y) size_t shift_numdigits; int shift_numbits; - assert(POW2_P(CHAR_BIT)); - assert(POW2_P(BITSPERDIG)); + RUBY_ASSERT(POW2_P(CHAR_BIT)); + RUBY_ASSERT(POW2_P(BITSPERDIG)); if (BIGZEROP(x)) return INT2FIX(0); @@ -4703,7 +4774,7 @@ power_cache_get_power(int base, int power_level, size_t *numdigits_ret) rb_obj_hide(power); base36_power_cache[base - 2][power_level] = power; base36_numdigits_cache[base - 2][power_level] = numdigits; - rb_gc_register_mark_object(power); + rb_vm_register_global_object(power); } if (numdigits_ret) *numdigits_ret = base36_numdigits_cache[base - 2][power_level]; @@ -4739,7 +4810,7 @@ big2str_2bdigits(struct big2str_struct *b2s, BDIGIT *xds, size_t xn, size_t tail int beginning = !b2s->ptr; size_t len = 0; - assert(xn <= 2); + RUBY_ASSERT(xn <= 2); num = bary2bdigitdbl(xds, xn); if (beginning) { @@ -4754,7 +4825,7 @@ big2str_2bdigits(struct big2str_struct *b2s, BDIGIT *xds, size_t xn, size_t tail } while (num); len = sizeof(buf) - j; big2str_alloc(b2s, len + taillen); - MEMCPY(b2s->ptr, buf + j, char, len); + MEMCPY(b2s->ptr, buf + j, char, len); } else { p = b2s->ptr; @@ -4771,7 +4842,7 @@ big2str_2bdigits(struct big2str_struct *b2s, BDIGIT *xds, size_t xn, size_t tail static void big2str_karatsuba(struct big2str_struct *b2s, BDIGIT *xds, size_t xn, size_t wn, - int power_level, size_t taillen) + int power_level, size_t taillen) { VALUE b; size_t half_numdigits, lower_numdigits; @@ -4801,17 +4872,17 @@ big2str_karatsuba(struct big2str_struct *b2s, BDIGIT *xds, size_t xn, size_t wn, */ if (xn == 0 || bary_zero_p(xds, xn)) { - if (b2s->ptr) { + if (b2s->ptr) { /* When x is zero, power_cache_get_power(base, power_level) should be cached already. */ power_cache_get_power(b2s->base, power_level, &len); - memset(b2s->ptr, '0', len); + memset(b2s->ptr, '0', len); b2s->ptr += len; - } + } return; } if (power_level == 0) { - big2str_2bdigits(b2s, xds, xn, taillen); + big2str_2bdigits(b2s, xds, xn, taillen); return; } @@ -4839,7 +4910,7 @@ big2str_karatsuba(struct big2str_struct *b2s, BDIGIT *xds, size_t xn, size_t wn, memset(b2s->ptr, '0', len); b2s->ptr += len; } - big2str_2bdigits(b2s, xds, xn, taillen); + big2str_2bdigits(b2s, xds, xn, taillen); } else { BDIGIT *qds, *rds; @@ -4867,7 +4938,7 @@ big2str_karatsuba(struct big2str_struct *b2s, BDIGIT *xds, size_t xn, size_t wn, /* bigdivrem_restoring will modify y. * So use temporary buffer. */ tds = xds + qn; - assert(qn + bn <= xn + wn); + RUBY_ASSERT(qn + bn <= xn + wn); bary_small_lshift(tds, bds, bn, shift); xds[xn] = bary_small_lshift(xds, xds, xn, shift); } @@ -4885,7 +4956,7 @@ big2str_karatsuba(struct big2str_struct *b2s, BDIGIT *xds, size_t xn, size_t wn, } BARY_TRUNC(qds, qn); - assert(qn <= bn); + RUBY_ASSERT(qn <= bn); big2str_karatsuba(b2s, qds, qn, xn+wn - (rn+qn), lower_power_level, lower_numdigits+taillen); BARY_TRUNC(rds, rn); big2str_karatsuba(b2s, rds, rn, xn+wn - rn, lower_power_level, taillen); @@ -4943,14 +5014,14 @@ big2str_generic(VALUE x, int base) BARY_TRUNC(xds, xn); if (xn == 0) { - return rb_usascii_str_new2("0"); + return rb_usascii_str_new2("0"); } if (!valid_radix_p(base)) - invalid_radix(base); + invalid_radix(base); if (xn >= LONG_MAX/BITSPERDIG) { - rb_raise(rb_eRangeError, "bignum too big to convert into `string'"); + rb_raise(rb_eRangeError, "bignum too big to convert into 'string'"); } power_level = 0; @@ -4960,7 +5031,7 @@ big2str_generic(VALUE x, int base) power_level++; power = power_cache_get_power(base, power_level, NULL); } - assert(power_level != MAX_BASE36_POWER_TABLE_ENTRIES); + RUBY_ASSERT(power_level != MAX_BASE36_POWER_TABLE_ENTRIES); if ((size_t)BIGNUM_LEN(power) <= xn) { /* @@ -4984,7 +5055,7 @@ big2str_generic(VALUE x, int base) b2s_data.ptr = NULL; if (power_level == 0) { - big2str_2bdigits(&b2s_data, xds, xn, 0); + big2str_2bdigits(&b2s_data, xds, xn, 0); } else { VALUE tmpw = 0; @@ -4993,7 +5064,7 @@ big2str_generic(VALUE x, int base) wn = power_level * BIGDIVREM_EXTRA_WORDS + BIGNUM_LEN(power); wds = ALLOCV_N(BDIGIT, tmpw, xn + wn); MEMCPY(wds, xds, BDIGIT, xn); - big2str_karatsuba(&b2s_data, wds, xn, wn, power_level, 0); + big2str_karatsuba(&b2s_data, wds, xn, wn, power_level, 0); if (tmpw) ALLOCV_END(tmpw); } @@ -5012,7 +5083,7 @@ rb_big2str_generic(VALUE x, int base) return big2str_generic(x, base); } -#ifdef USE_GMP +#if USE_GMP static VALUE big2str_gmp(VALUE x, int base) { @@ -5059,7 +5130,7 @@ rb_big2str1(VALUE x, int base) size_t xn; if (FIXNUM_P(x)) { - return rb_fix2str(x, base); + return rb_fix2str(x, base); } bigtrunc(x); @@ -5068,14 +5139,14 @@ rb_big2str1(VALUE x, int base) BARY_TRUNC(xds, xn); if (xn == 0) { - return rb_usascii_str_new2("0"); + return rb_usascii_str_new2("0"); } if (!valid_radix_p(base)) - invalid_radix(base); + invalid_radix(base); if (xn >= LONG_MAX/BITSPERDIG) { - rb_raise(rb_eRangeError, "bignum too big to convert into `string'"); + rb_raise(rb_eRangeError, "bignum too big to convert into 'string'"); } if (POW2_P(base)) { @@ -5083,7 +5154,7 @@ rb_big2str1(VALUE x, int base) return big2str_base_poweroftwo(x, base); } -#ifdef USE_GMP +#if USE_GMP if (GMP_BIG2STR_DIGITS < xn) { return big2str_gmp(x, base); } @@ -5111,7 +5182,7 @@ big2ulong(VALUE x, const char *type) if (len == 0) return 0; if (BIGSIZE(x) > sizeof(long)) { - rb_raise(rb_eRangeError, "bignum too big to convert into `%s'", type); + rb_raise(rb_eRangeError, "bignum too big to convert into '%s'", type); } ds = BDIGITS(x); #if SIZEOF_LONG <= SIZEOF_BDIGIT @@ -5119,7 +5190,7 @@ big2ulong(VALUE x, const char *type) #else num = 0; for (i = 0; i < len; i++) { - num <<= BITSPERDIG; + num <<= BITSPERDIG; num += (unsigned long)ds[len - i - 1]; /* overflow is already checked */ } #endif @@ -5154,7 +5225,7 @@ rb_big2long(VALUE x) if (num <= 1+(unsigned long)(-(LONG_MIN+1))) return -(long)(num-1)-1; } - rb_raise(rb_eRangeError, "bignum too big to convert into `long'"); + rb_raise(rb_eRangeError, "bignum too big to convert into 'long'"); } #if HAVE_LONG_LONG @@ -5172,13 +5243,13 @@ big2ull(VALUE x, const char *type) if (len == 0) return 0; if (BIGSIZE(x) > SIZEOF_LONG_LONG) - rb_raise(rb_eRangeError, "bignum too big to convert into `%s'", type); + rb_raise(rb_eRangeError, "bignum too big to convert into '%s'", type); #if SIZEOF_LONG_LONG <= SIZEOF_BDIGIT num = (unsigned LONG_LONG)ds[0]; #else num = 0; for (i = 0; i < len; i++) { - num = BIGUP(num); + num = BIGUP(num); num += ds[len - i - 1]; } #endif @@ -5213,7 +5284,7 @@ rb_big2ll(VALUE x) if (num <= 1+(unsigned LONG_LONG)(-(LLONG_MIN+1))) return -(LONG_LONG)(num-1)-1; } - rb_raise(rb_eRangeError, "bignum too big to convert into `long long'"); + rb_raise(rb_eRangeError, "bignum too big to convert into 'long long'"); } #endif /* HAVE_LONG_LONG */ @@ -5228,23 +5299,23 @@ dbl2big(double d) double u = (d < 0)?-d:d; if (isinf(d)) { - rb_raise(rb_eFloatDomainError, d < 0 ? "-Infinity" : "Infinity"); + rb_raise(rb_eFloatDomainError, d < 0 ? "-Infinity" : "Infinity"); } if (isnan(d)) { - rb_raise(rb_eFloatDomainError, "NaN"); + rb_raise(rb_eFloatDomainError, "NaN"); } while (1.0 <= u) { - u /= (double)(BIGRAD); - i++; + u /= (double)(BIGRAD); + i++; } z = bignew(i, d>=0); digits = BDIGITS(z); while (i--) { - u *= BIGRAD; - c = (BDIGIT)u; - u -= c; - digits[i] = c; + u *= BIGRAD; + c = (BDIGIT)u; + u -= c; + digits[i] = c; } return z; @@ -5264,28 +5335,28 @@ big2dbl(VALUE x) BDIGIT *ds = BDIGITS(x), dl; if (i) { - bits = i * BITSPERDIG - nlz(ds[i-1]); - if (bits > DBL_MANT_DIG+DBL_MAX_EXP) { - d = HUGE_VAL; - } - else { - if (bits > DBL_MANT_DIG+1) - lo = (bits -= DBL_MANT_DIG+1) / BITSPERDIG; - else - bits = 0; - while (--i > lo) { - d = ds[i] + BIGRAD*d; - } - dl = ds[i]; - if (bits && (dl & ((BDIGIT)1 << (bits %= BITSPERDIG)))) { - int carry = (dl & ~(BDIGMAX << bits)) != 0; - if (!carry) { - while (i-- > 0) { - carry = ds[i] != 0; - if (carry) break; - } - } - if (carry) { + bits = i * BITSPERDIG - nlz(ds[i-1]); + if (bits > DBL_MANT_DIG+DBL_MAX_EXP) { + d = HUGE_VAL; + } + else { + if (bits > DBL_MANT_DIG+1) + lo = (bits -= DBL_MANT_DIG+1) / BITSPERDIG; + else + bits = 0; + while (--i > lo) { + d = ds[i] + BIGRAD*d; + } + dl = ds[i]; + if (bits && (dl & ((BDIGIT)1 << (bits %= BITSPERDIG)))) { + int carry = (dl & ~(BDIGMAX << bits)) != 0; + if (!carry) { + while (i-- > 0) { + carry = ds[i] != 0; + if (carry) break; + } + } + if (carry) { BDIGIT mask = BDIGMAX; BDIGIT bit = 1; mask <<= bits; @@ -5293,19 +5364,19 @@ big2dbl(VALUE x) dl &= mask; dl += bit; dl = BIGLO(dl); - if (!dl) d += 1; - } - } - d = dl + BIGRAD*d; - if (lo) { - if (lo > INT_MAX / BITSPERDIG) - d = HUGE_VAL; - else if (lo < INT_MIN / BITSPERDIG) - d = 0.0; - else - d = ldexp(d, (int)(lo * BITSPERDIG)); - } - } + if (!dl) d += 1; + } + } + d = dl + BIGRAD*d; + if (lo) { + if (lo > INT_MAX / BITSPERDIG) + d = HUGE_VAL; + else if (lo < INT_MIN / BITSPERDIG) + d = 0.0; + else + d = ldexp(d, (int)(lo * BITSPERDIG)); + } + } } if (BIGNUM_NEGATIVE_P(x)) d = -d; return d; @@ -5317,11 +5388,11 @@ rb_big2dbl(VALUE x) double d = big2dbl(x); if (isinf(d)) { - rb_warning("Bignum out of Float range"); - if (d < 0.0) - d = -HUGE_VAL; - else - d = HUGE_VAL; + rb_warning("Integer out of Float range"); + if (d < 0.0) + d = -HUGE_VAL; + else + d = HUGE_VAL; } return d; } @@ -5418,26 +5489,26 @@ VALUE rb_big_cmp(VALUE x, VALUE y) { if (FIXNUM_P(y)) { - x = bigfixize(x); + x = bigfixize(x); if (FIXNUM_P(x)) { - /* SIGNED_VALUE and Fixnum have same sign-bits, same - * order */ - SIGNED_VALUE sx = (SIGNED_VALUE)x, sy = (SIGNED_VALUE)y; - if (sx < sy) return INT2FIX(-1); - return INT2FIX(sx > sy); + /* SIGNED_VALUE and Fixnum have same sign-bits, same + * order */ + SIGNED_VALUE sx = (SIGNED_VALUE)x, sy = (SIGNED_VALUE)y; + if (sx < sy) return INT2FIX(-1); + return INT2FIX(sx > sy); } } else if (RB_BIGNUM_TYPE_P(y)) { - if (BIGNUM_SIGN(x) == BIGNUM_SIGN(y)) { - int cmp = bary_cmp(BDIGITS(x), BIGNUM_LEN(x), BDIGITS(y), BIGNUM_LEN(y)); - return INT2FIX(BIGNUM_SIGN(x) ? cmp : -cmp); - } + if (BIGNUM_SIGN(x) == BIGNUM_SIGN(y)) { + int cmp = bary_cmp(BDIGITS(x), BIGNUM_LEN(x), BDIGITS(y), BIGNUM_LEN(y)); + return INT2FIX(BIGNUM_SIGN(x) ? cmp : -cmp); + } } else if (RB_FLOAT_TYPE_P(y)) { return rb_integer_float_cmp(x, y); } else { - return rb_num_coerce_cmp(x, y, idCmp); + return rb_num_coerce_cmp(x, y, idCmp); } return INT2FIX(BIGNUM_SIGN(x) ? 1 : -1); } @@ -5456,30 +5527,30 @@ big_op(VALUE x, VALUE y, enum big_op_t op) int n; if (RB_INTEGER_TYPE_P(y)) { - rel = rb_big_cmp(x, y); + rel = rb_big_cmp(x, y); } else if (RB_FLOAT_TYPE_P(y)) { rel = rb_integer_float_cmp(x, y); } else { - ID id = 0; - switch (op) { - case big_op_gt: id = '>'; break; - case big_op_ge: id = idGE; break; - case big_op_lt: id = '<'; break; - case big_op_le: id = idLE; break; - } - return rb_num_coerce_relop(x, y, id); + ID id = 0; + switch (op) { + case big_op_gt: id = '>'; break; + case big_op_ge: id = idGE; break; + case big_op_lt: id = '<'; break; + case big_op_le: id = idLE; break; + } + return rb_num_coerce_relop(x, y, id); } if (NIL_P(rel)) return Qfalse; n = FIX2INT(rel); switch (op) { - case big_op_gt: return RBOOL(n > 0); - case big_op_ge: return RBOOL(n >= 0); - case big_op_lt: return RBOOL(n < 0); - case big_op_le: return RBOOL(n <= 0); + case big_op_gt: return RBOOL(n > 0); + case big_op_ge: return RBOOL(n >= 0); + case big_op_lt: return RBOOL(n < 0); + case big_op_le: return RBOOL(n <= 0); } return Qundef; } @@ -5523,7 +5594,7 @@ VALUE rb_big_eq(VALUE x, VALUE y) { if (FIXNUM_P(y)) { - return RBOOL(bignorm(x) == y); + return RBOOL(bignorm(x) == y); } else if (RB_BIGNUM_TYPE_P(y)) { } @@ -5531,7 +5602,7 @@ rb_big_eq(VALUE x, VALUE y) return rb_integer_float_eq(x, y); } else { - return rb_equal(y, x); + return rb_equal(y, x); } if (BIGNUM_SIGN(x) != BIGNUM_SIGN(y)) return Qfalse; if (BIGNUM_LEN(x) != BIGNUM_LEN(y)) return Qfalse; @@ -5635,13 +5706,13 @@ bigsub_int(VALUE x, long y0) zds = BDIGITS(z); #if SIZEOF_BDIGIT >= SIZEOF_LONG - assert(xn == zn); + RUBY_ASSERT(xn == zn); num = (BDIGIT_DBL_SIGNED)xds[0] - y; if (xn == 1 && num < 0) { - BIGNUM_NEGATE(z); - zds[0] = (BDIGIT)-num; - RB_GC_GUARD(x); - return bignorm(z); + BIGNUM_NEGATE(z); + zds[0] = (BDIGIT)-num; + RB_GC_GUARD(x); + return bignorm(z); } zds[0] = BIGLO(num); num = BIGDN(num); @@ -5653,10 +5724,10 @@ bigsub_int(VALUE x, long y0) num = 0; for (i=0; i < xn; i++) { if (y == 0) goto y_is_zero_x; - num += (BDIGIT_DBL_SIGNED)xds[i] - BIGLO(y); - zds[i] = BIGLO(num); - num = BIGDN(num); - y = BIGDN(y); + num += (BDIGIT_DBL_SIGNED)xds[i] - BIGLO(y); + zds[i] = BIGLO(num); + num = BIGDN(num); + y = BIGDN(y); } for (; i < zn; i++) { if (y == 0) goto y_is_zero_z; @@ -5671,9 +5742,9 @@ bigsub_int(VALUE x, long y0) for (; i < xn; i++) { y_is_zero_x: if (num == 0) goto num_is_zero_x; - num += xds[i]; - zds[i] = BIGLO(num); - num = BIGDN(num); + num += xds[i]; + zds[i] = BIGLO(num); + num = BIGDN(num); } #if SIZEOF_BDIGIT < SIZEOF_LONG for (; i < zn; i++) { @@ -5687,7 +5758,7 @@ bigsub_int(VALUE x, long y0) for (; i < xn; i++) { num_is_zero_x: - zds[i] = xds[i]; + zds[i] = xds[i]; } #if SIZEOF_BDIGIT < SIZEOF_LONG for (; i < zn; i++) { @@ -5698,10 +5769,10 @@ bigsub_int(VALUE x, long y0) goto finish; finish: - assert(num == 0 || num == -1); + RUBY_ASSERT(num == 0 || num == -1); if (num < 0) { get2comp(z); - BIGNUM_NEGATE(z); + BIGNUM_NEGATE(z); } RB_GC_GUARD(x); return bignorm(z); @@ -5744,17 +5815,17 @@ bigadd_int(VALUE x, long y) num = 0; for (i=0; i < xn; i++) { if (y == 0) goto y_is_zero_x; - num += (BDIGIT_DBL)xds[i] + BIGLO(y); - zds[i] = BIGLO(num); - num = BIGDN(num); - y = BIGDN(y); + num += (BDIGIT_DBL)xds[i] + BIGLO(y); + zds[i] = BIGLO(num); + num = BIGDN(num); + y = BIGDN(y); } for (; i < zn; i++) { if (y == 0) goto y_is_zero_z; - num += BIGLO(y); - zds[i] = BIGLO(num); - num = BIGDN(num); - y = BIGDN(y); + num += BIGLO(y); + zds[i] = BIGLO(num); + num = BIGDN(num); + y = BIGDN(y); } goto finish; @@ -5763,25 +5834,25 @@ bigadd_int(VALUE x, long y) for (;i < xn; i++) { y_is_zero_x: if (num == 0) goto num_is_zero_x; - num += (BDIGIT_DBL)xds[i]; - zds[i] = BIGLO(num); - num = BIGDN(num); + num += (BDIGIT_DBL)xds[i]; + zds[i] = BIGLO(num); + num = BIGDN(num); } for (; i < zn; i++) { y_is_zero_z: if (num == 0) goto num_is_zero_z; - zds[i] = BIGLO(num); - num = BIGDN(num); + zds[i] = BIGLO(num); + num = BIGDN(num); } goto finish; for (;i < xn; i++) { num_is_zero_x: - zds[i] = xds[i]; + zds[i] = xds[i]; } for (; i < zn; i++) { num_is_zero_z: - zds[i] = 0; + zds[i] = 0; } goto finish; @@ -5798,15 +5869,15 @@ bigadd(VALUE x, VALUE y, int sign) sign = (sign == BIGNUM_SIGN(y)); if (BIGNUM_SIGN(x) != sign) { - if (sign) return bigsub(y, x); - return bigsub(x, y); + if (sign) return bigsub(y, x); + return bigsub(x, y); } if (BIGNUM_LEN(x) > BIGNUM_LEN(y)) { - len = BIGNUM_LEN(x) + 1; + len = BIGNUM_LEN(x) + 1; } else { - len = BIGNUM_LEN(y) + 1; + len = BIGNUM_LEN(y) + 1; } z = bignew(len, sign); @@ -5823,26 +5894,26 @@ rb_big_plus(VALUE x, VALUE y) long n; if (FIXNUM_P(y)) { - n = FIX2LONG(y); - if ((n > 0) != BIGNUM_SIGN(x)) { - if (n < 0) { - n = -n; - } - return bigsub_int(x, n); - } - if (n < 0) { - n = -n; - } - return bigadd_int(x, n); + n = FIX2LONG(y); + if ((n > 0) != BIGNUM_SIGN(x)) { + if (n < 0) { + n = -n; + } + return bigsub_int(x, n); + } + if (n < 0) { + n = -n; + } + return bigadd_int(x, n); } else if (RB_BIGNUM_TYPE_P(y)) { - return bignorm(bigadd(x, y, 1)); + return bignorm(bigadd(x, y, 1)); } else if (RB_FLOAT_TYPE_P(y)) { - return DBL2NUM(rb_big2dbl(x) + RFLOAT_VALUE(y)); + return DBL2NUM(rb_big2dbl(x) + RFLOAT_VALUE(y)); } else { - return rb_num_coerce_bin(x, y, '+'); + return rb_num_coerce_bin(x, y, '+'); } } @@ -5852,26 +5923,26 @@ rb_big_minus(VALUE x, VALUE y) long n; if (FIXNUM_P(y)) { - n = FIX2LONG(y); - if ((n > 0) != BIGNUM_SIGN(x)) { - if (n < 0) { - n = -n; - } - return bigadd_int(x, n); - } - if (n < 0) { - n = -n; - } - return bigsub_int(x, n); + n = FIX2LONG(y); + if ((n > 0) != BIGNUM_SIGN(x)) { + if (n < 0) { + n = -n; + } + return bigadd_int(x, n); + } + if (n < 0) { + n = -n; + } + return bigsub_int(x, n); } else if (RB_BIGNUM_TYPE_P(y)) { - return bignorm(bigadd(x, y, 0)); + return bignorm(bigadd(x, y, 0)); } else if (RB_FLOAT_TYPE_P(y)) { - return DBL2NUM(rb_big2dbl(x) - RFLOAT_VALUE(y)); + return DBL2NUM(rb_big2dbl(x) - RFLOAT_VALUE(y)); } else { - return rb_num_coerce_bin(x, y, '-'); + return rb_num_coerce_bin(x, y, '-'); } } @@ -5883,6 +5954,8 @@ bigsq(VALUE x) BDIGIT *xds, *zds; xn = BIGNUM_LEN(x); + if (MUL_OVERFLOW_LONG_P(2, xn)) + rb_raise(rb_eArgError, "square overflow"); zn = 2 * xn; z = bignew(zn, 1); @@ -5911,6 +5984,8 @@ bigmul0(VALUE x, VALUE y) xn = BIGNUM_LEN(x); yn = BIGNUM_LEN(y); + if (ADD_OVERFLOW_LONG_P(xn, yn)) + rb_raise(rb_eArgError, "multiplication overflow"); zn = xn + yn; z = bignew(zn, BIGNUM_SIGN(x)==BIGNUM_SIGN(y)); @@ -5930,15 +6005,15 @@ VALUE rb_big_mul(VALUE x, VALUE y) { if (FIXNUM_P(y)) { - y = rb_int2big(FIX2LONG(y)); + y = rb_int2big(FIX2LONG(y)); } else if (RB_BIGNUM_TYPE_P(y)) { } else if (RB_FLOAT_TYPE_P(y)) { - return DBL2NUM(rb_big2dbl(x) * RFLOAT_VALUE(y)); + return DBL2NUM(rb_big2dbl(x) * RFLOAT_VALUE(y)); } else { - return rb_num_coerce_bin(x, y, '*'); + return rb_num_coerce_bin(x, y, '*'); } return bignorm(bigmul0(x, y)); @@ -5965,21 +6040,21 @@ bigdivrem(VALUE x, VALUE y, volatile VALUE *divp, volatile VALUE *modp) BARY_TRUNC(xds, xn); if (xn < yn || (xn == yn && xds[xn - 1] < yds[yn - 1])) { - if (divp) *divp = rb_int2big(0); - if (modp) *modp = x; - return Qnil; + if (divp) *divp = rb_int2big(0); + if (modp) *modp = x; + return Qnil; } if (yn == 1) { - dd = yds[0]; - z = bignew(xn, BIGNUM_SIGN(x)==BIGNUM_SIGN(y)); - zds = BDIGITS(z); + dd = yds[0]; + z = bignew(xn, BIGNUM_SIGN(x)==BIGNUM_SIGN(y)); + zds = BDIGITS(z); dd = bigdivrem_single(zds, xds, xn, dd); - if (modp) { - *modp = rb_uint2big((uintptr_t)dd); - BIGNUM_SET_SIGN(*modp, BIGNUM_SIGN(x)); - } - if (divp) *divp = z; - return Qnil; + if (modp) { + *modp = rb_uint2big((uintptr_t)dd); + BIGNUM_SET_SIGN(*modp, BIGNUM_SIGN(x)); + } + if (divp) *divp = z; + return Qnil; } if (xn == 2 && yn == 2) { BDIGIT_DBL x0 = bary2bdigitdbl(xds, 2); @@ -6044,11 +6119,11 @@ bigdivmod(VALUE x, VALUE y, volatile VALUE *divp, volatile VALUE *modp) bigdivrem(x, y, divp, &mod); if (BIGNUM_SIGN(x) != BIGNUM_SIGN(y) && !BIGZEROP(mod)) { - if (divp) *divp = bigadd(*divp, rb_int2big(1), 0); - if (modp) *modp = bigadd(mod, y, 1); + if (divp) *divp = bigadd(*divp, rb_int2big(1), 0); + if (modp) *modp = bigadd(mod, y, 1); } else if (modp) { - *modp = mod; + *modp = mod; } } @@ -6059,25 +6134,25 @@ rb_big_divide(VALUE x, VALUE y, ID op) VALUE z; if (FIXNUM_P(y)) { - y = rb_int2big(FIX2LONG(y)); + y = rb_int2big(FIX2LONG(y)); } else if (RB_BIGNUM_TYPE_P(y)) { } else if (RB_FLOAT_TYPE_P(y)) { - if (op == '/') { + if (op == '/') { double dx = rb_big2dbl(x); return rb_flo_div_flo(DBL2NUM(dx), y); - } - else { + } + else { VALUE v; - double dy = RFLOAT_VALUE(y); - if (dy == 0.0) rb_num_zerodiv(); + double dy = RFLOAT_VALUE(y); + if (dy == 0.0) rb_num_zerodiv(); v = rb_big_divide(x, y, '/'); return rb_dbl2big(RFLOAT_VALUE(v)); - } + } } else { - return rb_num_coerce_bin(x, y, op); + return rb_num_coerce_bin(x, y, op); } bigdivmod(x, y, &z, 0); @@ -6102,10 +6177,10 @@ rb_big_modulo(VALUE x, VALUE y) VALUE z; if (FIXNUM_P(y)) { - y = rb_int2big(FIX2LONG(y)); + y = rb_int2big(FIX2LONG(y)); } else if (!RB_BIGNUM_TYPE_P(y)) { - return rb_num_coerce_bin(x, y, '%'); + return rb_num_coerce_bin(x, y, '%'); } bigdivmod(x, y, 0, &z); @@ -6118,10 +6193,10 @@ rb_big_remainder(VALUE x, VALUE y) VALUE z; if (FIXNUM_P(y)) { - y = rb_int2big(FIX2LONG(y)); + y = rb_int2big(FIX2LONG(y)); } else if (!RB_BIGNUM_TYPE_P(y)) { - return rb_num_coerce_bin(x, y, rb_intern("remainder")); + return rb_num_coerce_bin(x, y, rb_intern("remainder")); } bigdivrem(x, y, 0, &z); @@ -6134,7 +6209,7 @@ rb_big_divmod(VALUE x, VALUE y) VALUE div, mod; if (FIXNUM_P(y)) { - y = rb_int2big(FIX2LONG(y)); + y = rb_int2big(FIX2LONG(y)); } else if (!RB_BIGNUM_TYPE_P(y)) { return rb_num_coerce_bin(x, y, idDivmod); @@ -6148,9 +6223,9 @@ static VALUE big_shift(VALUE x, long n) { if (n < 0) - return big_lshift(x, 1+(unsigned long)(-(n+1))); + return big_lshift(x, 1+(unsigned long)(-(n+1))); else if (n > 0) - return big_rshift(x, (unsigned long)n); + return big_rshift(x, (unsigned long)n); return x; } @@ -6174,9 +6249,9 @@ big_fdiv(VALUE x, VALUE y, long ey) l = ex - ey; #if SIZEOF_LONG > SIZEOF_INT { - /* Visual C++ can't be here */ - if (l > INT_MAX) return HUGE_VAL; - if (l < INT_MIN) return 0.0; + /* Visual C++ can't be here */ + if (l > INT_MAX) return HUGE_VAL; + if (l < INT_MIN) return 0.0; } #endif return ldexp(big2dbl(z), (int)l); @@ -6210,19 +6285,19 @@ rb_big_fdiv_double(VALUE x, VALUE y) dx = big2dbl(x); if (FIXNUM_P(y)) { - dy = (double)FIX2LONG(y); - if (isinf(dx)) - return big_fdiv_int(x, rb_int2big(FIX2LONG(y))); + dy = (double)FIX2LONG(y); + if (isinf(dx)) + return big_fdiv_int(x, rb_int2big(FIX2LONG(y))); } else if (RB_BIGNUM_TYPE_P(y)) { - return big_fdiv_int(x, y); + return big_fdiv_int(x, y); } else if (RB_FLOAT_TYPE_P(y)) { - dy = RFLOAT_VALUE(y); - if (isnan(dy)) - return dy; - if (isinf(dx)) - return big_fdiv_float(x, y); + dy = RFLOAT_VALUE(y); + if (isnan(dy)) + return dy; + if (isinf(dx)) + return big_fdiv_float(x, y); } else { return NUM2DBL(rb_num_coerce_bin(x, y, idFdiv)); @@ -6247,20 +6322,19 @@ rb_big_pow(VALUE x, VALUE y) if (y == INT2FIX(0)) return INT2FIX(1); if (y == INT2FIX(1)) return x; if (RB_FLOAT_TYPE_P(y)) { - d = RFLOAT_VALUE(y); - if ((BIGNUM_NEGATIVE_P(x) && !BIGZEROP(x))) { + d = RFLOAT_VALUE(y); + if ((BIGNUM_NEGATIVE_P(x) && !BIGZEROP(x))) { return rb_dbl_complex_new_polar_pi(pow(-rb_big2dbl(x), d), d); - } + } } else if (RB_BIGNUM_TYPE_P(y)) { - y = bignorm(y); - if (FIXNUM_P(y)) - goto again; - rb_warn("in a**b, b may be too big"); - d = rb_big2dbl(y); + y = bignorm(y); + if (FIXNUM_P(y)) + goto again; + rb_raise(rb_eArgError, "exponent is too large"); } else if (FIXNUM_P(y)) { - yy = FIX2LONG(y); + yy = FIX2LONG(y); if (yy < 0) { x = rb_big_pow(x, LONG2NUM(-yy)); @@ -6269,31 +6343,35 @@ rb_big_pow(VALUE x, VALUE y) else return DBL2NUM(1.0 / NUM2DBL(x)); } - else { - VALUE z = 0; - SIGNED_VALUE mask; + else { + VALUE z = 0; + SIGNED_VALUE mask; const size_t xbits = rb_absint_numwords(x, 1, NULL); - const size_t BIGLEN_LIMIT = 32*1024*1024; +#if SIZEOF_SIZE_T == 4 + const size_t BIGLEN_LIMIT = 1ULL << 31; // 2 GB +#else // SIZEOF_SIZE_T == 8 + const size_t BIGLEN_LIMIT = 1ULL << 34; // 16 GB +#endif - if (xbits == (size_t)-1 || + if (xbits == (size_t)-1 || (xbits > BIGLEN_LIMIT) || + MUL_OVERFLOW_LONG_P(yy, xbits) || (xbits * yy > BIGLEN_LIMIT)) { - rb_warn("in a**b, b may be too big"); - d = (double)yy; - } - else { - for (mask = FIXNUM_MAX + 1; mask; mask >>= 1) { - if (z) z = bigsq(z); - if (yy & mask) { - z = z ? bigtrunc(bigmul0(z, x)) : x; - } - } - return bignorm(z); - } - } + rb_raise(rb_eArgError, "exponent is too large"); + } + else { + for (mask = FIXNUM_MAX + 1; mask; mask >>= 1) { + if (z) z = bigsq(z); + if (yy & mask) { + z = z ? bigtrunc(bigmul0(z, x)) : x; + } + } + return bignorm(z); + } + } } else { - return rb_num_coerce_bin(x, y, idPow); + return rb_num_coerce_bin(x, y, idPow); } return DBL2NUM(pow(rb_big2dbl(x), d)); } @@ -6308,13 +6386,13 @@ bigand_int(VALUE x, long xn, BDIGIT hibitsx, long y) BDIGIT hibitsy; if (y == 0) return INT2FIX(0); - if (xn == 0) return hibitsx ? LONG2NUM(y) : 0; + if (xn == 0) return hibitsx ? LONG2NUM(y) : INT2FIX(0); hibitsy = 0 <= y ? 0 : BDIGMAX; xds = BDIGITS(x); #if SIZEOF_BDIGIT >= SIZEOF_LONG if (!hibitsy) { - y &= xds[0]; - return LONG2NUM(y); + y &= xds[0]; + return LONG2NUM(y); } #endif @@ -6343,10 +6421,10 @@ bigand_int(VALUE x, long xn, BDIGIT hibitsx, long y) } #endif for (;i < xn; i++) { - zds[i] = xds[i] & hibitsy; + zds[i] = xds[i] & hibitsy; } for (;i < zn; i++) { - zds[i] = hibitsx & hibitsy; + zds[i] = hibitsx & hibitsy; } twocomp2abs_bang(z, hibitsx && hibitsy); RB_GC_GUARD(x); @@ -6366,12 +6444,12 @@ rb_big_and(VALUE x, VALUE y) long tmpn; if (!RB_INTEGER_TYPE_P(y)) { - return rb_num_coerce_bit(x, y, '&'); + return rb_num_coerce_bit(x, y, '&'); } hibitsx = abs2twocomp(&x, &xn); if (FIXNUM_P(y)) { - return bigand_int(x, xn, hibitsx, FIX2LONG(y)); + return bigand_int(x, xn, hibitsx, FIX2LONG(y)); } hibitsy = abs2twocomp(&y, &yn); if (xn > yn) { @@ -6393,10 +6471,10 @@ rb_big_and(VALUE x, VALUE y) zds = BDIGITS(z); for (i=0; i<n1; i++) { - zds[i] = ds1[i] & ds2[i]; + zds[i] = ds1[i] & ds2[i]; } for (; i<n2; i++) { - zds[i] = hibits1 & ds2[i]; + zds[i] = hibits1 & ds2[i]; } twocomp2abs_bang(z, hibits1 && hibits2); RB_GC_GUARD(x); @@ -6485,12 +6563,12 @@ rb_big_or(VALUE x, VALUE y) long tmpn; if (!RB_INTEGER_TYPE_P(y)) { - return rb_num_coerce_bit(x, y, '|'); + return rb_num_coerce_bit(x, y, '|'); } hibitsx = abs2twocomp(&x, &xn); if (FIXNUM_P(y)) { - return bigor_int(x, xn, hibitsx, FIX2LONG(y)); + return bigor_int(x, xn, hibitsx, FIX2LONG(y)); } hibitsy = abs2twocomp(&y, &yn); if (xn > yn) { @@ -6512,10 +6590,10 @@ rb_big_or(VALUE x, VALUE y) zds = BDIGITS(z); for (i=0; i<n1; i++) { - zds[i] = ds1[i] | ds2[i]; + zds[i] = ds1[i] | ds2[i]; } for (; i<n2; i++) { - zds[i] = hibits1 | ds2[i]; + zds[i] = hibits1 | ds2[i]; } twocomp2abs_bang(z, hibits1 || hibits2); RB_GC_GUARD(x); @@ -6579,12 +6657,12 @@ rb_big_xor(VALUE x, VALUE y) long tmpn; if (!RB_INTEGER_TYPE_P(y)) { - return rb_num_coerce_bit(x, y, '^'); + return rb_num_coerce_bit(x, y, '^'); } hibitsx = abs2twocomp(&x, &xn); if (FIXNUM_P(y)) { - return bigxor_int(x, xn, hibitsx, FIX2LONG(y)); + return bigxor_int(x, xn, hibitsx, FIX2LONG(y)); } hibitsy = abs2twocomp(&y, &yn); if (xn > yn) { @@ -6603,10 +6681,10 @@ rb_big_xor(VALUE x, VALUE y) zds = BDIGITS(z); for (i=0; i<n1; i++) { - zds[i] = ds1[i] ^ ds2[i]; + zds[i] = ds1[i] ^ ds2[i]; } for (; i<n2; i++) { - zds[i] = hibitsx ^ ds2[i]; + zds[i] = hibitsx ^ ds2[i]; } twocomp2abs_bang(z, (hibits1 ^ hibits2) != 0); RB_GC_GUARD(x); @@ -6622,25 +6700,25 @@ rb_big_lshift(VALUE x, VALUE y) int shift_numbits; for (;;) { - if (FIXNUM_P(y)) { - long l = FIX2LONG(y); + if (FIXNUM_P(y)) { + long l = FIX2LONG(y); unsigned long shift; - if (0 <= l) { - lshift_p = 1; + if (0 <= l) { + lshift_p = 1; shift = l; } else { - lshift_p = 0; - shift = 1+(unsigned long)(-(l+1)); - } + lshift_p = 0; + shift = 1+(unsigned long)(-(l+1)); + } shift_numbits = (int)(shift & (BITSPERDIG-1)); shift_numdigits = shift >> bit_length(BITSPERDIG-1); return bignorm(big_shift3(x, lshift_p, shift_numdigits, shift_numbits)); - } - else if (RB_BIGNUM_TYPE_P(y)) { + } + else if (RB_BIGNUM_TYPE_P(y)) { return bignorm(big_shift2(x, 1, y)); - } - y = rb_to_int(y); + } + y = rb_to_int(y); } } @@ -6652,8 +6730,8 @@ rb_big_rshift(VALUE x, VALUE y) int shift_numbits; for (;;) { - if (FIXNUM_P(y)) { - long l = FIX2LONG(y); + if (FIXNUM_P(y)) { + long l = FIX2LONG(y); unsigned long shift; if (0 <= l) { lshift_p = 0; @@ -6661,16 +6739,16 @@ rb_big_rshift(VALUE x, VALUE y) } else { lshift_p = 1; - shift = 1+(unsigned long)(-(l+1)); - } + shift = 1+(unsigned long)(-(l+1)); + } shift_numbits = (int)(shift & (BITSPERDIG-1)); shift_numdigits = shift >> bit_length(BITSPERDIG-1); return bignorm(big_shift3(x, lshift_p, shift_numdigits, shift_numbits)); - } - else if (RB_BIGNUM_TYPE_P(y)) { + } + else if (RB_BIGNUM_TYPE_P(y)) { return bignorm(big_shift2(x, 0, y)); - } - y = rb_to_int(y); + } + y = rb_to_int(y); } } @@ -6684,22 +6762,22 @@ rb_big_aref(VALUE x, VALUE y) BDIGIT bit; if (RB_BIGNUM_TYPE_P(y)) { - if (BIGNUM_NEGATIVE_P(y)) - return INT2FIX(0); - bigtrunc(y); - if (BIGSIZE(y) > sizeof(size_t)) { - return BIGNUM_SIGN(x) ? INT2FIX(0) : INT2FIX(1); - } + if (BIGNUM_NEGATIVE_P(y)) + return INT2FIX(0); + bigtrunc(y); + if (BIGSIZE(y) > sizeof(size_t)) { + return BIGNUM_SIGN(x) ? INT2FIX(0) : INT2FIX(1); + } #if SIZEOF_SIZE_T <= SIZEOF_LONG - shift = big2ulong(y, "long"); + shift = big2ulong(y, "long"); #else - shift = big2ull(y, "long long"); + shift = big2ull(y, "long long"); #endif } else { - l = NUM2LONG(y); - if (l < 0) return INT2FIX(0); - shift = (size_t)l; + l = NUM2LONG(y); + if (l < 0) return INT2FIX(0); + shift = (size_t)l; } s1 = shift/BITSPERDIG; s2 = shift%BITSPERDIG; @@ -6720,6 +6798,73 @@ rb_big_aref(VALUE x, VALUE y) } VALUE +rb_big_aref2(VALUE x, VALUE beg, VALUE len) +{ + BDIGIT *xds, *vds; + VALUE v; + size_t copy_begin, xn, shift; + ssize_t begin, length, end; + bool negative_add_one; + + beg = rb_to_int(beg); + len = rb_to_int(len); + length = NUM2SSIZET(len); + begin = NUM2SSIZET(beg); + end = NUM2SSIZET(rb_int_plus(beg, len)); + shift = begin < 0 ? -begin : 0; + xn = BIGNUM_LEN(x); + xds = BDIGITS(x); + + if (length < 0) return rb_big_rshift(x, beg); + if (length == 0 || end <= 0) return INT2FIX(0); + if (begin < 0) begin = 0; + + if ((size_t)(end - 1) / BITSPERDIG >= xn) { + /* end > xn * BITSPERDIG */ + end = xn * BITSPERDIG; + } + + if ((size_t)begin / BITSPERDIG < xn) { + /* begin < xn * BITSPERDIG */ + size_t shift_bits, copy_end; + copy_begin = begin / BITSPERDIG; + shift_bits = begin % BITSPERDIG; + copy_end = (end - 1) / BITSPERDIG + 1; + v = bignew(copy_end - copy_begin, 1); + vds = BDIGITS(v); + MEMCPY(vds, xds + copy_begin, BDIGIT, copy_end - copy_begin); + negative_add_one = (vds[0] & ((1 << shift_bits) - 1)) == 0; + v = bignorm(v); + if (shift_bits) v = rb_int_rshift(v, SIZET2NUM(shift_bits)); + } + else { + /* Out of range */ + v = INT2FIX(0); + negative_add_one = false; + copy_begin = begin = end = 0; + } + + if (BIGNUM_NEGATIVE_P(x)) { + size_t mask_size = length - shift; + VALUE mask = rb_int_minus(rb_int_lshift(INT2FIX(1), SIZET2NUM(mask_size)), INT2FIX(1)); + v = rb_int_xor(v, mask); + for (size_t i = 0; negative_add_one && i < copy_begin; i++) { + if (xds[i]) negative_add_one = false; + } + if (negative_add_one) v = rb_int_plus(v, INT2FIX(1)); + v = rb_int_and(v, mask); + } + else { + size_t mask_size = (size_t)end - begin; + VALUE mask = rb_int_minus(rb_int_lshift(INT2FIX(1), SIZET2NUM(mask_size)), INT2FIX(1)); + v = rb_int_and(v, mask); + } + RB_GC_GUARD(x); + if (shift) v = rb_int_lshift(v, SSIZET2NUM(shift)); + return v; +} + +VALUE rb_big_hash(VALUE x) { st_index_t hash; @@ -6730,14 +6875,15 @@ rb_big_hash(VALUE x) /* * call-seq: - * big.coerce(numeric) -> array + * int.coerce(numeric) -> array * - * Returns an array with both a +numeric+ and a +big+ represented as Bignum - * objects. + * Returns an array with both a +numeric+ and a +int+ represented as + * Integer objects or Float objects. * - * This is achieved by converting +numeric+ to a Bignum. + * This is achieved by converting +numeric+ to an Integer or a Float. * - * A TypeError is raised if the +numeric+ is not a Fixnum or Bignum type. + * A TypeError is raised if the +numeric+ is not an Integer or a Float + * type. * * (0x3FFFFFFFFFFFFFFF+1).coerce(42) #=> [42, 4611686018427387904] */ @@ -6759,8 +6905,8 @@ VALUE rb_big_abs(VALUE x) { if (BIGNUM_NEGATIVE_P(x)) { - x = rb_big_clone(x); - BIGNUM_SET_POSITIVE_SIGN(x); + x = rb_big_clone(x); + BIGNUM_SET_POSITIVE_SIGN(x); } return x; } @@ -6834,7 +6980,7 @@ VALUE rb_big_even_p(VALUE num) { if (BIGNUM_LEN(num) != 0 && BDIGITS(num)[0] & 1) { - return Qfalse; + return Qfalse; } return Qtrue; } @@ -6852,94 +6998,36 @@ BDIGIT rb_bdigit_dbl_isqrt(BDIGIT_DBL); # define BDIGIT_DBL_TO_DOUBLE(n) (double)(n) #endif -static BDIGIT * -estimate_initial_sqrt(VALUE *xp, const size_t xn, const BDIGIT *nds, size_t len) -{ - enum {dbl_per_bdig = roomof(DBL_MANT_DIG,BITSPERDIG)}; - const int zbits = nlz(nds[len-1]); - VALUE x = *xp = bignew_1(0, xn, 1); /* division may release the GVL */ - BDIGIT *xds = BDIGITS(x); - BDIGIT_DBL d = bary2bdigitdbl(nds+len-dbl_per_bdig, dbl_per_bdig); - BDIGIT lowbits = 1; - int rshift = (int)((BITSPERDIG*2-zbits+(len&BITSPERDIG&1) - DBL_MANT_DIG + 1) & ~1); - double f; - - if (rshift > 0) { - lowbits = (BDIGIT)d & ~(~(BDIGIT)1U << rshift); - d >>= rshift; - } - else if (rshift < 0) { - d <<= -rshift; - d |= nds[len-dbl_per_bdig-1] >> (BITSPERDIG+rshift); - } - f = sqrt(BDIGIT_DBL_TO_DOUBLE(d)); - d = (BDIGIT_DBL)ceil(f); - if (BDIGIT_DBL_TO_DOUBLE(d) == f) { - if (lowbits || (lowbits = !bary_zero_p(nds, len-dbl_per_bdig))) - ++d; - } - else { - lowbits = 1; - } - rshift /= 2; - rshift += (2-(len&1))*BITSPERDIG/2; - if (rshift >= 0) { - if (nlz((BDIGIT)d) + rshift >= BITSPERDIG) { - /* (d << rshift) does cause overflow. - * example: Integer.sqrt(0xffff_ffff_ffff_ffff ** 2) - */ - d = ~(BDIGIT_DBL)0; - } - else { - d <<= rshift; - } - } - BDIGITS_ZERO(xds, xn-2); - bdigitdbl2bary(&xds[xn-2], 2, d); - - if (!lowbits) return NULL; /* special case, exact result */ - return xds; -} - VALUE rb_big_isqrt(VALUE n) { BDIGIT *nds = BDIGITS(n); size_t len = BIGNUM_LEN(n); - size_t xn = (len+1) / 2; - VALUE x; - BDIGIT *xds; if (len <= 2) { - BDIGIT sq = rb_bdigit_dbl_isqrt(bary2bdigitdbl(nds, len)); + BDIGIT sq = rb_bdigit_dbl_isqrt(bary2bdigitdbl(nds, len)); #if SIZEOF_BDIGIT > SIZEOF_LONG - return ULL2NUM(sq); + return ULL2NUM(sq); #else - return ULONG2NUM(sq); + return ULONG2NUM(sq); #endif } - else if ((xds = estimate_initial_sqrt(&x, xn, nds, len)) != 0) { - size_t tn = xn + BIGDIVREM_EXTRA_WORDS; - VALUE t = bignew_1(0, tn, 1); - BDIGIT *tds = BDIGITS(t); - tn = BIGNUM_LEN(t); - - /* t = n/x */ - while (bary_divmod_branch(tds, tn, NULL, 0, nds, len, xds, xn), - bary_cmp(tds, tn, xds, xn) < 0) { - int carry; - BARY_TRUNC(tds, tn); - /* x = (x+t)/2 */ - carry = bary_add(xds, xn, xds, xn, tds, tn); - bary_small_rshift(xds, xds, xn, 1, carry); - tn = BIGNUM_LEN(t); - } - } - RBASIC_SET_CLASS_RAW(x, rb_cInteger); - return x; + else { + size_t shift = FIX2LONG(rb_big_bit_length(n)) / 4; + VALUE n2 = rb_int_rshift(n, SIZET2NUM(2 * shift)); + VALUE x = FIXNUM_P(n2) ? LONG2FIX(rb_ulong_isqrt(FIX2ULONG(n2))) : rb_big_isqrt(n2); + /* x = (x+n/x)/2 */ + x = rb_int_plus(rb_int_lshift(x, SIZET2NUM(shift - 1)), rb_int_idiv(rb_int_rshift(n, SIZET2NUM(shift + 1)), x)); + VALUE xx = rb_int_mul(x, x); + while (rb_int_gt(xx, n)) { + xx = rb_int_minus(xx, rb_int_minus(rb_int_plus(x, x), INT2FIX(1))); + x = rb_int_minus(x, INT2FIX(1)); + } + return x; + } } -#ifdef USE_GMP +#if USE_GMP static void bary_powm_gmp(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGIT *yds, size_t yn, const BDIGIT *mds, size_t mn) { @@ -6965,7 +7053,7 @@ bary_powm_gmp(BDIGIT *zds, size_t zn, const BDIGIT *xds, size_t xn, const BDIGIT static VALUE int_pow_tmp3(VALUE x, VALUE y, VALUE m, int nega_flg) { -#ifdef USE_GMP +#if USE_GMP VALUE z; size_t xn, yn, mn, zn; @@ -6975,14 +7063,14 @@ int_pow_tmp3(VALUE x, VALUE y, VALUE m, int nega_flg) if (FIXNUM_P(y)) { y = rb_int2big(FIX2LONG(y)); } - assert(RB_BIGNUM_TYPE_P(m)); + RUBY_ASSERT(RB_BIGNUM_TYPE_P(m)); xn = BIGNUM_LEN(x); yn = BIGNUM_LEN(y); mn = BIGNUM_LEN(m); zn = mn; z = bignew(zn, 1); bary_powm_gmp(BDIGITS(z), zn, BDIGITS(x), xn, BDIGITS(y), yn, BDIGITS(m), mn); - if (nega_flg & BIGNUM_POSITIVE_P(z)) { + if (nega_flg && BIGNUM_POSITIVE_P(z) && !BIGZEROP(z)) { z = rb_big_minus(z, m); } RB_GC_GUARD(x); @@ -7010,7 +7098,7 @@ int_pow_tmp3(VALUE x, VALUE y, VALUE m, int nega_flg) x = rb_int_modulo(x, m); } - if (nega_flg && rb_int_positive_p(tmp)) { + if (nega_flg && rb_int_positive_p(tmp) && !rb_int_zero_p(tmp)) { tmp = rb_int_minus(tmp, m); } return tmp; @@ -7122,6 +7210,11 @@ rb_int_powm(int const argc, VALUE * const argv, VALUE const num) rb_raise(rb_eTypeError, "Integer#pow() 2nd argument not allowed unless all arguments are integers"); } + if (rb_int_zero_p(a) && !rb_int_zero_p(b)) { + /* shortcut; 0**x => 0 except for x == 0 */ + return INT2FIX(0); + } + if (rb_int_negative_p(m)) { m = rb_int_uminus(m); nega_flg = 1; @@ -7141,7 +7234,7 @@ rb_int_powm(int const argc, VALUE * const argv, VALUE const num) } else { if (rb_bigzero_p(m)) rb_num_zerodiv(); - if (bignorm(m) == INT2FIX(1)) return INT2FIX(0); + if (bignorm(m) == INT2FIX(1)) return INT2FIX(0); return int_pow_tmp3(rb_int_modulo(a, m), b, m, nega_flg); } } @@ -7169,13 +7262,9 @@ rb_int_powm(int const argc, VALUE * const argv, VALUE const num) void Init_Bignum(void) { - /* An obsolete class, use Integer */ - rb_define_const(rb_cObject, "Bignum", rb_cInteger); - rb_deprecate_constant(rb_cObject, "Bignum"); - rb_define_method(rb_cInteger, "coerce", rb_int_coerce, 1); -#ifdef USE_GMP +#if USE_GMP /* The version of loaded GMP. */ rb_define_const(rb_cInteger, "GMP_VERSION", rb_sprintf("GMP %s", gmp_version)); #endif @@ -1,25 +1,12 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems' -require 'rubygems/gem_runner' -require 'rubygems/exceptions' - -required_version = Gem::Requirement.new ">= 1.8.7" - -unless required_version.satisfied_by? Gem.ruby_version then - abort "Expected Ruby Version #{required_version}, is #{Gem.ruby_version}" -end - -args = ARGV.clone - -begin - Gem::GemRunner.new.run args -rescue Gem::SystemExitException => e - exit e.exit_code -end +require "rubygems/gem_runner" +Gem::GemRunner.new.run ARGV.clone diff --git a/bootstraptest/runner.rb b/bootstraptest/runner.rb index 2cd91ffd07..04de0c93b9 100755 --- a/bootstraptest/runner.rb +++ b/bootstraptest/runner.rb @@ -6,7 +6,8 @@ # Never use optparse in this file. # Never use test/unit in this file. # Never use Ruby extensions in this file. -# Maintain Ruby 1.8 compatibility for now + +$start_time = Time.now begin require 'fileutils' @@ -15,6 +16,7 @@ rescue LoadError $:.unshift File.join(File.dirname(__FILE__), '../lib') retry end +require_relative '../tool/lib/test/jobserver' if !Dir.respond_to?(:mktmpdir) # copied from lib/tmpdir.rb @@ -58,24 +60,99 @@ if !Dir.respond_to?(:mktmpdir) end end +# Configuration +bt = Struct.new(:ruby, + :verbose, + :color, + :tty, + :quiet, + :wn, + :progress, + :progress_bs, + :passed, + :failed, + :reset, + :columns, + :window_width, + :width, + :indent, + :platform, + :timeout, + :timeout_scale, + :launchable_test_reports + ) +BT = Class.new(bt) do + def indent=(n) + super + if (self.columns ||= 0) < n + $stderr.print(' ' * (n - self.columns)) + end + self.columns = indent + end + + def putc(c) + unless self.quiet + if self.window_width == nil + unless w = ENV["COLUMNS"] and (w = w.to_i) > 0 + w = 80 + end + w -= 1 + self.window_width = w + end + if self.window_width and self.columns >= self.window_width + $stderr.print "\n", " " * (self.indent ||= 0) + self.columns = indent + end + $stderr.print c + $stderr.flush + self.columns += 1 + end + end + + def wn=(wn) + unless wn == 1 + wn = Test::JobServer.max_jobs(wn > 0 ? wn : 1024, ENV.delete("MAKEFLAGS")) || wn + if wn <= 0 + require 'etc' + wn = [Etc.nprocessors / 2, 1].max + end + end + super wn + end + + def apply_timeout_scale(timeout) + timeout&.*(timeout_scale) + end +end.new + +BT_STATE = Struct.new(:count, :error).new + def main - @ruby = File.expand_path('miniruby') - @verbose = false + BT.ruby = File.expand_path('miniruby') + BT.verbose = false $VERBOSE = false $stress = false - @color = nil - @tty = nil - @quiet = false + BT.color = nil + BT.tty = nil + BT.quiet = false + BT.timeout = 180 + BT.timeout_scale = 1 + if (ts = (ENV["RUBY_TEST_TIMEOUT_SCALE"] || ENV["RUBY_TEST_SUBPROCESS_TIMEOUT_SCALE"]).to_i) > 1 + BT.timeout_scale *= ts + end + + # BT.wn = 1 dir = nil quiet = false tests = nil ARGV.delete_if {|arg| case arg when /\A--ruby=(.*)/ - @ruby = $1 - @ruby.gsub!(/^([^ ]*)/){File.expand_path($1)} - @ruby.gsub!(/(\s+-I\s*)((?!(?:\.\/)*-(?:\s|\z))\S+)/){$1+File.expand_path($2)} - @ruby.gsub!(/(\s+-r\s*)(\.\.?\/\S+)/){$1+File.expand_path($2)} + ruby = $1 + ruby.gsub!(/^([^ ]*)/){File.expand_path($1)} + ruby.gsub!(/(\s+-I\s*)((?!(?:\.\/)*-(?:\s|\z))\S+)/){$1+File.expand_path($2)} + ruby.gsub!(/(\s+-r\s*)(\.\.?\/\S+)/){$1+File.expand_path($2)} + BT.ruby = ruby true when /\A--sets=(.*)/ tests = Dir.glob("#{File.dirname($0)}/test_{#{$1}}*.rb").sort @@ -88,18 +165,27 @@ def main $stress = true when /\A--color(?:=(?:always|(auto)|(never)|(.*)))?\z/ warn "unknown --color argument: #$3" if $3 - @color = $1 ? nil : !$2 + BT.color = color = $1 ? nil : !$2 true when /\A--tty(=(?:yes|(no)|(.*)))?\z/ warn "unknown --tty argument: #$3" if $3 - @tty = !$1 || !$2 + BT.tty = !$1 || !$2 true - when /\A(-q|--q(uiet))\z/ + when /\A(-q|--q(uiet)?)\z/ quiet = true - @quiet = true + BT.quiet = true + true + when /\A-j(\d+)?/ + BT.wn = $1.to_i + true + when /\A--timeout=(\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?)(?::(\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?))?/ + BT.timeout = $1.to_f + BT.timeout_scale = $2.to_f if defined?($2) + true + when /\A(-v|--v(erbose)?)\z/ + BT.verbose = true + BT.quiet = false true - when /\A(-v|--v(erbose))\z/ - @verbose = true when /\A(-h|--h(elp)?)\z/ puts(<<-End) Usage: #{File.basename($0, '.*')} --ruby=PATH [--sets=NAME,NAME,...] @@ -108,6 +194,7 @@ Usage: #{File.basename($0, '.*')} --ruby=PATH [--sets=NAME,NAME,...] default: /tmp/bootstraptestXXXXX.tmpwd --color[=WHEN] Colorize the output. WHEN defaults to 'always' or can be 'never' or 'auto'. + --timeout=TIMEOUT Default timeout in seconds. -s, --stress stress test. -v, --verbose Output test name before exec. -q, --quiet Don\'t print header message. @@ -116,27 +203,53 @@ End exit true when /\A-j/ true + when /--launchable-test-reports=(.*)/ + if File.exist?($1) + # To protect files from overwritten, do nothing when the file exists. + return true + end + + begin + require_relative '../tool/lib/launchable' + rescue LoadError + # The following error sometimes happens, so we're going to skip writing Launchable report files in this case. + # + # ``` + # /tmp/tmp.bISss9CtXZ/.ext/common/json/ext.rb:15:in 'Kernel#require': + # /tmp/tmp.bISss9CtXZ/.ext/x86_64-linux/json/ext/parser.so: + # undefined symbol: ruby_abi_version - ruby_abi_version (LoadError) + # ``` + # + return true + end + BT.launchable_test_reports = writer = Launchable::JsonStreamWriter.new($1) + writer.write_array('testCases') + at_exit { + writer.close + } + true else false end } if tests and not ARGV.empty? - $stderr.puts "--tests and arguments are exclusive" - exit false + abort "--sets and arguments are exclusive" end tests ||= ARGV tests = Dir.glob("#{File.dirname($0)}/test_*.rb").sort if tests.empty? - pathes = tests.map {|path| File.expand_path(path) } + paths = tests.map {|path| File.expand_path(path) } - @progress = %w[- \\ | /] - @progress_bs = "\b" * @progress[0].size - @tty = $stderr.tty? if @tty.nil? - case @color + BT.progress = %w[- \\ | /] + BT.progress_bs = "\b" * BT.progress[0].size + BT.tty = $stderr.tty? if BT.tty.nil? + BT.wn ||= /-j(\d+)?/ =~ (ENV["MAKEFLAGS"] || ENV["MFLAGS"]) ? $1.to_i : 1 + + case BT.color when nil - @color = @tty && /dumb/ !~ ENV["TERM"] + BT.color = BT.tty && /dumb/ !~ ENV["TERM"] end - @tty &&= !@verbose - if @color + BT.tty &&= !BT.verbose + if BT.color # dircolors-like style colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {} begin @@ -145,203 +258,459 @@ End end rescue end - @passed = "\e[;#{colors["pass"] || "32"}m" - @failed = "\e[;#{colors["fail"] || "31"}m" - @reset = "\e[m" + BT.passed = "\e[;#{colors["pass"] || "32"}m" + BT.failed = "\e[;#{colors["fail"] || "31"}m" + BT.reset = "\e[m" else - @passed = @failed = @reset = "" + BT.passed = BT.failed = BT.reset = "" end + target_version = `#{BT.ruby} -v`.chomp + BT.platform = target_version[/\[(.*)\]\z/, 1] unless quiet - puts Time.now + puts $start_time if defined?(RUBY_DESCRIPTION) puts "Driver is #{RUBY_DESCRIPTION}" elsif defined?(RUBY_PATCHLEVEL) - puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}#{RUBY_PLATFORM}) [#{RUBY_PLATFORM}]" + puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}#{RUBY_PATCHLEVEL}) [#{RUBY_PLATFORM}]" else puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]" end - puts "Target is #{`#{@ruby} -v`.chomp}" + puts "Target is #{target_version}" puts $stdout.flush end - in_temporary_working_directory(dir) { - exec_test pathes - } + in_temporary_working_directory(dir) do + exec_test paths + end end def erase(e = true) - if e and @columns > 0 and @tty and !@verbose + if e and BT.columns > 0 and BT.tty and !BT.verbose "\e[1K\r" else "" end end -def exec_test(pathes) - @count = 0 - @error = 0 - @errbuf = [] - @location = nil - @columns = 0 - @width = pathes.map {|path| File.basename(path).size}.max + 2 - pathes.each do |path| - @basename = File.basename(path) - $stderr.printf("%s%-*s ", erase(@quiet), @width, @basename) - $stderr.flush - @columns = @width + 1 - $stderr.puts if @verbose - count = @count - error = @error +def load_test paths + paths.each do |path| load File.expand_path(path) - if @tty - if @error == error - msg = "PASS #{@count-count}" - @columns += msg.size - 1 - $stderr.print "#{@progress_bs}#{@passed}#{msg}#{@reset}" + end +end + +def concurrent_exec_test + aq = Queue.new + rq = Queue.new + + ts = BT.wn.times.map do + Thread.new do + while as = aq.pop + as.call + rq << as + end + ensure + rq << nil + end + end + + Assertion.all.to_a.shuffle.each do |path, assertions| + assertions.each do |as| + aq << as + end + end + + BT.indent = 1 + aq.close + i = 1 + term_wn = 0 + begin + while BT.wn != term_wn + if r = rq.pop + BT_STATE.count += 1 + case + when BT.quiet + when BT.tty + $stderr.print "#{BT.progress_bs}#{BT.progress[(i+=1) % BT.progress.size]}" + else + BT.putc '.' + end else - msg = "FAIL #{@error-error}/#{@count-count}" - $stderr.print "#{@progress_bs}#{@failed}#{msg}#{@reset}" - @columns = 0 + term_wn += 1 + end + end + ensure + ts.each(&:kill) + ts.each(&:join) + end +end + +## +# Module for writing a test file for uploading test results into Launchable. +# In bootstraptest, we aggregate the test results based on file level. +module Launchable + @@last_test_name = nil + @@failure_log = '' + @@duration = 0 + + def show_progress(message = '') + faildesc, t = super + + if writer = BT.launchable_test_reports + if faildesc + @@failure_log += faildesc end + repo_path = File.expand_path("#{__dir__}/../") + relative_path = File.join(__dir__, self.path).delete_prefix("#{repo_path}/") + if @@last_test_name != nil && @@last_test_name != relative_path + # The test path is a URL-encoded representation. + # https://github.com/launchableinc/cli/blob/v1.81.0/launchable/testpath.py#L18 + test_path = "#{encode_test_path_component("file")}=#{encode_test_path_component(@@last_test_name)}" + if @@failure_log.size > 0 + status = 'TEST_FAILED' + else + status = 'TEST_PASSED' + end + writer.write_object( + { + testPath: test_path, + status: status, + duration: t, + createdAt: Time.now.to_s, + stderr: @@failure_log, + stdout: nil, + data: { + lineNumber: self.lineno + } + } + ) + @@duration = 0 + @@failure_log = '' + end + @@last_test_name = relative_path + @@duration += t + end + end + + private + def encode_test_path_component component + component.to_s.gsub('%', '%25').gsub('=', '%3D').gsub('#', '%23').gsub('&', '%26') + end +end + +def exec_test(paths) + # setup + load_test paths + BT_STATE.count = 0 + BT_STATE.error = 0 + BT.columns = 0 + BT.width = paths.map {|path| File.basename(path).size}.max + 2 + + # execute tests + if BT.wn > 1 + concurrent_exec_test + else + prev_basename = nil + Assertion.all.each do |basename, assertions| + if !BT.quiet && basename != prev_basename + prev_basename = basename + $stderr.printf("%s%-*s ", erase(BT.quiet), BT.width, basename) + $stderr.flush + end + BT.columns = BT.width + 1 + $stderr.puts if BT.verbose + count = BT_STATE.count + error = BT_STATE.error + + assertions.each do |assertion| + BT_STATE.count += 1 + assertion.call + end + + if BT.tty + if BT_STATE.error == error + msg = "PASS #{BT_STATE.count-count}" + BT.columns += msg.size - 1 + $stderr.print "#{BT.progress_bs}#{BT.passed}#{msg}#{BT.reset}" unless BT.quiet + else + msg = "FAIL #{BT_STATE.error-error}/#{BT_STATE.count-count}" + $stderr.print "#{BT.progress_bs}#{BT.failed}#{msg}#{BT.reset}" + BT.columns = 0 + end + end + $stderr.puts if !BT.quiet and (BT.tty or BT_STATE.error == error) end - $stderr.puts unless @quiet and @tty and @error == error end - $stderr.print(erase) if @quiet - @errbuf.each do |msg| + + # show results + unless BT.quiet + $stderr.puts(erase) + + sec = Time.now - $start_time + $stderr.puts "Finished in #{'%.2f' % sec} sec\n\n" if Assertion.count > 0 + end + + Assertion.errbuf.each do |msg| $stderr.puts msg end - if @error == 0 - if @count == 0 - $stderr.puts "No tests, no problem" + + out = BT.quiet ? $stdout : $stderr + + if BT_STATE.error == 0 + if Assertion.count == 0 + out.puts "No tests, no problem" unless BT.quiet else - $stderr.puts "#{@passed}PASS#{@reset} all #{@count} tests" + out.puts "#{BT.passed}PASS#{BT.reset} all #{Assertion.count} tests" end - exit true + true else - $stderr.puts "#{@failed}FAIL#{@reset} #{@error}/#{@count} tests failed" - exit false + $stderr.puts "#{BT.failed}FAIL#{BT.reset} #{BT_STATE.error}/#{BT_STATE.count} tests failed" + false end end -def show_progress(message = '') - if @verbose - $stderr.print "\##{@count} #{@location} " - elsif @tty - $stderr.print "#{@progress_bs}#{@progress[@count % @progress.size]}" - end - t = Time.now if @verbose - faildesc, errout = with_stderr {yield} - t = Time.now - t if @verbose - if !faildesc - if @tty - $stderr.print "#{@progress_bs}#{@progress[@count % @progress.size]}" - elsif @verbose - $stderr.printf(". %.3f\n", t) - else - $stderr.print '.' +def target_platform + BT.platform or RUBY_PLATFORM +end + +class Assertion < Struct.new(:src, :path, :lineno, :proc) + prepend Launchable + @count = 0 + @all = Hash.new{|h, k| h[k] = []} + @errbuf = [] + + class << self + attr_reader :count, :errbuf + + def all + @all end - else - $stderr.print "#{@failed}F" - $stderr.printf(" %.3f", t) if @verbose - $stderr.print @reset - $stderr.puts if @verbose - error faildesc, message - unless errout.empty? - $stderr.print "#{@failed}stderr output is not empty#{@reset}\n", adjust_indent(errout) + + def add as + @all[as.path] << as + as.id = (@count += 1) end - if @tty and !@verbose - $stderr.printf("%-*s%s", @width, @basename, @progress[@count % @progress.size]) + end + + attr_accessor :id + attr_reader :err, :category + + def initialize(*args) + super + self.class.add self + @category = self.path[/\Atest_(.+)\.rb\z/, 1] + end + + def call + self.proc.call self + end + + def assert_check(message = '', opt = '', **argh) + show_progress(message) { + result = get_result_string(opt, **argh) + yield(result) + } + end + + def with_stderr + out = err = nil + r, w = IO.pipe + @err = w + err_reader = Thread.new{ r.read } + + begin + out = yield + ensure + w.close + err = err_reader.value + r.close rescue nil end + + return out, err end -rescue Interrupt - $stderr.puts "\##{@count} #{@location}" - raise -rescue Exception => err - $stderr.print 'E' - $stderr.puts if @verbose - error err.message, message -ensure - begin - check_coredump - rescue CoreDumpError => err + + def show_error(msg, additional_message) + msg = "#{BT.failed}\##{self.id} #{self.path}:#{self.lineno}#{BT.reset}: #{msg} #{additional_message}" + if BT.tty + $stderr.puts "#{erase}#{msg}" + else + Assertion.errbuf << msg + end + BT_STATE.error += 1 + end + + + def show_progress(message = '') + if BT.quiet || BT.wn > 1 + # do nothing + elsif BT.verbose + $stderr.print "\##{@id} #{self.path}:#{self.lineno} " + elsif BT.tty + $stderr.print "#{BT.progress_bs}#{BT.progress[BT_STATE.count % BT.progress.size]}" + end + + t = Time.now if BT.verbose || BT.launchable_test_reports + faildesc, errout = with_stderr {yield} + t = Time.now - t if BT.verbose || BT.launchable_test_reports + + if !faildesc + # success + if BT.quiet || BT.wn > 1 + # do nothing + elsif BT.tty + $stderr.print "#{BT.progress_bs}#{BT.progress[BT_STATE.count % BT.progress.size]}" + elsif BT.verbose + $stderr.printf(". %.3f\n", t) + else + BT.putc '.' + end + else + $stderr.print "#{BT.failed}F" + $stderr.printf(" %.3f", t) if BT.verbose + $stderr.print BT.reset + $stderr.puts if BT.verbose + show_error faildesc, message + unless errout.empty? + $stderr.print "#{BT.failed}stderr output is not empty#{BT.reset}\n", adjust_indent(errout) + end + + if BT.tty and !BT.verbose and BT.wn == 1 + $stderr.printf("%-*s%s", BT.width, path, BT.progress[BT_STATE.count % BT.progress.size]) + end + end + + [faildesc, t] + rescue Interrupt + $stderr.puts "\##{@id} #{path}:#{lineno}" + raise + rescue Exception => err $stderr.print 'E' - $stderr.puts if @verbose - error err.message, message + $stderr.puts if BT.verbose + show_error err.message, message + ensure + begin + check_coredump + rescue CoreDumpError => err + $stderr.print 'E' + $stderr.puts if BT.verbose + show_error err.message, message + cleanup_coredump + end end -end -def show_limit(testsrc, opt = '', **argh) - result = get_result_string(testsrc, opt, **argh) - if @tty and @verbose - $stderr.puts ".{#@reset}\n#{erase}#{result}" - else - @errbuf.push result + class Timeout < StandardError; end + + def get_result_string(opt = '', timeout: BT.timeout, **argh) + if BT.ruby + timeout = BT.apply_timeout_scale(timeout) + filename = make_srcfile(**argh) + begin + kw = self.err ? {err: self.err} : {} + out = IO.popen("#{BT.ruby} -W0 #{opt} #{filename}", **kw) + pid = out.pid + th = Thread.new {out.read.tap {Process.waitpid(pid); out.close}} + if th.join(timeout) + th.value + else + Timeout.new("timed out after #{timeout} seconds") + end + ensure + raise Interrupt if $? and $?.signaled? && $?.termsig == Signal.list["INT"] + + begin + Process.kill :KILL, pid + rescue Errno::ESRCH + # OK + end + end + else + eval(src).to_s + end + end + + def make_srcfile(frozen_string_literal: nil) + filename = "bootstraptest.#{self.path}_#{self.lineno}_#{self.id}.rb" + File.open(filename, 'w') {|f| + f.puts "#frozen_string_literal:#{frozen_string_literal}" unless frozen_string_literal.nil? + if $stress + f.puts "GC.stress = true" if $stress + else + f.puts "" + end + f.puts "class BT_Skip < Exception; end; def skip(msg) = raise(BT_Skip, msg.to_s)" + f.puts "print(begin; #{self.src}; rescue BT_Skip; $!.message; end)" + } + filename end end -def assert_check(testsrc, message = '', opt = '', **argh) - show_progress(message) { - result = get_result_string(testsrc, opt, **argh) - yield(result) - } +def add_assertion src, pr + loc = caller_locations(2, 1).first + lineno = loc.lineno + path = File.basename(loc.path) + + Assertion.new(src, path, lineno, pr) end -def assert_equal(expected, testsrc, message = '', opt = '', **argh) - newtest - assert_check(testsrc, message, opt, **argh) {|result| - if expected == result - nil - else - desc = "#{result.inspect} (expected #{expected.inspect})" - pretty(testsrc, desc, result) - end - } +def assert_equal(expected, testsrc, message = '', opt = '', **kwargs) + add_assertion testsrc, -> as do + as.assert_check(message, opt, **kwargs) {|result| + if expected == result + nil + else + desc = "#{result.inspect} (expected #{expected.inspect})" + pretty(testsrc, desc, result) + end + } + end end -def assert_match(expected_pattern, testsrc, message = '') - newtest - assert_check(testsrc, message) {|result| - if expected_pattern =~ result - nil - else - desc = "#{expected_pattern.inspect} expected to be =~\n#{result.inspect}" - pretty(testsrc, desc, result) - end - } +def assert_match(expected_pattern, testsrc, message = '', **argh) + add_assertion testsrc, -> as do + as.assert_check(message, **argh) {|result| + if expected_pattern =~ result + nil + else + desc = "#{expected_pattern.inspect} expected to be =~\n#{result.inspect}" + pretty(testsrc, desc, result) + end + } + end end def assert_not_match(unexpected_pattern, testsrc, message = '') - newtest - assert_check(testsrc, message) {|result| - if unexpected_pattern !~ result - nil - else - desc = "#{unexpected_pattern.inspect} expected to be !~\n#{result.inspect}" - pretty(testsrc, desc, result) - end - } + add_assertion testsrc, -> as do + as.assert_check(message) {|result| + if unexpected_pattern !~ result + nil + else + desc = "#{unexpected_pattern.inspect} expected to be !~\n#{result.inspect}" + pretty(testsrc, desc, result) + end + } + end end def assert_valid_syntax(testsrc, message = '') - newtest - assert_check(testsrc, message, '-c') {|result| - result if /Syntax OK/ !~ result - } + add_assertion testsrc, -> as do + as.assert_check(message, '-c') {|result| + result if /Syntax OK/ !~ result + } + end end -def assert_normal_exit(testsrc, *rest, timeout: nil, **opt) - newtest - message, ignore_signals = rest - message ||= '' - show_progress(message) { - faildesc = nil - filename = make_srcfile(testsrc) - old_stderr = $stderr.dup - timeout_signaled = false - begin - $stderr.reopen("assert_normal_exit.log", "w") - io = IO.popen("#{@ruby} -W0 #{filename}") +def assert_normal_exit(testsrc, *rest, timeout: BT.timeout, **opt) + add_assertion testsrc, -> as do + timeout = BT.apply_timeout_scale(timeout) + message, ignore_signals = rest + message ||= '' + as.show_progress(message) { + faildesc = nil + filename = as.make_srcfile + timeout_signaled = false + logfile = "assert_normal_exit.#{as.path}.#{as.lineno}.log" + + io = IO.popen("#{BT.ruby} -W0 #{filename}", err: logfile) pid = io.pid th = Thread.new { io.read @@ -353,83 +722,93 @@ def assert_normal_exit(testsrc, *rest, timeout: nil, **opt) timeout_signaled = true end status = th.value - ensure - $stderr.reopen(old_stderr) - old_stderr.close - end - if status && status.signaled? - signo = status.termsig - signame = Signal.list.invert[signo] - unless ignore_signals and ignore_signals.include?(signame) - sigdesc = "signal #{signo}" - if signame - sigdesc = "SIG#{signame} (#{sigdesc})" - end - if timeout_signaled - sigdesc << " (timeout)" - end - faildesc = pretty(testsrc, "killed by #{sigdesc}", nil) - stderr_log = File.read("assert_normal_exit.log") - if !stderr_log.empty? - faildesc << "\n" if /\n\z/ !~ faildesc - stderr_log << "\n" if /\n\z/ !~ stderr_log - stderr_log.gsub!(/^.*\n/) { '| ' + $& } - faildesc << stderr_log + + if status && status.signaled? + signo = status.termsig + signame = Signal.list.invert[signo] + unless ignore_signals and ignore_signals.include?(signame) + sigdesc = "signal #{signo}" + if signame + sigdesc = "SIG#{signame} (#{sigdesc})" + end + if timeout_signaled + sigdesc << " (timeout)" + end + faildesc = pretty(testsrc, "killed by #{sigdesc}", nil) + stderr_log = File.read(logfile) + if !stderr_log.empty? + faildesc << "\n" if /\n\z/ !~ faildesc + stderr_log << "\n" if /\n\z/ !~ stderr_log + stderr_log.gsub!(/^.*\n/) { '| ' + $& } + faildesc << stderr_log + end end end - end - faildesc - } + faildesc + } + end end def assert_finish(timeout_seconds, testsrc, message = '') - if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # for --jit-wait - timeout_seconds *= 3 - end - newtest - show_progress(message) { - faildesc = nil - filename = make_srcfile(testsrc) - io = IO.popen("#{@ruby} -W0 #{filename}") - pid = io.pid - waited = false - tlimit = Time.now + timeout_seconds - diff = timeout_seconds - while diff > 0 - if Process.waitpid pid, Process::WNOHANG - waited = true - break - end - if io.respond_to?(:read_nonblock) - if IO.select([io], nil, nil, diff) - begin - io.read_nonblock(1024) - rescue Errno::EAGAIN, IO::WaitReadable, EOFError - break - end while true + add_assertion testsrc, -> as do + timeout_seconds = BT.apply_timeout_scale(timeout_seconds) + + as.show_progress(message) { + faildesc = nil + filename = as.make_srcfile + io = IO.popen("#{BT.ruby} -W0 #{filename}", err: as.err) + pid = io.pid + waited = false + tlimit = Time.now + timeout_seconds + diff = timeout_seconds + while diff > 0 + if Process.waitpid pid, Process::WNOHANG + waited = true + break end - else - sleep 0.1 + if io.respond_to?(:read_nonblock) + if IO.select([io], nil, nil, diff) + begin + io.read_nonblock(1024) + rescue Errno::EAGAIN, IO::WaitReadable, EOFError + break + end while true + end + else + sleep 0.1 + end + diff = tlimit - Time.now end - diff = tlimit - Time.now - end - if !waited - Process.kill(:KILL, pid) - Process.waitpid pid - faildesc = pretty(testsrc, "not finished in #{timeout_seconds} seconds", nil) - end - io.close - faildesc - } + if !waited + Process.kill(:KILL, pid) + Process.waitpid pid + faildesc = pretty(testsrc, "not finished in #{timeout_seconds} seconds", nil) + end + io.close + faildesc + } + end end def flunk(message = '') - newtest - show_progress('') { message } + add_assertion '', -> as do + as.show_progress('') { message } + end +end + +def show_limit(testsrc, opt = '', **argh) + return if BT.quiet + + add_assertion testsrc, -> as do + result = as.get_result_string(opt, **argh) + Assertion.errbuf << result + end end def pretty(src, desc, result) src = src.sub(/\A\s*\n/, '') + lines = src.lines + src = lines[0..20].join + "(...snip)\n" if lines.size > 20 (/\n/ =~ src ? "\n#{adjust_indent(src)}" : src) + " #=> #{desc}" end @@ -443,66 +822,6 @@ def untabify(str) str.gsub(/^\t+/) {' ' * (8 * $&.size) } end -def make_srcfile(src, frozen_string_literal: nil) - filename = 'bootstraptest.tmp.rb' - File.open(filename, 'w') {|f| - f.puts "#frozen_string_literal:true" if frozen_string_literal - f.puts "GC.stress = true" if $stress - f.puts "print(begin; #{src}; end)" - } - filename -end - -def get_result_string(src, opt = '', **argh) - if @ruby - filename = make_srcfile(src, **argh) - begin - `#{@ruby} -W0 #{opt} #{filename}` - ensure - raise Interrupt if $? and $?.signaled? && $?.termsig == Signal.list["INT"] - end - else - eval(src).to_s - end -end - -def with_stderr - out = err = nil - begin - r, w = IO.pipe - stderr = $stderr.dup - $stderr.reopen(w) - w.close - reader = Thread.start {r.read} - begin - out = yield - ensure - $stderr.reopen(stderr) - err = reader.value - end - ensure - w.close rescue nil - r.close rescue nil - end - return out, err -end - -def newtest - @location = File.basename(caller(2).first) - @count += 1 - cleanup_coredump -end - -def error(msg, additional_message) - msg = "#{@failed}\##{@count} #{@location}#{@reset}: #{msg} #{additional_message}" - if @tty - $stderr.puts "#{erase}#{msg}" - else - @errbuf.push msg - end - @error += 1 -end - def in_temporary_working_directory(dir) if dir Dir.mkdir dir @@ -530,21 +849,29 @@ def cleanup_coredump core_path = "/tmp/bootstraptest-core.#{Time.now.utc.iso8601}" warn "A core file is found. Saving it at: #{core_path.dump}" FileUtils.mv('core', core_path) - cmd = ['gdb', @ruby, '-c', core_path, '-ex', 'bt', '-batch'] + cmd = ['gdb', BT.ruby, '-c', core_path, '-ex', 'bt', '-batch'] p cmd # debugging why it's not working system(*cmd) end FileUtils.rm_f Dir.glob('core.*') - FileUtils.rm_f @ruby+'.stackdump' if @ruby + FileUtils.rm_f BT.ruby+'.stackdump' if BT.ruby end class CoreDumpError < StandardError; end def check_coredump if File.file?('core') or not Dir.glob('core.*').empty? or - (@ruby and File.exist?(@ruby+'.stackdump')) + (BT.ruby and File.exist?(BT.ruby+'.stackdump')) raise CoreDumpError, "core dumped" end end -main +def yjit_enabled? + ENV.key?('RUBY_YJIT_ENABLE') || ENV.fetch('RUN_OPTS', '').include?('yjit') || BT.ruby.include?('yjit') +end + +def zjit_enabled? + ENV.key?('RUBY_ZJIT_ENABLE') || ENV.fetch('RUN_OPTS', '').include?('zjit') || BT.ruby.include?('zjit') +end + +exit main diff --git a/bootstraptest/test_attr.rb b/bootstraptest/test_attr.rb index 721a847145..3cb9d3eb39 100644 --- a/bootstraptest/test_attr.rb +++ b/bootstraptest/test_attr.rb @@ -34,3 +34,19 @@ assert_equal %{ok}, %{ print "ok" end }, '[ruby-core:15120]' + +assert_equal %{ok}, %{ + class Big + attr_reader :foo + def initialize + @foo = "ok" + end + end + + obj = Big.new + 100.times do |i| + obj.instance_variable_set(:"@ivar_\#{i}", i) + end + + Big.new.foo +} diff --git a/bootstraptest/test_autoload.rb b/bootstraptest/test_autoload.rb index a9f8e6dacd..de66f1f3ee 100644 --- a/bootstraptest/test_autoload.rb +++ b/bootstraptest/test_autoload.rb @@ -1,7 +1,7 @@ assert_equal 'ok', %q{ - File.unlink('zzz.rb') if File.file?('zzz.rb') + File.unlink('zzz1.rb') if File.file?('zzz1.rb') instance_eval do - autoload :ZZZ, './zzz.rb' + autoload :ZZZ, './zzz1.rb' begin ZZZ rescue LoadError @@ -11,9 +11,9 @@ assert_equal 'ok', %q{ }, '[ruby-dev:43816]' assert_equal 'ok', %q{ - open('zzz.rb', 'w') {|f| f.puts '' } + File.write('zzz2.rb', '') instance_eval do - autoload :ZZZ, './zzz.rb' + autoload :ZZZ, './zzz2.rb' begin ZZZ rescue NameError @@ -23,29 +23,29 @@ assert_equal 'ok', %q{ }, '[ruby-dev:43816]' assert_equal 'ok', %q{ - open('zzz.rb', 'w') {|f| f.puts 'class ZZZ; def self.ok;:ok;end;end'} + File.write('zzz3.rb', "class ZZZ; def self.ok;:ok;end;end\n") instance_eval do - autoload :ZZZ, './zzz.rb' + autoload :ZZZ, './zzz3.rb' ZZZ.ok end }, '[ruby-dev:43816]' assert_equal 'ok', %q{ - open("zzz.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"} - autoload :ZZZ, "./zzz.rb" + File.write("zzz4.rb", "class ZZZ; def self.ok;:ok;end;end\n") + autoload :ZZZ, "./zzz4.rb" ZZZ.ok } assert_equal 'ok', %q{ - open("zzz.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"} - autoload :ZZZ, "./zzz.rb" - require "./zzz.rb" + File.write("zzz5.rb", "class ZZZ; def self.ok;:ok;end;end\n") + autoload :ZZZ, "./zzz5.rb" + require "./zzz5.rb" ZZZ.ok } assert_equal 'okok', %q{ - open("zzz.rb", "w") {|f| f.puts "class ZZZ; def self.ok;:ok;end;end"} - autoload :ZZZ, "./zzz.rb" + File.write("zzz6.rb", "class ZZZ; def self.ok;:ok;end;end\n") + autoload :ZZZ, "./zzz6.rb" t1 = Thread.new {ZZZ.ok} t2 = Thread.new {ZZZ.ok} [t1.value, t2.value].join @@ -60,9 +60,9 @@ assert_finish 5, %q{ }, '[ruby-core:21696]' assert_equal 'A::C', %q{ - open("zzz.rb", "w") {} + File.write("zzz7.rb", "") class A - autoload :C, "./zzz" + autoload :C, "./zzz7" class C end C diff --git a/bootstraptest/test_constant_cache.rb b/bootstraptest/test_constant_cache.rb new file mode 100644 index 0000000000..1fa83256ed --- /dev/null +++ b/bootstraptest/test_constant_cache.rb @@ -0,0 +1,187 @@ +# Constant lookup is cached. +assert_equal '1', %q{ + CONST = 1 + + def const + CONST + end + + const + const +} + +# Invalidate when a constant is set. +assert_equal '2', %q{ + CONST = 1 + + def const + CONST + end + + const + + CONST = 2 + + const +} + +# Invalidate when a constant of the same name is set. +assert_equal '1', %q{ + CONST = 1 + + def const + CONST + end + + const + + class Container + CONST = 2 + end + + const +} + +# Invalidate when a constant is removed. +assert_equal 'missing', %q{ + class Container + CONST = 1 + + def const + CONST + end + + def self.const_missing(name) + 'missing' + end + + new.const + remove_const :CONST + end + + Container.new.const +} + +# Invalidate when a constant's visibility changes. +assert_equal 'missing', %q{ + class Container + CONST = 1 + + def self.const_missing(name) + 'missing' + end + end + + def const + Container::CONST + end + + const + + Container.private_constant :CONST + + const +} + +# Invalidate when a constant's visibility changes even if the call to the +# visibility change method fails. +assert_equal 'missing', %q{ + class Container + CONST1 = 1 + + def self.const_missing(name) + 'missing' + end + end + + def const1 + Container::CONST1 + end + + const1 + + begin + Container.private_constant :CONST1, :CONST2 + rescue NameError + end + + const1 +} + +# Invalidate when a module is included. +assert_equal 'INCLUDE', %q{ + module Include + CONST = :INCLUDE + end + + class Parent + CONST = :PARENT + end + + class Child < Parent + def const + CONST + end + + new.const + + include Include + end + + Child.new.const +} + +# Invalidate when const_missing is hit. +assert_equal '2', %q{ + module Container + Foo = 1 + Bar = 2 + + class << self + attr_accessor :count + + def const_missing(name) + @count += 1 + @count == 1 ? Foo : Bar + end + end + + @count = 0 + end + + def const + Container::Baz + end + + const + const +} + +# Invalidate when the iseq gets cleaned up. +assert_equal '2', %q{ + CONSTANT = 1 + + iseq = RubyVM::InstructionSequence.compile(<<~RUBY) + CONSTANT + RUBY + + iseq.eval + iseq = nil + + GC.start + CONSTANT = 2 +} + +# Invalidate when the iseq gets cleaned up even if it was never in the cache. +assert_equal '2', %q{ + CONSTANT = 1 + + iseq = RubyVM::InstructionSequence.compile(<<~RUBY) + CONSTANT + RUBY + + iseq = nil + + GC.start + CONSTANT = 2 +} diff --git a/bootstraptest/test_eval.rb b/bootstraptest/test_eval.rb index a9f389c673..20bd9615f4 100644 --- a/bootstraptest/test_eval.rb +++ b/bootstraptest/test_eval.rb @@ -217,7 +217,7 @@ assert_equal %q{[10, main]}, %q{ } %w[break next redo].each do |keyword| - assert_match %r"Can't escape from eval with #{keyword}\b", %{ + assert_match %r"Invalid #{keyword}\b", %{ $stderr = STDOUT begin eval "0 rescue #{keyword}" @@ -227,6 +227,16 @@ assert_equal %q{[10, main]}, %q{ }, '[ruby-dev:31372]' end +assert_normal_exit %{ + $stderr = STDOUT + 5000.times do + begin + eval "0 rescue break" + rescue SyntaxError + end + end +} + assert_normal_exit %q{ $stderr = STDOUT class Foo @@ -354,3 +364,34 @@ assert_normal_exit %q{ end }, 'check escaping the internal value th->base_block' +assert_equal "false", <<~RUBY, "literal strings are mutable", "--disable-frozen-string-literal" + eval("'test'").frozen? +RUBY + +assert_equal "false", <<~RUBY, "literal strings are mutable", "--disable-frozen-string-literal", frozen_string_literal: true + eval("'test'").frozen? +RUBY + +assert_equal "true", <<~RUBY, "literal strings are frozen", "--enable-frozen-string-literal" + eval("'test'").frozen? +RUBY + +assert_equal "true", <<~RUBY, "literal strings are frozen", "--enable-frozen-string-literal", frozen_string_literal: false + eval("'test'").frozen? +RUBY + +assert_equal "false", <<~RUBY, "__FILE__ is mutable", "--disable-frozen-string-literal" + eval("__FILE__").frozen? +RUBY + +assert_equal "false", <<~RUBY, "__FILE__ is mutable", "--disable-frozen-string-literal", frozen_string_literal: true + eval("__FILE__").frozen? +RUBY + +assert_equal "true", <<~RUBY, "__FILE__ is frozen", "--enable-frozen-string-literal" + eval("__FILE__").frozen? +RUBY + +assert_equal "true", <<~RUBY, "__FILE__ is frozen", "--enable-frozen-string-literal", frozen_string_literal: false + eval("__FILE__").frozen? +RUBY diff --git a/bootstraptest/test_exception.rb b/bootstraptest/test_exception.rb index 0fb6f552b8..decfdc08a3 100644 --- a/bootstraptest/test_exception.rb +++ b/bootstraptest/test_exception.rb @@ -370,7 +370,7 @@ assert_equal %q{}, %q{ } ## -assert_match /undefined method `foo\'/, %q{#` +assert_match /undefined method 'foo\'/, %q{#` STDERR.reopen(STDOUT) class C def inspect diff --git a/bootstraptest/test_fiber.rb b/bootstraptest/test_fiber.rb index 2614dd13bf..ae809a5936 100644 --- a/bootstraptest/test_fiber.rb +++ b/bootstraptest/test_fiber.rb @@ -37,3 +37,8 @@ assert_normal_exit %q{ assert_normal_exit %q{ Fiber.new(&Object.method(:class_eval)).resume("foo") }, '[ruby-dev:34128]' + +# [Bug #21400] +assert_normal_exit %q{ + Thread.new { Fiber.current.kill }.join +} diff --git a/bootstraptest/test_finalizer.rb b/bootstraptest/test_finalizer.rb index 22a16b1220..ccfa0b55d6 100644 --- a/bootstraptest/test_finalizer.rb +++ b/bootstraptest/test_finalizer.rb @@ -6,3 +6,11 @@ ObjectSpace.define_finalizer(b1,proc{b1.inspect}) ObjectSpace.define_finalizer(a2,proc{a1.inspect}) ObjectSpace.define_finalizer(a1,proc{}) }, '[ruby-dev:35778]' + +assert_equal 'true', %q{ + obj = Object.new + id = obj.object_id + + ObjectSpace.define_finalizer(obj, proc { |i| print(id == i) }) + nil +} diff --git a/bootstraptest/test_flow.rb b/bootstraptest/test_flow.rb index 35f19db588..15528a4213 100644 --- a/bootstraptest/test_flow.rb +++ b/bootstraptest/test_flow.rb @@ -363,7 +363,7 @@ assert_equal %q{[1, 2, 3, 5, 2, 3, 5, 7, 8]}, %q{$a = []; begin; ; $a << 1 ; $a << 8 ; rescue Exception; $a << 99; end; $a} assert_equal %q{[1, 2, 6, 3, 5, 7, 8]}, %q{$a = []; begin; ; $a << 1 - o = "test"; $a << 2 + o = "test".dup; $a << 2 def o.test(a); $a << 3 return a; $a << 4 ensure; $a << 5 diff --git a/bootstraptest/test_fork.rb b/bootstraptest/test_fork.rb index 83923dad97..860ef285d0 100644 --- a/bootstraptest/test_fork.rb +++ b/bootstraptest/test_fork.rb @@ -75,3 +75,30 @@ assert_equal '[1, 2]', %q{ end }, '[ruby-dev:44005] [Ruby 1.9 - Bug #4950]' +assert_equal 'ok', %q{ + def now = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + Thread.new do + loop { sleep 0.0001 } + end + + 10.times do + pid = fork{ exit!(0) } + deadline = now + 10 + while true + _, status = Process.waitpid2(pid, Process::WNOHANG) + break if status + if now > deadline + Process.kill(:KILL, pid) + raise "failed" + end + sleep 0.001 + end + unless status.success? + raise "child exited with status #{status}" + end + rescue NotImplementedError + end + :ok +}, '[Bug #20670]' + diff --git a/bootstraptest/test_insns.rb b/bootstraptest/test_insns.rb index 91fba9b011..9e2c4a52a7 100644 --- a/bootstraptest/test_insns.rb +++ b/bootstraptest/test_insns.rb @@ -92,7 +92,7 @@ tests = [ [ 'intern', %q{ :"#{true}" }, ], [ 'newarray', %q{ ["true"][0] }, ], - [ 'newarraykwsplat', %q{ [**{x:'true'}][0][:x] }, ], + [ 'pushtoarraykwsplat', %q{ [**{x:'true'}][0][:x] }, ], [ 'duparray', %q{ [ true ][0] }, ], [ 'expandarray', %q{ y = [ true, false, nil ]; x, = y; x }, ], [ 'expandarray', %q{ y = [ true, false, nil ]; x, *z = y; x }, ], @@ -214,9 +214,24 @@ tests = [ 'true'.freeze }, - [ 'opt_newarray_max', %q{ [ ].max.nil? }, ], - [ 'opt_newarray_max', %q{ [1, x = 2, 3].max == 3 }, ], - [ 'opt_newarray_max', <<-'},', ], # { + [ 'opt_duparray_send', %q{ x = :a; [:a, :b].include?(x) }, ], + [ 'opt_duparray_send', <<-'},', ], # { + class Array + def include?(i) + i == 1 + end + end + x = 1 + [:a, :b].include?(x) + }, + + [ 'opt_newarray_send', %q{ ![ ].hash.nil? }, ], + + [ 'opt_newarray_send', %q{ v=2; [1, Object.new, 2].include?(v) }, ], + + [ 'opt_newarray_send', %q{ [ ].max.nil? }, ], + [ 'opt_newarray_send', %q{ [1, x = 2, 3].max == 3 }, ], + [ 'opt_newarray_send', <<-'},', ], # { class Array def max true @@ -224,9 +239,9 @@ tests = [ end [1, x = 2, 3].max }, - [ 'opt_newarray_min', %q{ [ ].min.nil? }, ], - [ 'opt_newarray_min', %q{ [3, x = 2, 1].min == 1 }, ], - [ 'opt_newarray_min', <<-'},', ], # { + [ 'opt_newarray_send', %q{ [ ].min.nil? }, ], + [ 'opt_newarray_send', %q{ [3, x = 2, 1].min == 1 }, ], + [ 'opt_newarray_send', <<-'},', ], # { class Array def min true @@ -234,6 +249,48 @@ tests = [ end [3, x = 2, 1].min }, + [ 'opt_newarray_send', %q{ v = 1.23; [v, v*2].pack("E*").unpack("E*") == [v, v*2] }, ], + [ 'opt_newarray_send', %q{ v = 4.56; b = +"x"; [v, v*2].pack("E*", buffer: b); b[1..].unpack("E*") == [v, v*2] }, ], + [ 'opt_newarray_send', <<-'},', ], # { + v = 7.89; + b = +"x"; + class Array + alias _pack pack + def pack(s, buffer: nil, prefix: "y") + buffer ||= +"b" + buffer << prefix + _pack(s, buffer: buffer) + end + end + tests = [] + + ret = [v].pack("E*", prefix: "z") + tests << (ret[0..1] == "bz") + tests << (ret[2..].unpack("E*") == [v]) + + ret = [v].pack("E*") + tests << (ret[0..1] == "by") + tests << (ret[2..].unpack("E*") == [v]) + + [v, v*2, v*3].pack("E*", buffer: b) + tests << (b[0..1] == "xy") + tests << (b[2..].unpack("E*") == [v, v*2, v*3]) + + class Array + def pack(_fmt, buffer:) = buffer + end + + b = nil + tests << [v].pack("E*", buffer: b).nil? + + class Array + def pack(_fmt, **kw) = kw.empty? + end + + tests << [v].pack("E*") == true + + tests.all? or puts tests + }, [ 'throw', %q{ false.tap { break true } }, ], [ 'branchif', %q{ x = nil; x ||= true }, ], @@ -352,7 +409,7 @@ tests = [ [ 'opt_ge', %q{ +0.0.next_float >= 0.0 }, ], [ 'opt_ge', %q{ ?z >= ?a }, ], - [ 'opt_ltlt', %q{ '' << 'true' }, ], + [ 'opt_ltlt', %q{ +'' << 'true' }, ], [ 'opt_ltlt', %q{ ([] << 'true').join }, ], [ 'opt_ltlt', %q{ (1 << 31) == 2147483648 }, ], @@ -361,7 +418,7 @@ tests = [ [ 'opt_aref', %q{ 'true'[0] == ?t }, ], [ 'opt_aset', %q{ [][0] = true }, ], [ 'opt_aset', %q{ {}[0] = true }, ], - [ 'opt_aset', %q{ x = 'frue'; x[0] = 't'; x }, ], + [ 'opt_aset', %q{ x = +'frue'; x[0] = 't'; x }, ], [ 'opt_aset', <<-'},', ], # { # opt_aref / opt_aset mixup situation class X; def x; {}; end; end @@ -369,11 +426,6 @@ tests = [ x&.x[true] ||= true # here }, - [ 'opt_aref_with', %q{ { 'true' => true }['true'] }, ], - [ 'opt_aref_with', %q{ Struct.new(:nil).new['nil'].nil? }, ], - [ 'opt_aset_with', %q{ {}['true'] = true }, ], - [ 'opt_aset_with', %q{ Struct.new(:true).new['true'] = true }, ], - [ 'opt_length', %q{ 'true' .length == 4 }, ], [ 'opt_length', %q{ :true .length == 4 }, ], [ 'opt_length', %q{ [ 'true' ] .length == 1 }, ], diff --git a/bootstraptest/test_io.rb b/bootstraptest/test_io.rb index 89c00d0b88..4081769a8c 100644 --- a/bootstraptest/test_io.rb +++ b/bootstraptest/test_io.rb @@ -1,3 +1,4 @@ +/freebsd/ =~ RUBY_PLATFORM or assert_finish 5, %q{ r, w = IO.pipe t1 = Thread.new { r.sysread(1) } @@ -30,7 +31,8 @@ assert_finish 10, %q{ end }, '[ruby-dev:32566]' -assert_finish 1, %q{ +/freebsd/ =~ RUBY_PLATFORM or +assert_finish 5, %q{ r, w = IO.pipe Thread.new { w << "ab" @@ -83,12 +85,13 @@ assert_normal_exit %q{ ARGF.set_encoding "foo" } +/(freebsd|mswin)/ =~ RUBY_PLATFORM or 10.times do assert_normal_exit %q{ at_exit { p :foo } megacontent = "abc" * 12345678 - #File.open("megasrc", "w") {|f| f << megacontent } + #File.write("megasrc", megacontent) t0 = Thread.main Thread.new { sleep 0.001 until t0.stop?; Process.kill(:INT, $$) } diff --git a/bootstraptest/test_jump.rb b/bootstraptest/test_jump.rb index d07c47a56d..8751343b1f 100644 --- a/bootstraptest/test_jump.rb +++ b/bootstraptest/test_jump.rb @@ -292,7 +292,7 @@ assert_equal "true", %q{ end end end - s = "foo" + s = +"foo" s.return_eigenclass == class << s; self; end }, '[ruby-core:21379]' diff --git a/bootstraptest/test_literal.rb b/bootstraptest/test_literal.rb index 9b3c10d519..39e6527027 100644 --- a/bootstraptest/test_literal.rb +++ b/bootstraptest/test_literal.rb @@ -65,8 +65,12 @@ assert_equal ':a3c', ':"a#{1+2}c".inspect' assert_equal 'Symbol', ':"a#{1+2}c".class' # xstring -assert_equal "foo\n", %q(`echo foo`) -assert_equal "foo\n", %q(s = "foo"; `echo #{s}`) +# WASI doesn't support spawning a new process for now. +if /wasi/ !~ target_platform + assert_equal "foo\n", %q(`echo foo`) + assert_equal "foo\n", %q(s = "foo"; `echo #{s}`) +end +assert_equal "ECHO FOO", %q(def `(s) s.upcase; end; `echo foo`) # regexp assert_equal '', '//.source' @@ -113,16 +117,16 @@ assert_equal '1', 'a = [obj = Object.new]; a.size' assert_equal 'true', 'a = [obj = Object.new]; a[0] == obj' assert_equal '5', 'a = [1,2,3]; a[1] = 5; a[1]' assert_equal 'bar', '[*:foo];:bar' -assert_equal '[1, 2]', 'def nil.to_a; [2]; end; [1, *nil]' -assert_equal '[1, 2]', 'def nil.to_a; [1, 2]; end; [*nil]' -assert_equal '[0, 1, {2=>3}]', '[0, *[1], 2=>3]', "[ruby-dev:31592]" +assert_equal '[]', 'def nil.to_a; [1, 2]; end; [*nil]' +assert_equal '[1]', 'def nil.to_a; [2]; end; [1, *nil]' +assert_equal '[0, 1, {2 => 3}]', '[0, *[1], 2=>3]', "[ruby-dev:31592]" # hash assert_equal 'Hash', '{}.class' assert_equal '{}', '{}.inspect' assert_equal 'Hash', '{1=>2}.class' -assert_equal '{1=>2}', '{1=>2}.inspect' +assert_equal '{1 => 2}', '{1=>2}.inspect' assert_equal '2', 'h = {1 => 2}; h[1]' assert_equal '0', 'h = {1 => 2}; h.delete(1); h.size' assert_equal '', 'h = {1 => 2}; h.delete(1); h[1]' diff --git a/bootstraptest/test_literal_suffix.rb b/bootstraptest/test_literal_suffix.rb index c36fa7078f..7a4d67d0fa 100644 --- a/bootstraptest/test_literal_suffix.rb +++ b/bootstraptest/test_literal_suffix.rb @@ -46,9 +46,9 @@ assert_equal '1', '1rescue nil' assert_equal '10000000000000000001/10000000000000000000', '1.0000000000000000001r' -assert_equal 'syntax error, unexpected local variable or method, expecting end-of-input', - %q{begin eval('1ir', nil, '', 0); rescue SyntaxError => e; e.message[/\A:(?:\d+:)? (.*)/, 1] end} -assert_equal 'syntax error, unexpected local variable or method, expecting end-of-input', - %q{begin eval('1.2ir', nil, '', 0); rescue SyntaxError => e; e.message[/\A:(?:\d+:)? (.*)/, 1] end} -assert_equal 'syntax error, unexpected local variable or method, expecting end-of-input', - %q{begin eval('1e1r', nil, '', 0); rescue SyntaxError => e; e.message[/\A:(?:\d+:)? (.*)/, 1] end} +assert_equal 'unexpected local variable or method, expecting end-of-input', + %q{begin eval('1ir', nil, '', 0); rescue SyntaxError => e; e.message[/(?:\^~*|\A:(?:\d+:)? syntax error,) (.*)/, 1]; end} +assert_equal 'unexpected local variable or method, expecting end-of-input', + %q{begin eval('1.2ir', nil, '', 0); rescue SyntaxError => e; e.message[/(?:\^~*|\A:(?:\d+:)? syntax error,) (.*)/, 1]; end} +assert_equal 'unexpected local variable or method, expecting end-of-input', + %q{begin eval('1e1r', nil, '', 0); rescue SyntaxError => e; e.message[/(?:\^~*|\A:(?:\d+:)? syntax error,) (.*)/, 1]; end} diff --git a/bootstraptest/test_load.rb b/bootstraptest/test_load.rb index e63c93a8f4..fa8d31c098 100644 --- a/bootstraptest/test_load.rb +++ b/bootstraptest/test_load.rb @@ -1,9 +1,9 @@ assert_equal 'ok', %q{ - open("require-lock-test.rb", "w") {|f| - f.puts "sleep 0.1" - f.puts "module M" - f.puts "end" - } + File.write("require-lock-test.rb", <<-END) + sleep 0.1 + module M + end + END $:.unshift Dir.pwd vs = (1..2).map {|i| Thread.start { @@ -16,7 +16,7 @@ assert_equal 'ok', %q{ assert_equal 'ok', %q{ %w[a a/foo b].each {|d| Dir.mkdir(d)} - open("b/foo", "w") {|f| f.puts "$ok = :ok"} + File.write("b/foo", "$ok = :ok\n") $:.replace(%w[a b]) begin load "foo" diff --git a/bootstraptest/test_method.rb b/bootstraptest/test_method.rb index 3462aa9434..78aab73485 100644 --- a/bootstraptest/test_method.rb +++ b/bootstraptest/test_method.rb @@ -22,7 +22,7 @@ assert_match /\Awrong number of arguments \(.*\b0\b.* 1\)\z/, %q{ } # default argument -assert_equal '1', 'def m(x=1) x end; m()' +assert_equal '1', 'def m(x=1) x end; m();' assert_equal '1', 'def m(x=7) x end; m(1)' assert_equal '1', 'def m(a,x=1) x end; m(7)' assert_equal '1', 'def m(a,x=7) x end; m(7,1)' @@ -340,24 +340,6 @@ assert_equal '1', %q( class C; def m() 7 end; private :m end assert_equal '1', %q( class C; def m() 1 end; private :m end C.new.send(:m) ) -# with block -assert_equal '[[:ok1, :foo], [:ok2, :foo, :bar]]', -%q{ - class C - def [](a) - $ary << [yield, a] - end - def []=(a, b) - $ary << [yield, a, b] - end - end - - $ary = [] - C.new[:foo, &lambda{:ok1}] - C.new[:foo, &lambda{:ok2}] = :bar - $ary -} - # with assert_equal '[:ok1, [:ok2, 11]]', %q{ class C @@ -404,7 +386,6 @@ $result # aset and splat assert_equal '4', %q{class Foo;def []=(a,b,c,d);end;end;Foo.new[1,*a=[2,3]]=4} -assert_equal '4', %q{class Foo;def []=(a,b,c,d);end;end;def m(&blk)Foo.new[1,*a=[2,3],&blk]=4;end;m{}} # post test assert_equal %q{[1, 2, :o1, :o2, [], 3, 4, NilClass, nil, nil]}, %q{ @@ -1107,10 +1088,6 @@ assert_equal 'ok', %q{ 'ok' end } -assert_equal 'ok', %q{ - [0][0, &proc{}] += 21 - 'ok' -}, '[ruby-core:30534]' # should not cache when splat assert_equal 'ok', %q{ @@ -1190,3 +1167,270 @@ assert_equal 'DC', %q{ test2 o1, [], block $result.join } + +assert_equal 'ok', %q{ + def foo + binding + ["ok"].first + end + foo + foo +}, '[Bug #20178]' + +assert_equal 'ok', %q{ + def bar(x); x; end + def foo(...); bar(...); end + foo('ok') +} + +assert_equal 'ok', %q{ + def bar(x); x; end + def foo(z, ...); bar(...); end + foo(1, 'ok') +} + +assert_equal 'ok', %q{ + def bar(x, y); x; end + def foo(...); bar("ok", ...); end + foo(1) +} + +assert_equal 'ok', %q{ + def bar(x); x; end + def foo(...); 1.times { return bar(...) }; end + foo("ok") +} + +assert_equal 'ok', %q{ + def bar(x); x; end + def foo(...); x = nil; 1.times { x = bar(...) }; x; end + foo("ok") +} + +assert_equal 'ok', %q{ + def bar(x); yield; end + def foo(...); bar(...); end + foo(1) { "ok" } +} + +assert_equal 'ok', %q{ + def baz(x); x; end + def bar(...); baz(...); end + def foo(...); bar(...); end + foo("ok") +} + +assert_equal '[1, 2, 3, 4]', %q{ + def baz(a, b, c, d); [a, b, c, d]; end + def bar(...); baz(1, ...); end + def foo(...); bar(2, ...); end + foo(3, 4) +} + +assert_equal 'ok', %q{ + class Foo; def self.foo(x); x; end; end + class Bar < Foo; def self.foo(...); super; end; end + Bar.foo('ok') +} + +assert_equal 'ok', %q{ + class Foo; def self.foo(x); x; end; end + class Bar < Foo; def self.foo(...); super(...); end; end + Bar.foo('ok') +} + +assert_equal 'ok', %q{ + class Foo; def self.foo(x, y); x + y; end; end + class Bar < Foo; def self.foo(...); super("o", ...); end; end + Bar.foo('k') +} + +assert_equal 'ok', %q{ + def bar(a); a; end + def foo(...); lambda { bar(...) }; end + foo("ok").call +} + +assert_equal 'ok', %q{ + class Foo; def self.foo(x, y); x + y; end; end + class Bar < Foo; def self.y(&b); b; end; def self.foo(...); y { super("o", ...) }; end; end + Bar.foo('k').call +} + +assert_equal 'ok', %q{ + def baz(n); n; end + def foo(...); bar = baz(...); lambda { lambda { bar } }; end + foo("ok").call.call +} + +assert_equal 'ok', %q{ + class A; def self.foo(...); new(...); end; attr_reader :b; def initialize(a, b:"ng"); @a = a; @b = b; end end + A.foo(1).b + A.foo(1, b: "ok").b +} + +assert_equal 'ok', %q{ + class A; def initialize; @a = ["ok"]; end; def first(...); @a.first(...); end; end + def call x; x.first; end + def call1 x; x.first(1); end + call(A.new) + call1(A.new).first +} + +assert_equal 'ok', %q{ + class A; def foo; yield("o"); end; end + class B < A; def foo(...); super { |x| yield(x + "k") }; end; end + B.new.foo { |x| x } +} + +assert_equal "[1, 2, 3, 4]", %q{ + def foo(*b) = b + + def forward(...) + splat = [1,2,3] + foo(*splat, ...) + end + + forward(4) +} + +assert_equal "[1, 2, 3, 4]", %q{ +class A + def foo(*b) = b +end + +class B < A + def foo(...) + splat = [1,2,3] + super(*splat, ...) + end +end + +B.new.foo(4) +} + +assert_equal 'ok', %q{ + class A; attr_reader :iv; def initialize(...) = @iv = "ok"; end + A.new("foo", bar: []).iv +} + +assert_equal 'ok', %q{ + def foo(a, b) = a + b + def bar(...) = foo(...) + bar(1, 2) + bar(1, 2) + begin + bar(1, 2, 3) + "ng" + rescue ArgumentError + "ok" + end +} + +assert_equal 'ok', %q{ + class C + def foo(...) = :ok + def bar(...) = __send__(:foo, ...) + end + + C.new.bar +} + +assert_equal 'ok', %q{ + class C + def method_missing(...) = :ok + def foo(...) = xyzzy(...) + end + + C.new.foo +} + +assert_equal 'ok', %q{ + class C + def initialize(a) + end + end + + def foo(...) + C.new(...) + :ok + end + + foo(*["bar"]) + foo("baz") +} + +assert_equal 'ok', %q{ + class C + def foo(b:) + b + end + end + + def foo(...) + C.new.send(...) + end + + foo(:foo, b: :ok) + foo(*["foo"], b: :ok) +} + +assert_equal 'ok', %q{ + Thing = Struct.new(:value) + + Obj = Thing.new("ok") + + def delegate(...) + Obj.value(...) + end + + def no_args + delegate + end + + def splat_args(*args) + delegate(*args) + end + + no_args + splat_args +} + +assert_equal 'ok', %q{ + class A + private + def foo = "ng" + end + + class B + def initialize(o) + @o = o + end + + def foo(...) = @o.foo(...) + def internal_foo = foo + end + + b = B.new(A.new) + + begin + b.internal_foo + rescue NoMethodError + "ok" + end +} + +assert_equal 'ok', <<~RUBY + def test(*, kw: false) + "ok" + end + + test +RUBY + +assert_equal '[1, 2, 3]', %q{ + def target(*args) = args + def x = [1] + def forwarder(...) = target(*x, 2, ...) + forwarder(3).inspect +}, '[Bug #21832] post-splat args before forwarding' diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index b29db7ab0e..e2a3e8dd5b 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -67,7 +67,7 @@ assert_equal "#<Ractor:#1 running>", %q{ # Return id, loc, and status for no-name ractor assert_match /^#<Ractor:#([^ ]*?) .+:[0-9]+ terminated>$/, %q{ r = Ractor.new { '' } - r.take + r.join sleep 0.1 until r.inspect =~ /terminated/ r.inspect } @@ -75,7 +75,7 @@ assert_match /^#<Ractor:#([^ ]*?) .+:[0-9]+ terminated>$/, %q{ # Return id, name, loc, and status for named ractor assert_match /^#<Ractor:#([^ ]*?) Test Ractor .+:[0-9]+ terminated>$/, %q{ r = Ractor.new(name: 'Test Ractor') { '' } - r.take + r.join sleep 0.1 until r.inspect =~ /terminated/ r.inspect } @@ -86,7 +86,7 @@ assert_equal 'ok', %q{ r = Ractor.new do 'ok' end - r.take + r.value } # Passed arguments to Ractor.new will be a block parameter @@ -96,7 +96,7 @@ assert_equal 'ok', %q{ r = Ractor.new 'ok' do |msg| msg end - r.take + r.value } # Pass multiple arguments to Ractor.new @@ -105,7 +105,7 @@ assert_equal 'ok', %q{ r = Ractor.new 'ping', 'pong' do |msg, msg2| [msg, msg2] end - 'ok' if r.take == ['ping', 'pong'] + 'ok' if r.value == ['ping', 'pong'] } # Ractor#send passes an object with copy to a Ractor @@ -115,65 +115,23 @@ assert_equal 'ok', %q{ msg = Ractor.receive end r.send 'ok' - r.take + r.value } # Ractor#receive_if can filter the message -assert_equal '[2, 3, 1]', %q{ - r = Ractor.new Ractor.current do |main| - main << 1 - main << 2 - main << 3 - end - a = [] - a << Ractor.receive_if{|msg| msg == 2} - a << Ractor.receive_if{|msg| msg == 3} - a << Ractor.receive -} - -# Ractor#receive_if with break -assert_equal '[2, [1, :break], 3]', %q{ - r = Ractor.new Ractor.current do |main| - main << 1 - main << 2 - main << 3 - end - - a = [] - a << Ractor.receive_if{|msg| msg == 2} - a << Ractor.receive_if{|msg| break [msg, :break]} - a << Ractor.receive -} +assert_equal '[1, 2, 3]', %q{ + ports = 3.times.map{Ractor::Port.new} -# Ractor#receive_if can't be called recursively -assert_equal '[[:e1, 1], [:e2, 2]]', %q{ - r = Ractor.new Ractor.current do |main| - main << 1 - main << 2 - main << 3 + r = Ractor.new ports do |ports| + ports[0] << 3 + ports[1] << 1 + ports[2] << 2 end - a = [] - - Ractor.receive_if do |msg| - begin - Ractor.receive - rescue Ractor::Error - a << [:e1, msg] - end - true # delete 1 from queue - end - - Ractor.receive_if do |msg| - begin - Ractor.receive_if{} - rescue Ractor::Error - a << [:e2, msg] - end - true # delete 2 from queue - end - - a # + a << ports[1].receive # 1 + a << ports[2].receive # 2 + a << ports[0].receive # 3 + a } # dtoa race condition @@ -184,73 +142,145 @@ assert_equal '[:ok, :ok, :ok]', %q{ 10_000.times{ rand.to_s } :ok } - }.map(&:take) + }.map(&:value) +} + +assert_equal "42", %q{ + a = 42 + Ractor.shareable_lambda{ a }.call } -# Ractor.make_shareable issue for locals in proc [Bug #18023] +# Ractor.shareable_proc issue for locals in proc [Bug #18023] assert_equal '[:a, :b, :c, :d, :e]', %q{ v1, v2, v3, v4, v5 = :a, :b, :c, :d, :e - closure = Ractor.current.instance_eval{ Proc.new { [v1, v2, v3, v4, v5] } } + closure = Proc.new { [v1, v2, v3, v4, v5] } + Ractor.shareable_proc(&closure).call +} + +# Ractor.shareable_proc makes a copy of given Proc +assert_equal '[true, true]', %q{ + pr1 = Proc.new do + self + end + pr2 = Ractor.shareable_proc(&pr1) - Ractor.make_shareable(closure).call + [pr1.call == self, pr2.call == nil] } -# Ractor.make_shareable issue for locals in proc [Bug #18023] -assert_equal '[:a, :b, :c, :d, :e, :f, :g]', %q{ - a = :a - closure = Ractor.current.instance_eval do - -> { - b, c, d = :b, :c, :d - -> { - e, f, g = :e, :f, :g - -> { [a, b, c, d, e, f, g] } - }.call - }.call +# Ractor.shareable_proc keeps the original Proc intact +assert_equal '[SyntaxError, [Object, 43, 43], Binding]', %q{ + a = 42 + pr1 = Proc.new do + [self.class, eval("a"), binding.local_variable_get(:a)] end + a += 1 + pr2 = Ractor.shareable_proc(&pr1) - Ractor.make_shareable(closure).call + r = [] + begin + pr2.call + rescue SyntaxError + r << SyntaxError + end + + r << pr1.call << pr1.binding.class } -# Now autoload in non-main Ractor is not supported -assert_equal 'ok', %q{ - autoload :Foo, 'foo.rb' - r = Ractor.new do - p Foo - rescue Ractor::UnsafeError - :ok +# Ractor.make_shareable mutates the original Proc +# This is the current behavior, it's currently considered safe enough +# because in most cases it would raise anyway due to not-shared self or not-shared captured variable value +assert_equal '[[42, 42], Binding, true, SyntaxError, "Can\'t create Binding from isolated Proc"]', %q{ + a = 42 + pr1 = nil.instance_exec do + Proc.new do + [eval("a"), binding.local_variable_get(:a)] + end + end + + r = [pr1.call, pr1.binding.class] + + pr2 = Ractor.make_shareable(pr1) + r << pr1.equal?(pr2) + + begin + pr1.call + rescue SyntaxError + r << SyntaxError + end + + begin + r << pr1.binding + rescue ArgumentError + r << $!.message + end + + r +} + +# Ractor::IsolationError cases +assert_equal '3', %q{ + ok = 0 + + begin + a = 1 + Ractor.shareable_proc{a} + a = 2 + rescue Ractor::IsolationError => e + ok += 1 + end + + begin + cond = false + b = 1 + b = 2 if cond + Ractor.shareable_proc{b} + rescue Ractor::IsolationError => e + ok += 1 + end + + begin + 1.times{|i| + i = 2 + Ractor.shareable_proc{i} + } + rescue Ractor::IsolationError => e + ok += 1 end - r.take } ### ### # Ractor still has several memory corruption so skip huge number of tests -if ENV['GITHUB_WORKFLOW'] && - ENV['GITHUB_WORKFLOW'] == 'Compilations' +if ENV['GITHUB_WORKFLOW'] == 'Compilations' # ignore the follow else -# Ractor.select(*ractors) receives a values from a ractors. -# It is similar to select(2) and Go's select syntax. -# The return value is [ch, received_value] +# Ractor.select with a Ractor argument assert_equal 'ok', %q{ # select 1 r1 = Ractor.new{'r1'} - r, obj = Ractor.select(r1) - 'ok' if r == r1 and obj == 'r1' + port, obj = Ractor.select(r1) + if port == r1 and obj == 'r1' + 'ok' + else + # failed + [port, obj].inspect + end } # Ractor.select from two ractors. assert_equal '["r1", "r2"]', %q{ # select 2 - r1 = Ractor.new{'r1'} - r2 = Ractor.new{'r2'} - rs = [r1, r2] + p1 = Ractor::Port.new + p2 = Ractor::Port.new + r1 = Ractor.new(p1){|p1| p1 << 'r1'} + r2 = Ractor.new(p2){|p2| p2 << 'r2'} + ps = [p1, p2] as = [] - r, obj = Ractor.select(*rs) - rs.delete(r) + port, obj = Ractor.select(*ps) + ps.delete(port) as << obj - r, obj = Ractor.select(*rs) + port, obj = Ractor.select(*ps) as << obj as.sort #=> ["r1", "r2"] } @@ -283,11 +313,10 @@ assert_equal 30.times.map { 'ok' }.to_s, %q{ 30.times.map{|i| test i } -} unless ENV['RUN_OPTS'] =~ /--jit-min-calls=5/ || # This always fails with --jit-wait --jit-min-calls=5 - (ENV.key?('TRAVIS') && ENV['TRAVIS_CPU_ARCH'] == 'arm64') # https://bugs.ruby-lang.org/issues/17878 +} unless (ENV.key?('TRAVIS') && ENV['TRAVIS_CPU_ARCH'] == 'arm64') # https://bugs.ruby-lang.org/issues/17878 # Exception for empty select -assert_match /specify at least one ractor/, %q{ +assert_match /specify at least one Ractor::Port or Ractor/, %q{ begin Ractor.select rescue ArgumentError => e @@ -295,30 +324,12 @@ assert_match /specify at least one ractor/, %q{ end } -# Outgoing port of a ractor will be closed when the Ractor is terminated. -assert_equal 'ok', %q{ - r = Ractor.new do - 'finish' - end - - r.take - sleep 0.1 until r.inspect =~ /terminated/ - - begin - o = r.take - rescue Ractor::ClosedError - 'ok' - else - "ng: #{o}" - end -} - # Raise Ractor::ClosedError when try to send into a terminated ractor assert_equal 'ok', %q{ r = Ractor.new do end - r.take # closed + r.join # closed sleep 0.1 until r.inspect =~ /terminated/ begin @@ -330,47 +341,16 @@ assert_equal 'ok', %q{ end } -# Raise Ractor::ClosedError when try to send into a closed actor -assert_equal 'ok', %q{ - r = Ractor.new { Ractor.receive } - r.close_incoming - - begin - r.send(1) - rescue Ractor::ClosedError - 'ok' - else - 'ng' - end -} - -# Raise Ractor::ClosedError when try to take from closed actor -assert_equal 'ok', %q{ - r = Ractor.new do - Ractor.yield 1 - Ractor.receive - end - - r.close_outgoing - begin - r.take - rescue Ractor::ClosedError - 'ok' - else - 'ng' - end -} - -# Can mix with Thread#interrupt and Ractor#take [Bug #17366] +# Can mix with Thread#interrupt and Ractor#join [Bug #17366] assert_equal 'err', %q{ - Ractor.new{ + Ractor.new do t = Thread.current begin Thread.new{ t.raise "err" }.join rescue => e e.message end - }.take + end.value } # Killed Ractor's thread yields nil @@ -378,34 +358,18 @@ assert_equal 'nil', %q{ Ractor.new{ t = Thread.current Thread.new{ t.kill }.join - }.take.inspect #=> nil + }.value.inspect #=> nil } -# Ractor.yield raises Ractor::ClosedError when outgoing port is closed. +# Raise Ractor::ClosedError when try to send into a ractor with closed default port assert_equal 'ok', %q{ - r = Ractor.new Ractor.current do |main| + r = Ractor.new { + Ractor.current.close + Ractor.main << :ok Ractor.receive - main << true - Ractor.yield 1 - end - - r.close_outgoing - r << true - Ractor.receive - - begin - r.take - rescue Ractor::ClosedError - 'ok' - else - 'ng' - end -} + } -# Raise Ractor::ClosedError when try to send into a ractor with closed incoming port -assert_equal 'ok', %q{ - r = Ractor.new { Ractor.receive } - r.close_incoming + Ractor.receive # wait for ok begin r.send(1) @@ -416,148 +380,82 @@ assert_equal 'ok', %q{ end } -# A ractor with closed incoming port still can send messages out -assert_equal '[1, 2]', %q{ - r = Ractor.new do - Ractor.yield 1 - 2 - end - r.close_incoming - - [r.take, r.take] -} - -# Raise Ractor::ClosedError when try to take from a ractor with closed outgoing port -assert_equal 'ok', %q{ - r = Ractor.new do - Ractor.yield 1 - Ractor.receive - end - - sleep 0.01 # wait for Ractor.yield in r - r.close_outgoing - begin - r.take - rescue Ractor::ClosedError - 'ok' - else - 'ng' - end -} - -# A ractor with closed outgoing port still can receive messages from incoming port -assert_equal 'ok', %q{ - r = Ractor.new do - Ractor.receive - end - - r.close_outgoing - begin - r.send(1) - rescue Ractor::ClosedError - 'ng' - else - 'ok' - end -} - # Ractor.main returns main ractor assert_equal 'true', %q{ Ractor.new{ Ractor.main - }.take == Ractor.current + }.value == Ractor.current } # a ractor with closed outgoing port should terminate assert_equal 'ok', %q{ Ractor.new do - close_outgoing + Ractor.current.close end true until Ractor.count == 1 :ok } -# multiple Ractors can receive (wait) from one Ractor -assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{ - pipe = Ractor.new do - loop do - Ractor.yield Ractor.receive - end +# an exception in a Ractor main thread will be re-raised at Ractor#receive +assert_equal '[RuntimeError, "ok", true]', %q{ + r = Ractor.new do + raise 'ok' # exception will be transferred receiver + end + begin + r.join + rescue Ractor::RemoteError => e + [e.cause.class, #=> RuntimeError + e.cause.message, #=> 'ok' + e.ractor == r] #=> true end - - RN = 10 - rs = RN.times.map{|i| - Ractor.new pipe, i do |pipe, i| - msg = pipe.take - msg # ping-pong - end - } - RN.times{|i| - pipe << i - } - RN.times.map{ - r, n = Ractor.select(*rs) - rs.delete r - n - }.sort } -# Ractor.select also support multiple take, receive and yield -assert_equal '[true, true, true]', %q{ - RN = 10 - CR = Ractor.current - - rs = (1..RN).map{ - Ractor.new do - CR.send 'send' + CR.take #=> 'sendyield' - 'take' - end - } - received = [] - take = [] - yielded = [] - until rs.empty? - r, v = Ractor.select(CR, *rs, yield_value: 'yield') - case r - when :receive - received << v - when :yield - yielded << v - else - take << v - rs.delete r - end +# an exception in a Ractor will be re-raised at Ractor#value +assert_equal '[RuntimeError, "ok", true]', %q{ + r = Ractor.new do + raise 'ok' # exception will be transferred receiver + end + begin + r.value + rescue Ractor::RemoteError => e + [e.cause.class, #=> RuntimeError + e.cause.message, #=> 'ok' + e.ractor == r] #=> true end - [received.all?('sendyield'), yielded.all?(nil), take.all?('take')] } -# multiple Ractors can send to one Ractor -assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{ - pipe = Ractor.new do - loop do - Ractor.yield Ractor.receive +# an exception in a Ractor non-main thread will not be re-raised at Ractor#receive +assert_equal 'ok', %q{ + r = Ractor.new do + Thread.new do + raise 'ng' end + sleep 0.1 + 'ok' end - - RN = 10 - RN.times.map{|i| - Ractor.new pipe, i do |pipe, i| - pipe << i - end - } - RN.times.map{ - pipe.take - }.sort + r.value } -# an exception in a Ractor will be re-raised at Ractor#receive -assert_equal '[RuntimeError, "ok", true]', %q{ - r = Ractor.new do - raise 'ok' # exception will be transferred receiver +# SystemExit from a Ractor is re-raised +# [Bug #21505] +assert_equal '[SystemExit, "exit", true]', %q{ + r = Ractor.new { exit } + begin + r.value + rescue Ractor::RemoteError => e + [e.cause.class, #=> RuntimeError + e.cause.message, #=> 'ok' + e.ractor == r] #=> true end +} + +# SystemExit from a Thread inside a Ractor is re-raised +# [Bug #21505] +assert_equal '[SystemExit, "exit", true]', %q{ + r = Ractor.new { Thread.new { exit }.join } begin - r.take + r.value rescue Ractor::RemoteError => e [e.cause.class, #=> RuntimeError e.cause.message, #=> 'ok' @@ -566,7 +464,7 @@ assert_equal '[RuntimeError, "ok", true]', %q{ } # threads in a ractor will killed -assert_equal '{:ok=>3}', %q{ +assert_equal '{ok: 3}', %q{ Ractor.new Ractor.current do |main| q = Thread::Queue.new Thread.new do @@ -596,7 +494,7 @@ assert_equal '{:ok=>3}', %q{ end 3.times.map{Ractor.receive}.tally -} +} unless yjit_enabled? # YJIT: `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec() # unshareable object are copied assert_equal 'false', %q{ @@ -605,7 +503,7 @@ assert_equal 'false', %q{ msg.object_id end - obj.object_id == r.take + obj.object_id == r.value } # To copy the object, now Marshal#dump is used @@ -623,11 +521,12 @@ assert_equal "allocator undefined for Thread", %q{ } # send shareable and unshareable objects -assert_equal "ok", %q{ - echo_ractor = Ractor.new do +assert_equal "ok", <<~'RUBY', frozen_string_literal: false + port = Ractor::Port.new + echo_ractor = Ractor.new port do |port| loop do v = Ractor.receive - Ractor.yield v + port << v end end @@ -675,13 +574,13 @@ assert_equal "ok", %q{ shareable_objects.map{|o| echo_ractor << o - o2 = echo_ractor.take + o2 = port.receive results << "#{o} is copied" unless o.object_id == o2.object_id } unshareable_objects.map{|o| echo_ractor << o - o2 = echo_ractor.take + o2 = port.receive results << "#{o.inspect} is not copied" if o.object_id == o2.object_id } @@ -690,10 +589,10 @@ assert_equal "ok", %q{ else results.inspect end -} +RUBY # frozen Objects are shareable -assert_equal [false, true, false].inspect, %q{ +assert_equal [false, true, false].inspect, <<~'RUBY', frozen_string_literal: false class C def initialize freeze @a = 1 @@ -707,7 +606,7 @@ assert_equal [false, true, false].inspect, %q{ def check obj1 obj2 = Ractor.new obj1 do |obj| obj - end.take + end.value obj1.object_id == obj2.object_id end @@ -716,11 +615,11 @@ assert_equal [false, true, false].inspect, %q{ results << check(C.new(true)) # false results << check(C.new(true).freeze) # true results << check(C.new(false).freeze) # false -} +RUBY # move example2: String # touching moved object causes an error -assert_equal 'hello world', %q{ +assert_equal 'hello world', <<~'RUBY', frozen_string_literal: false # move r = Ractor.new do obj = Ractor.receive @@ -729,7 +628,7 @@ assert_equal 'hello world', %q{ str = 'hello' r.send str, move: true - modified = r.take + modified = r.value begin str << ' exception' # raise Ractor::MovedError @@ -738,7 +637,7 @@ assert_equal 'hello world', %q{ else raise 'unreachable' end -} +RUBY # move example2: Array assert_equal '[0, 1]', %q{ @@ -749,7 +648,7 @@ assert_equal '[0, 1]', %q{ a1 = [0] r.send a1, move: true - a2 = r.take + a2 = r.value begin a1 << 2 # raise Ractor::MovedError rescue Ractor::MovedError @@ -757,70 +656,39 @@ assert_equal '[0, 1]', %q{ end } -# move with yield -assert_equal 'hello', %q{ - r = Ractor.new do - Thread.current.report_on_exception = false - obj = 'hello' - Ractor.yield obj, move: true - obj << 'world' - end - - str = r.take - begin - r.take - rescue Ractor::RemoteError - str #=> "hello" - end -} - -# yield/move should not make moved object when the yield is not succeeded -assert_equal '"str"', %q{ - R = Ractor.new{} - M = Ractor.current - r = Ractor.new do - s = 'str' - selected_r, v = Ractor.select R, yield_value: s, move: true - raise if selected_r != R # taken from R - M.send s.inspect # s should not be a moved object - end - - Ractor.receive -} - -# yield/move can fail -assert_equal "allocator undefined for Thread", %q{ +# unshareable frozen objects should still be frozen in new ractor after move +assert_equal 'true', %q{ r = Ractor.new do - obj = Thread.new{} - Ractor.yield obj - rescue => e - e.message + obj = receive + { frozen: obj.frozen? } end - r.take + obj = [Object.new].freeze + r.send(obj, move: true) + r.value[:frozen] } -# Access to global-variables are prohibited -assert_equal 'can not access global variables $gv from non-main Ractors', %q{ +# Access to global-variables are prohibited (read) +assert_equal 'can not access global variable $gv from non-main Ractor', %q{ $gv = 1 r = Ractor.new do $gv end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end } -# Access to global-variables are prohibited -assert_equal 'can not access global variables $gv from non-main Ractors', %q{ +# Access to global-variables are prohibited (write) +assert_equal 'can not access global variable $gv from non-main Ractor', %q{ r = Ractor.new do $gv = 1 end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end @@ -834,7 +702,7 @@ assert_equal 'ok', %q{ } end - [$stdin, $stdout, $stderr].zip(r.take){|io, (oid, fno)| + [$stdin, $stdout, $stderr].zip(r.value){|io, (oid, fno)| raise "should not be different object" if io.object_id == oid raise "fd should be same" unless io.fileno == fno } @@ -850,7 +718,7 @@ assert_equal 'ok', %q{ 'ok' end - r.take + r.value } # $DEBUG, $VERBOSE are Ractor local @@ -908,7 +776,7 @@ assert_equal 'true', %q{ h = Ractor.new do ractor_local_globals - end.take + end.value ractor_local_globals == h #=> true } @@ -917,7 +785,8 @@ assert_equal 'false', %q{ r = Ractor.new do self.object_id end - r.take == self.object_id #=> false + ret = r.value + ret == self.object_id } # self is a Ractor instance @@ -925,7 +794,12 @@ assert_equal 'true', %q{ r = Ractor.new do self.object_id end - r.object_id == r.take #=> true + ret = r.value + if r.object_id == ret #=> true + true + else + raise [ret, r.object_id].inspect + end } # given block Proc will be isolated, so can not access outer variables. @@ -940,8 +814,39 @@ assert_equal 'ArgumentError', %q{ end } +# eval with outer locals in a Ractor raises SyntaxError +# [Bug #21522] +assert_equal 'SyntaxError', %q{ + outer = 42 + r = Ractor.new do + eval("outer") + end + begin + r.value + rescue Ractor::RemoteError => e + e.cause.class + end +} + +# eval of an undefined name in a Ractor raises NameError +assert_equal 'NameError', %q{ + r = Ractor.new do + eval("totally_undefined_name") + end + begin + r.value + rescue Ractor::RemoteError => e + e.cause.class + end +} + +# eval of a local defined inside the Ractor works +assert_equal '99', %q{ + Ractor.new { inner = 99; eval("inner").to_s }.value +} + # ivar in shareable-objects are not allowed to access from non-main Ractor -assert_equal "can not get unshareable values from instance variables of classes/modules from non-main Ractors", %q{ +assert_equal "can not get unshareable values from instance variables of classes/modules from non-main Ractors (@iv from C)", <<~'RUBY', frozen_string_literal: false class C @iv = 'str' end @@ -952,13 +857,12 @@ assert_equal "can not get unshareable values from instance variables of classes/ end end - begin - r.take + r.value rescue Ractor::RemoteError => e e.cause.message end -} +RUBY # ivar in shareable-objects are not allowed to access from non-main Ractor assert_equal 'can not access instance variables of shareable objects from non-main Ractors', %q{ @@ -970,7 +874,7 @@ assert_equal 'can not access instance variables of shareable objects from non-ma end begin - r.take + r.value rescue Ractor::RemoteError => e e.cause.message end @@ -996,7 +900,7 @@ assert_equal 'can not access instance variables of shareable objects from non-ma end begin - r.take + r.value rescue Ractor::RemoteError => e e.cause.message end @@ -1017,7 +921,7 @@ assert_equal 'can not access instance variables of shareable objects from non-ma end begin - r.take + r.value rescue Ractor::RemoteError => e e.cause.message end @@ -1031,7 +935,7 @@ assert_equal '11', %q{ Ractor.new obj do |obj| obj.instance_variable_get('@a') - end.take.to_s + end.value.to_s }.join } @@ -1057,33 +961,68 @@ assert_equal '333', %q{ def self.fstr = @fstr end - a = Ractor.new{ C.int }.take + a = Ractor.new{ C.int }.value b = Ractor.new do C.str.to_i rescue Ractor::IsolationError 10 - end.take + end.value c = Ractor.new do C.fstr.to_i - end.take + end.value - d = Ractor.new{ M.int }.take + d = Ractor.new{ M.int }.value e = Ractor.new do M.str.to_i rescue Ractor::IsolationError 20 - end.take + end.value f = Ractor.new do M.fstr.to_i - end.take + end.value # 1 + 10 + 100 + 2 + 20 + 200 a + b + c + d + e + f } +assert_equal '["instance-variable", "instance-variable", nil]', %q{ + class C + @iv1 = "" + @iv2 = 42 + def self.iv1 = defined?(@iv1) # "instance-variable" + def self.iv2 = defined?(@iv2) # "instance-variable" + def self.iv3 = defined?(@iv3) # nil + end + + Ractor.new{ + [C.iv1, C.iv2, C.iv3] + }.value +} + +# moved objects have their shape properly set to original object's shape +assert_equal '1234', %q{ + class Obj + attr_accessor :a, :b, :c, :d + def initialize + @a = 1 + @b = 2 + @c = 3 + end + end + r = Ractor.new do + obj = receive + obj.d = 4 + [obj.a, obj.b, obj.c, obj.d] + end + obj = Obj.new + r.send(obj, move: true) + values = r.value + values.join +} + # cvar in shareable-objects are not allowed to access from non-main Ractor -assert_equal 'can not access class variables from non-main Ractors', %q{ +assert_equal 'can not access class variables from non-main Ractors (@@cv from C)', %q{ class C @@cv = 'str' end @@ -1095,14 +1034,14 @@ assert_equal 'can not access class variables from non-main Ractors', %q{ end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end } # also cached cvar in shareable-objects are not allowed to access from non-main Ractor -assert_equal 'can not access class variables from non-main Ractors', %q{ +assert_equal 'can not access class variables from non-main Ractors (@@cv from C)', %q{ class C @@cv = 'str' def self.cv @@ -1117,14 +1056,14 @@ assert_equal 'can not access class variables from non-main Ractors', %q{ end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end } # Getting non-shareable objects via constants by other Ractors is not allowed -assert_equal 'can not access non-shareable objects in constant C::CONST by non-main Ractor.', %q{ +assert_equal 'can not access non-shareable objects in constant C::CONST by non-main Ractor.', <<~'RUBY', frozen_string_literal: false class C CONST = 'str' end @@ -1132,44 +1071,58 @@ assert_equal 'can not access non-shareable objects in constant C::CONST by non-m C::CONST end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end -} + RUBY -# Constant cache should care about non-sharable constants -assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", %q{ +# Constant cache should care about non-shareable constants +assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", <<~'RUBY', frozen_string_literal: false STR = "hello" def str; STR; end s = str() # fill const cache begin - Ractor.new{ str() }.take + Ractor.new{ str() }.join rescue Ractor::RemoteError => e e.cause.message end -} +RUBY + +# The correct constant path shall be reported +assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", <<~'RUBY', frozen_string_literal: false + STR = "hello" + module M + def self.str; STR; end + end + + begin + Ractor.new{ M.str }.join + rescue Ractor::RemoteError => e + e.cause.message + end +RUBY # Setting non-shareable objects into constants by other Ractors is not allowed -assert_equal 'can not set constants with non-shareable objects by non-main Ractors', %q{ +assert_equal 'can not set constants with non-shareable objects by non-main Ractors', <<~'RUBY', frozen_string_literal: false class C end r = Ractor.new do C::CONST = 'str' end begin - r.take + r.join rescue Ractor::RemoteError => e e.cause.message end -} +RUBY # define_method is not allowed assert_equal "defined with an un-shareable Proc in a different Ractor", %q{ str = "foo" define_method(:buggy){|i| str << "#{i}"} begin - Ractor.new{buggy(10)}.take + Ractor.new{buggy(10)}.join rescue => e e.cause.message end @@ -1180,7 +1133,7 @@ assert_equal '[1000, 3]', %q{ A = Array.new(1000).freeze # [nil, ...] H = {a: 1, b: 2, c: 3}.freeze - Ractor.new{ [A.size, H.size] }.take + Ractor.new{ [A.size, H.size] }.value } # Ractor.count @@ -1190,15 +1143,15 @@ assert_equal '[1, 4, 3, 2, 1]', %q{ ractors = (1..3).map { Ractor.new { Ractor.receive } } counts << Ractor.count - ractors[0].send('End 0').take + ractors[0].send('End 0').join sleep 0.1 until ractors[0].inspect =~ /terminated/ counts << Ractor.count - ractors[1].send('End 1').take + ractors[1].send('End 1').join sleep 0.1 until ractors[1].inspect =~ /terminated/ counts << Ractor.count - ractors[2].send('End 2').take + ractors[2].send('End 2').join sleep 0.1 until ractors[2].inspect =~ /terminated/ counts << Ractor.count @@ -1211,11 +1164,11 @@ assert_equal '0', %q{ n = 0 ObjectSpace.each_object{|o| n += 1 unless Ractor.shareable?(o)} n - }.take + }.value } # ObjectSpace._id2ref can not handle unshareable objects with Ractors -assert_equal 'ok', %q{ +assert_equal 'ok', <<~'RUBY', frozen_string_literal: false s = 'hello' Ractor.new s.object_id do |id ;s| @@ -1224,11 +1177,29 @@ assert_equal 'ok', %q{ rescue => e :ok end - end.take -} + end.value +RUBY + +# Inserting into the id2ref table should be Ractor-safe +assert_equal 'ok', <<~'RUBY' + # Force all calls to Kernel#object_id to insert into the id2ref table + obj = Object.new + ObjectSpace._id2ref(obj.object_id) rescue nil + + 10.times.map do + Ractor.new do + 10_000.times do + a = Object.new + a.object_id + end + end + end.map(&:value) + + :ok +RUBY # Ractor.make_shareable(obj) -assert_equal 'true', %q{ +assert_equal 'true', <<~'RUBY', frozen_string_literal: false class C def initialize @a = 'foo' @@ -1299,7 +1270,7 @@ assert_equal 'true', %q{ } Ractor.shareable?(a) -} +RUBY # Ractor.make_shareable(obj) doesn't freeze shareable objects assert_equal 'true', %q{ @@ -1308,19 +1279,42 @@ assert_equal 'true', %q{ [a.frozen?, a[0].frozen?] == [true, false] } -# Ractor.make_shareable(a_proc) makes a proc shareable. -assert_equal 'true', %q{ - a = [1, [2, 3], {a: "4"}] +# Ractor.make_shareable(a_proc) requires a shareable receiver +assert_equal '[:ok, "Proc\'s self is not shareable:"]', %q{ + pr1 = nil.instance_exec { Proc.new{} } + pr2 = Proc.new{} - pr = Ractor.current.instance_eval do - Proc.new do - a + [pr1, pr2].map do |pr| + begin + Ractor.make_shareable(pr) + rescue Ractor::Error => e + e.message[/^.+?:/] + else + :ok end end +} + +# Ractor.make_shareable(Method/UnboundMethod) +assert_equal 'true', %q{ + # raise because receiver is unshareable + begin + _m0 = Ractor.make_shareable(self.method(:__id__)) + rescue => e + raise e unless e.message =~ /can not make shareable object/ + else + raise "no error" + end - Ractor.make_shareable(a) # referred value should be shareable - Ractor.make_shareable(pr) - Ractor.shareable?(pr) + # Method with shareable receiver + M1 = Ractor.make_shareable(Object.method(:__id__)) + + # UnboundMethod + M2 = Ractor.make_shareable(Object.instance_method(:__id__)) + + Ractor.new do + Object.__id__ == M1.call && M1.call == M2.bind_call(Object) + end.value } # Ractor.shareable?(recursive_objects) @@ -1355,29 +1349,10 @@ assert_equal '[C, M]', %q{ assert_equal '1', %q{ class C a = 1 - define_method "foo", Ractor.make_shareable(Proc.new{ a }) - a = 2 - end - - Ractor.new{ C.new.foo }.take -} - -# Ractor.make_shareable(a_proc) makes a proc shareable. -assert_equal 'can not make a Proc shareable because it accesses outer variables (a).', %q{ - a = b = nil - pr = Ractor.current.instance_eval do - Proc.new do - c = b # assign to a is okay because c is block local variable - # reading b is okay - a = b # assign to a is not allowed #=> Ractor::Error - end + define_method "foo", Ractor.shareable_proc{ a } end - begin - Ractor.make_shareable(pr) - rescue => e - e.message - end + Ractor.new{ C.new.foo }.value } # Ractor.make_shareable(obj, copy: true) makes copied shareable object. @@ -1396,14 +1371,14 @@ assert_equal '[false, false, true, true]', %q{ } # TracePoint with normal Proc should be Ractor local -assert_equal '[4, 8]', %q{ +assert_equal '[6, 10]', %q{ rs = [] TracePoint.new(:line){|tp| rs << tp.lineno if tp.path == __FILE__}.enable do - Ractor.new{ # line 4 + Ractor.new{ # line 5 a = 1 b = 2 - }.take - c = 3 # line 8 + }.value + c = 3 # line 9 end rs } @@ -1412,7 +1387,7 @@ assert_equal '[4, 8]', %q{ assert_equal '[true, false]', %q{ Ractor.new([[]].freeze) { |ary| [ary.frozen?, ary.first.frozen? ] - }.take + }.value } # Ractor deep copies frozen objects (str) @@ -1420,7 +1395,7 @@ assert_equal '[true, false]', %q{ s = String.new.instance_eval { @x = []; freeze} Ractor.new(s) { |s| [s.frozen?, s.instance_variable_get(:@x).frozen?] - }.take + }.value } # Can not trap with not isolated Proc on non-main ractor @@ -1428,33 +1403,85 @@ assert_equal '[:ok, :ok]', %q{ a = [] Ractor.new{ trap(:INT){p :ok} - }.take + }.join a << :ok begin Ractor.new{ s = 'str' trap(:INT){p s} - }.take - rescue => Ractor::RemoteError + }.join + rescue Ractor::RemoteError a << :ok end } +# Ractor.select is interruptible +assert_normal_exit %q{ + trap(:INT) do + exit + end + + r = Ractor.new do + loop do + sleep 1 + end + end + + Thread.new do + sleep 0.5 + Process.kill(:INT, Process.pid) + end + Ractor.select(r) +} + # Ractor-local storage assert_equal '[nil, "b", "a"]', %q{ ans = [] Ractor.current[:key] = 'a' r = Ractor.new{ - Ractor.yield self[:key] + Ractor.main << self[:key] self[:key] = 'b' self[:key] } - ans << r.take - ans << r.take + ans << Ractor.receive + ans << r.value ans << Ractor.current[:key] } +assert_equal '1', %q{ + N = 1_000 + Ractor.new{ + a = [] + 1_000.times.map{|i| + Thread.new(i){|i| + Thread.pass if i < N + a << Ractor.store_if_absent(:i){ i } + a << Ractor.current[:i] + } + }.each(&:join) + a.uniq.size + }.value +} + +# Ractor-local storage +assert_equal '2', %q{ + Ractor.new { + fails = 0 + begin + Ractor.main[:key] # cannot get ractor local storage from non-main ractor + rescue => e + fails += 1 if e.message =~ /Cannot get ractor local/ + end + begin + Ractor.main[:key] = 'val' + rescue => e + fails += 1 if e.message =~ /Cannot set ractor local/ + end + fails + }.value +} + ### ### Synchronization tests ### @@ -1468,51 +1495,54 @@ assert_equal "#{N}#{N}", %Q{ Ractor.new{ N.times{|i| -(i.to_s)} } - }.map{|r| r.take}.join + }.map{|r| r.value}.join } -# enc_table -assert_equal "#{N/10}", %Q{ - Ractor.new do - loop do - Encoding.find("test-enc-#{rand(5_000)}").inspect - rescue ArgumentError => e +assert_equal "ok", %Q{ + N = #{N} + a, b = 2.times.map{ + Ractor.new{ + N.times.map{|i| -(i.to_s)} + } + }.map{|r| r.value} + N.times do |i| + unless a[i].equal?(b[i]) + raise [a[i], b[i]].inspect + end + unless a[i] == i.to_s + raise [i, a[i], b[i]].inspect end end - - src = Encoding.find("UTF-8") - #{N/10}.times{|i| - src.replicate("test-enc-\#{i}") - } + :ok } -# Generic ivtbl +# Generic fields_tbl n = N/2 assert_equal "#{n}#{n}", %Q{ 2.times.map{ Ractor.new do #{n}.times do - obj = '' + obj = +'' obj.instance_variable_set("@a", 1) obj.instance_variable_set("@b", 1) obj.instance_variable_set("@c", 1) obj.instance_variable_defined?("@a") end end - }.map{|r| r.take}.join + }.map{|r| r.value}.join } -# NameError -assert_equal "ok", %q{ +# Now NoMethodError is copyable +assert_equal "NoMethodError", %q{ + obj = "".freeze # NameError refers the receiver indirectly begin - bar + obj.bar rescue => err end - begin - Ractor.new{} << err - rescue TypeError - 'ok' - end + + r = Ractor.new{ Ractor.receive } + r << err + r.value.class } assert_equal "ok", %q{ @@ -1530,53 +1560,997 @@ assert_equal "ok", %q{ # Can yield back values while GC is sweeping [Bug #18117] assert_equal "ok", %q{ + port = Ractor::Port.new workers = (0...8).map do - Ractor.new do + Ractor.new port do |port| loop do 10_000.times.map { Object.new } - Ractor.yield Time.now + port << Time.now + Ractor.receive end end end - 1_000.times { idle_worker, tmp_reporter = Ractor.select(*workers) } + 100.times { + workers.each do + port.receive + end + workers.each do |w| + w.send(nil) + end + } "ok" -} +} if !yjit_enabled? && ENV['GITHUB_WORKFLOW'] != 'ModGC' # flaky +# check method cache invalidation assert_equal "ok", %q{ - def foo(*); ->{ super }; end + module M + def foo + @foo + end + end + + class A + include M + + def initialize + 100.times { |i| instance_variable_set(:"@var_#{i}", "bad: #{i}") } + @foo = 2 + end + end + + class B + include M + + def initialize + @foo = 1 + end + end + + Ractor.new do + b = B.new + 100_000.times do + raise unless b.foo == 1 + end + end + + a = A.new + 100_000.times do + raise unless a.foo == 2 + end + + "ok" +} + +# check method cache invalidation +assert_equal 'true', %q{ + class C1; def self.foo = 1; end + class C2; def self.foo = 2; end + class C3; def self.foo = 3; end + class C4; def self.foo = 5; end + class C5; def self.foo = 7; end + class C6; def self.foo = 11; end + class C7; def self.foo = 13; end + class C8; def self.foo = 17; end + + LN = 10_000 + RN = 10 + CS = [C1, C2, C3, C4, C5, C6, C7, C8] + rs = RN.times.map{|i| + Ractor.new(CS.shuffle){|cs| + LN.times.sum{ + cs.inject(1){|r, c| r * c.foo} # c.foo invalidates method cache entry + } + } + } + + n = CS.inject(1){|r, c| r * c.foo} * LN + rs.map{|r| r.value} == Array.new(RN){n} +} + +# check method cache invalidation +assert_equal 'true', %q{ + class Foo + def hello = nil + end + + r1 = Ractor.new do + 1000.times do + class Foo + def hello = nil + end + end + end + + r2 = Ractor.new do + 1000.times do + o = Foo.new + o.hello + end + end + + r1.value + r2.value + + true +} + +# check experimental warning +assert_match /\Atest_ractor\.rb:1:\s+warning:\s+Ractor API is experimental/, %q{ + Warning[:experimental] = $VERBOSE = true + STDERR.reopen(STDOUT) + eval("Ractor.new{}.value", nil, "test_ractor.rb", 1) +}, frozen_string_literal: false + +# check moved object +assert_equal 'ok', %q{ + r = Ractor.new do + Ractor.receive + GC.start + :ok + end + + obj = begin + raise + rescue => e + e = Marshal.load(Marshal.dump(e)) + end + + r.send obj, move: true + r.value +} + +## Ractor::Selector + +# Selector#empty? returns true +assert_equal 'true', %q{ + skip true unless defined? Ractor::Selector + + s = Ractor::Selector.new + s.empty? +} + +# Selector#empty? returns false if there is target ractors +assert_equal 'false', %q{ + skip false unless defined? Ractor::Selector + + s = Ractor::Selector.new + s.add Ractor.new{} + s.empty? +} + +# Selector#clear removes all ractors from the waiting list +assert_equal 'true', %q{ + skip true unless defined? Ractor::Selector + + s = Ractor::Selector.new + s.add Ractor.new{10} + s.add Ractor.new{20} + s.clear + s.empty? +} + +# Selector#wait can wait multiple ractors +assert_equal '[10, 20, true]', %q{ + skip [10, 20, true] unless defined? Ractor::Selector + + s = Ractor::Selector.new + s.add Ractor.new{10} + s.add Ractor.new{20} + r, v = s.wait + vs = [] + vs << v + r, v = s.wait + vs << v + [*vs.sort, s.empty?] +} if defined? Ractor::Selector + +# Selector#wait can wait multiple ractors with receiving. +assert_equal '30', %q{ + skip 30 unless defined? Ractor::Selector + + RN = 30 + rs = RN.times.map{ + Ractor.new{ :v } + } + s = Ractor::Selector.new(*rs) + + results = [] + until s.empty? + results << s.wait + + # Note that s.wait can raise an exception because other Ractors/Threads + # can take from the same ractors in the waiting set. + # In this case there is no other takers so `s.wait` doesn't raise an error. + end + + results.size +} if defined? Ractor::Selector + +# Selector#wait can support dynamic addition +assert_equal '600', %q{ + skip 600 unless defined? Ractor::Selector + + RN = 100 + s = Ractor::Selector.new + port = Ractor::Port.new + rs = RN.times.map{ + Ractor.new{ + Ractor.main << Ractor.new(port){|port| port << :v3; :v4 } + Ractor.main << Ractor.new(port){|port| port << :v5; :v6 } + Ractor.yield :v1 + :v2 + } + } + + rs.each{|r| s.add(r)} + h = {v1: 0, v2: 0, v3: 0, v4: 0, v5: 0, v6: 0} + + loop do + case s.wait receive: true + in :receive, r + s.add r + in r, v + h[v] += 1 + break if h.all?{|k, v| v == RN} + end + end + + h.sum{|k, v| v} +} unless yjit_enabled? # http://ci.rvm.jp/results/trunk-yjit@ruby-sp2-docker/4466770 + +# Selector should be GCed (free'ed) without trouble +assert_equal 'ok', %q{ + skip :ok unless defined? Ractor::Selector + + RN = 30 + rs = RN.times.map{ + Ractor.new{ :v } + } + s = Ractor::Selector.new(*rs) + :ok +} + +end # if !ENV['GITHUB_WORKFLOW'] + +# Chilled strings are not shareable +assert_equal 'false', %q{ + Ractor.shareable?("chilled") +} + +# Chilled strings can be made shareable +assert_equal 'true', %q{ + shareable = Ractor.make_shareable("chilled") + shareable == "chilled" && Ractor.shareable?(shareable) +} + +# require in Ractor +assert_equal 'true', %q{ + Module.new do + def require feature + return Ractor._require(feature) unless Ractor.main? + super + end + Object.prepend self + set_temporary_name 'Ractor#require' + end + + Ractor.new{ + begin + require 'tempfile' + Tempfile.new + rescue SystemStackError + # prism parser with -O0 build consumes a lot of machine stack + Data.define(:fileno).new(1) + end + }.value.fileno > 0 +} + +# require_relative in Ractor +assert_equal 'true', %q{ + dummyfile = File.join(__dir__, "dummy#{rand}.rb") + return true if File.exist?(dummyfile) + begin - Ractor.make_shareable(foo) - rescue Ractor::IsolationError - "ok" + File.write dummyfile, '' + rescue Exception + # skip on any errors + return true + end + + begin + Ractor.new dummyfile do |f| + require_relative File.basename(f) + end.value + ensure + File.unlink dummyfile end } -assert_equal "ok", %q{ - def foo(**); ->{ super }; end +# require_relative in Ractor +assert_equal 'LoadError', %q{ + dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb") + return true if File.exist?(dummyfile) + + Ractor.new dummyfile do |f| + begin + require_relative File.basename(f) + rescue LoadError => e + e.class + end + end.value +} + +# autolaod in Ractor +assert_equal 'true', %q{ + autoload :Tempfile, 'tempfile' + + r = Ractor.new do + begin + Tempfile.new + rescue SystemStackError + # prism parser with -O0 build consumes a lot of machine stack + Data.define(:fileno).new(1) + end + end + r.value.fileno > 0 +} + +# failed in autolaod in Ractor +assert_equal 'LoadError', %q{ + dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb") + autoload :Tempfile, dummyfile + + r = Ractor.new do + begin + Tempfile.new + rescue LoadError => e + e.class + end + end + r.value +} + +# bind_call in Ractor [Bug #20934] +assert_equal 'ok', %q{ + 2.times.map do + Ractor.new do + 1000.times do + Object.instance_method(:itself).bind_call(self) + end + end + end.each(&:join) + GC.start + :ok.itself +} + +# moved objects being corrupted if embeded (String) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = "foobarbazfoobarbazfoobarbazfoobarbaz" + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved objects being corrupted if embeded (Array) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved objects being corrupted if embeded (Hash) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = { foo: 1, bar: 2 } + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved objects being corrupted if embeded (MatchData) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = "foo".match(/o/) + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved objects being corrupted if embeded (Struct) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Struct.new(:a, :b, :c, :d, :e, :f).new(1, 2, 3, 4, 5, 6) + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved objects being corrupted if embeded (Object) +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + class SomeObject + attr_reader :a, :b, :c, :d, :e, :f + def initialize + @a = @b = @c = @d = @e = @f = 1 + end + + def ==(o) + @a == o.a && + @b == o.b && + @c == o.c && + @d == o.d && + @e == o.e && + @f == o.f + end + end + + SomeObject.new # initial non-embeded + + obj = SomeObject.new + ractor.send(obj.dup, move: true) + roundtripped_obj = ractor.value + roundtripped_obj == obj ? :ok : roundtripped_obj +} + +# moved arrays can't be used +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = [1] + ractor.send(obj, move: true) begin - Ractor.make_shareable(foo) - rescue Ractor::IsolationError - "ok" + [].concat(obj) + rescue TypeError + :ok + else + :fail end } -assert_equal "ok", %q{ - def foo(...); ->{ super }; end +# moved strings can't be used +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = "hello" + ractor.send(obj, move: true) begin - Ractor.make_shareable(foo) - rescue Ractor::IsolationError - "ok" + "".replace(obj) + rescue TypeError + :ok + else + :fail end } -assert_equal "ok", %q{ - def foo((x), (y)); ->{ super }; end +# moved hashes can't be used +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = { a: 1 } + ractor.send(obj, move: true) begin - Ractor.make_shareable(foo([], [])) - rescue Ractor::IsolationError - "ok" + {}.merge(obj) + rescue TypeError + :ok + else + :fail end } -end # if !ENV['GITHUB_WORKFLOW'] +# move objects inside frozen containers +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + original = obj.dup + ractor.send([obj].freeze, move: true) + roundtripped_obj = ractor.value[0] + roundtripped_obj == original ? :ok : roundtripped_obj +} + +# move object with generic ivar +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + obj.instance_variable_set(:@array, [1]) + + ractor.send(obj, move: true) + roundtripped_obj = ractor.value + roundtripped_obj.instance_variable_get(:@array) == [1] ? :ok : roundtripped_obj +} + +# move object with many generic ivars +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + 0.upto(300) do |i| + obj.instance_variable_set(:"@array#{i}", [i]) + end + + ractor.send(obj, move: true) + roundtripped_obj = ractor.value + roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj +} + +# move object with complex generic ivars +assert_equal 'ok', %q{ + # Make Array too_complex + 30.times { |i| [].instance_variable_set(:"@complex#{i}", 1) } + + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + obj.instance_variable_set(:@array1, [1]) + + ractor.send(obj, move: true) + roundtripped_obj = ractor.value + roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj +} + +# move object with generic ivars and existing id2ref table +# [Bug #21664] +assert_equal 'ok', %q{ + obj = [1] + obj.instance_variable_set("@field", :ok) + ObjectSpace._id2ref(obj.object_id) # build id2ref table + + ractor = Ractor.new { Ractor.receive } + ractor.send(obj, move: true) + obj = ractor.value + obj.instance_variable_get("@field") +} + +# copy object with complex generic ivars +assert_equal 'ok', %q{ + # Make Array too_complex + 30.times { |i| [].instance_variable_set(:"@complex#{i}", 1) } + + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + obj.instance_variable_set(:@array1, [1]) + + ractor.send(obj) + roundtripped_obj = ractor.value + roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj +} + +# copy object with many generic ivars +assert_equal 'ok', %q{ + ractor = Ractor.new { Ractor.receive } + obj = Array.new(10, 42) + 0.upto(300) do |i| + obj.instance_variable_set(:"@array#{i}", [i]) + end + + ractor.send(obj) + roundtripped_obj = ractor.value + roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj +} + +# moved composite types move their non-shareable parts properly +assert_equal 'ok', %q{ + k, v = String.new("key"), String.new("value") + h = { k => v } + h.instance_variable_set("@b", String.new("b")) + a = [k,v] + o_singleton = Object.new + def o_singleton.a + @a + end + o_singleton.instance_variable_set("@a", String.new("a")) + class MyObject + attr_reader :a + def initialize(a) + @a = a + end + end + struct_class = Struct.new(:a) + struct = struct_class.new(String.new('a')) + o = MyObject.new(String.new('a')) + port = Ractor::Port.new + + r = Ractor.new port do |port| + loop do + obj = Ractor.receive + val = case obj + when Hash + obj['key'] == 'value' && obj.instance_variable_get("@b") == 'b' + when Array + obj[0] == 'key' + when Struct + obj.a == 'a' + when Object + obj.a == 'a' + end + port << val + end + end + + objs = [h, a, o_singleton, o, struct] + objs.each_with_index do |obj, i| + klass = obj.class + parts_moved = {} + case obj + when Hash + parts_moved[klass] = [obj['key'], obj.instance_variable_get("@b")] + when Array + parts_moved[klass] = obj.dup # the contents + when Struct, Object + parts_moved[klass] = [obj.a] + end + r.send(obj, move: true) + val = port.receive + if val != true + raise "bad val in ractor for obj at i:#{i}" + end + begin + p obj + rescue + else + raise "should be moved" + end + parts_moved.each do |klass, parts| + parts.each_with_index do |part, j| + case part + when Ractor::MovedObject + else + raise "part for class #{klass} at i:#{j} should be moved" + end + end + end + end + 'ok' +} + +# fork after creating Ractor +assert_equal 'ok', %q{ +begin + Ractor.new { Ractor.receive } + _, status = Process.waitpid2 fork { } + status.success? ? "ok" : status +rescue NotImplementedError + :ok +end +} + +# Ractors should be terminated after fork +assert_equal 'ok', %q{ +begin + r = Ractor.new { Ractor.receive } + _, status = Process.waitpid2 fork { + begin + raise if r.value != nil + end + } + r.send(123) + raise unless r.value == 123 + status.success? ? "ok" : status +rescue NotImplementedError + :ok +end +} + +# Ractors should be terminated after fork +assert_equal 'ok', %q{ +begin + r = Ractor.new { Ractor.receive } + _, status = Process.waitpid2 fork { + begin + r.send(123) + rescue Ractor::ClosedError + end + } + r.send(123) + raise unless r.value == 123 + status.success? ? "ok" : status +rescue NotImplementedError + :ok +end +} + +# Creating classes inside of Ractors +# [Bug #18119] +assert_equal 'ok', %q{ + port = Ractor::Port.new + workers = (0...8).map do + Ractor.new port do |port| + loop do + 100.times.map { Class.new } + port << nil + end + end + end + + 100.times { port.receive } + + 'ok' +} + +# Using Symbol#to_proc inside ractors +# [Bug #21354] +assert_equal 'ok', %q{ + :inspect.to_proc + Ractor.new do + # It should not use this cached proc, it should create a new one. If it used + # the cached proc, we would get a ractor_confirm_belonging error here. + :inspect.to_proc + end.join + 'ok' +} + +# take vm lock when deleting generic ivars from the global table +assert_equal 'ok', %q{ + Ractor.new do + a = [1, 2, 3] + a.object_id + a.dup # this deletes generic ivar on dupped object + 'ok' + end.value +} + +## Ractor#monitor + +# monitor port returns `:exited` when the monitering Ractor terminated. +assert_equal 'true', %q{ + r = Ractor.new do + Ractor.main << :ok1 + :ok2 + end + + r.monitor port = Ractor::Port.new + Ractor.receive # :ok1 + port.receive == :exited +} + +# monitor port returns `:exited` even if the monitoring Ractor was terminated. +assert_equal 'true', %q{ + r = Ractor.new do + :ok + end + + r.join # wait for r's terminateion + + r.monitor port = Ractor::Port.new + port.receive == :exited +} + +# monitor returns false if the monitoring Ractor was terminated. +assert_equal 'false', %q{ + r = Ractor.new do + :ok + end + + r.join # wait for r's terminateion + + r.monitor Ractor::Port.new +} + +# monitor port returns `:aborted` when the monitering Ractor is aborted. +assert_equal 'true', %q{ + r = Ractor.new do + Ractor.main << :ok1 + raise 'ok' + end + + r.monitor port = Ractor::Port.new + Ractor.receive # :ok1 + port.receive == :aborted +} + +# monitor port returns `:aborted` even if the monitoring Ractor was aborted. +assert_equal 'true', %q{ + r = Ractor.new do + raise 'ok' + end + + begin + r.join # wait for r's terminateion + rescue Ractor::RemoteError + # ignore + end + + r.monitor port = Ractor::Port.new + port.receive == :aborted +} + +## Ractor#join + +# Ractor#join returns self when the Ractor is terminated. +assert_equal 'true', %q{ + r = Ractor.new do + Ractor.receive + end + + r << :ok + r.join + r.inspect in /terminated/ +} if false # TODO + +# Ractor#join raises RemoteError when the remote Ractor aborted with an exception +assert_equal 'err', %q{ + r = Ractor.new do + raise 'err' + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +## Ractor#value + +# Ractor#value returns the last expression even if it is unshareable +assert_equal 'true', %q{ + r = Ractor.new do + obj = [1, 2] + obj << obj.object_id + end + + ret = r.value + ret == [1, 2, ret.object_id] +} + +# Only one Ractor can call Ractor#value +assert_equal '[["Only the successor ractor can take a value", 9], ["ok", 2]]', %q{ + r = Ractor.new do + 'ok' + end + + RN = 10 + + rs = RN.times.map do + Ractor.new r do |r| + begin + Ractor.main << r.value + Ractor.main << r.value # this ractor can get same result + rescue Ractor::Error => e + Ractor.main << e.message + end + end + end + + (RN+1).times.map{ + Ractor.receive + }.tally.sort +} + +# Cause lots of inline CC misses. +assert_equal 'ok', <<~'RUBY' + class A; def test; 1 + 1; end; end + class B; def test; 1 + 1; end; end + class C; def test; 1 + 1; end; end + class D; def test; 1 + 1; end; end + class E; def test; 1 + 1; end; end + class F; def test; 1 + 1; end; end + class G; def test; 1 + 1; end; end + + objs = [A.new, B.new, C.new, D.new, E.new, F.new, G.new].freeze + + def call_test(obj) + obj.test + end + + ractors = 7.times.map do + Ractor.new(objs) do |objs| + objs = objs.shuffle + 100_000.times do + objs.each do |o| + call_test(o) + end + end + end + end + ractors.each(&:join) + :ok +RUBY + +# This test checks that we do not trigger a GC when we have malloc with Ractor +# locks. We cannot trigger a GC with Ractor locks because GC requires VM lock +# and Ractor barrier. If another Ractor is waiting on this Ractor lock, then it +# will deadlock because the other Ractor will never join the barrier. +# +# Creating Ractor::Port requires locking the Ractor and inserting into an +# st_table, which can call malloc. +assert_equal 'ok', <<~'RUBY' + r = Ractor.new do + loop do + Ractor::Port.new + end + end + + 10.times do + 10_000.times do + r.send(nil) + end + sleep(0.01) + end + :ok +RUBY + +assert_equal 'ok', <<~'RUBY' + begin + 100.times do |i| + Ractor.new(i) do |j| + 1000.times do |i| + "#{j}-#{i}" + end + Ractor.receive + end + pid = fork { } + _, status = Process.waitpid2 pid + raise unless status.success? + end + + :ok + rescue NotImplementedError + :ok + end +RUBY + +assert_equal 'ok', <<~'RUBY' + begin + 100.times do |i| + Ractor.new(i) do |j| + 1000.times do |i| + "#{j}-#{i}" + end + end + pid = fork do + GC.verify_internal_consistency + end + _, status = Process.waitpid2 pid + raise unless status.success? + end + + :ok + rescue NotImplementedError + :ok + end +RUBY + +# When creating bmethods in Ractors, they should only be usable from their +# defining ractor, even if it is GC'd +assert_equal 'ok', <<~'RUBY' + +begin + CLASSES = 1000.times.map { Class.new }.freeze + + # This would be better to run in parallel, but there's a bug with lambda + # creation and YJIT causing crashes in dev mode + ractors = CLASSES.map do |klass| + Ractor.new(klass) do |klass| + Ractor.receive + klass.define_method(:foo) {} + end + end + + ractors.each do |ractor| + ractor << nil + ractor.join + end + + ractors.clear + GC.start + + any = 1000.times.map do + Ractor.new do + CLASSES.any? do |klass| + begin + klass.new.foo + true + rescue RuntimeError + false + end + end + end + end.map(&:value).none? && :ok +rescue ThreadError => e + # ignore limited memory machine + if /can\'t create Thread/ =~ e.message + :ok + else + raise + end +end +RUBY diff --git a/bootstraptest/test_syntax.rb b/bootstraptest/test_syntax.rb index 948e2d7809..fbc9c6f62e 100644 --- a/bootstraptest/test_syntax.rb +++ b/bootstraptest/test_syntax.rb @@ -528,24 +528,24 @@ assert_equal %q{1}, %q{ i } def assert_syntax_error expected, code, message = '' - assert_equal "#{expected}", - "begin eval(%q{#{code}}, nil, '', 0)"'; rescue SyntaxError => e; e.message[/\A:(?:\d+:)? (.*)/, 1] end', message + assert_match /^#{Regexp.escape(expected)}/, + "begin eval(%q{#{code}}, nil, '', 0)"'; rescue SyntaxError => e; e.message[/(?:\^~*|\A:(?:\d+:)?(?! syntax errors? found)(?: syntax error,)?) (.*)/, 1] end', message end assert_syntax_error "unterminated string meets end of file", '().."', '[ruby-dev:29732]' assert_equal %q{[]}, %q{$&;[]}, '[ruby-dev:31068]' -assert_syntax_error "syntax error, unexpected *, expecting '}'", %q{{*0}}, '[ruby-dev:31072]' -assert_syntax_error "`@0' is not allowed as an instance variable name", %q{@0..0}, '[ruby-dev:31095]' -assert_syntax_error "identifier $00 is not valid to get", %q{$00..0}, '[ruby-dev:31100]' -assert_syntax_error "identifier $00 is not valid to set", %q{0..$00=1} +assert_syntax_error "unexpected *, expecting '}'", %q{{*0}}, '[ruby-dev:31072]' +assert_syntax_error "'@0' is not allowed as an instance variable name", %q{@0..0}, '[ruby-dev:31095]' +assert_syntax_error "'$00' is not allowed as a global variable name", %q{$00..0}, '[ruby-dev:31100]' +assert_syntax_error "'$00' is not allowed as a global variable name", %q{0..$00=1} assert_equal %q{0}, %q{[*0];0}, '[ruby-dev:31102]' -assert_syntax_error "syntax error, unexpected ')'", %q{v0,(*,v1,) = 0}, '[ruby-dev:31104]' +assert_syntax_error "unexpected ')'", %q{v0,(*,v1,) = 0}, '[ruby-dev:31104]' assert_equal %q{1}, %q{ class << (ary=[]); def []; 0; end; def []=(x); super(0,x);end;end; ary[]+=1 }, '[ruby-dev:31110]' assert_syntax_error "Can't set variable $1", %q{0..$1=1}, '[ruby-dev:31118]' assert_valid_syntax %q{1.times{1+(1&&next)}}, '[ruby-dev:31119]' assert_valid_syntax %q{x=-1;loop{x+=1&&redo if (x+=1).zero?}}, '[ruby-dev:31119]' -assert_syntax_error %q{syntax error, unexpected end-of-input}, %q{!}, '[ruby-dev:31243]' +assert_syntax_error %q{unexpected end-of-input}, %q{!}, '[ruby-dev:31243]' assert_equal %q{[nil]}, %q{[()]}, '[ruby-dev:31252]' assert_equal %q{true}, %q{!_=()}, '[ruby-dev:31263]' assert_equal 'ok', %q{while true; redo; end if 1 == 2; :ok}, '[ruby-dev:31360]' @@ -629,7 +629,7 @@ assert_equal '2', %q{ assert_match /invalid multibyte char/, %q{ $stderr = STDOUT - eval("\"\xf0".force_encoding("utf-8")) + eval("\"\xf0".dup.force_encoding("utf-8")) }, '[ruby-dev:32429]' # method ! and != @@ -904,3 +904,35 @@ assert_normal_exit %q{ Class end }, '[ruby-core:30293]' + +assert_equal "false", <<~RUBY, "literal strings are mutable", "--disable-frozen-string-literal" + 'test'.frozen? +RUBY + +assert_equal "true", <<~RUBY, "literal strings are frozen", "--disable-frozen-string-literal", frozen_string_literal: true + 'test'.frozen? +RUBY + +assert_equal "true", <<~RUBY, "literal strings are frozen", "--enable-frozen-string-literal" + 'test'.frozen? +RUBY + +assert_equal "false", <<~RUBY, "literal strings are mutable", "--enable-frozen-string-literal", frozen_string_literal: false + 'test'.frozen? +RUBY + +assert_equal "false", <<~RUBY, "__FILE__ is mutable", "--disable-frozen-string-literal" + __FILE__.frozen? +RUBY + +assert_equal "true", <<~RUBY, "__FILE__ is frozen", "--disable-frozen-string-literal", frozen_string_literal: true + __FILE__.frozen? +RUBY + +assert_equal "true", <<~RUBY, "__FILE__ is frozen", "--enable-frozen-string-literal" + __FILE__.frozen? +RUBY + +assert_equal "false", <<~RUBY, "__FILE__ is mutable", "--enable-frozen-string-literal", frozen_string_literal: false + __FILE__.frozen? +RUBY diff --git a/bootstraptest/test_thread.rb b/bootstraptest/test_thread.rb index 38a55ff229..7ff5bb4a38 100644 --- a/bootstraptest/test_thread.rb +++ b/bootstraptest/test_thread.rb @@ -242,9 +242,22 @@ assert_equal 'true', %{ end } +assert_equal 'true', %{ + Thread.new{}.join + begin + Process.waitpid2 fork{ + Thread.new{ + sleep 0.1 + }.join + } + true + rescue NotImplementedError + true + end +} + assert_equal 'ok', %{ - open("zzz.rb", "w") do |f| - f.puts <<-END + File.write("zzz_t1.rb", <<-END) begin Thread.new { fork { GC.start } }.join pid, status = Process.wait2 @@ -253,8 +266,7 @@ assert_equal 'ok', %{ $result = :ok end END - end - require "./zzz.rb" + require "./zzz_t1.rb" $result } @@ -408,8 +420,7 @@ assert_equal 'ok', %q{ } assert_equal 'ok', %{ - open("zzz.rb", "w") do |f| - f.puts <<-'end;' # do + File.write("zzz_t2.rb", <<-'end;') # do begin m = Thread::Mutex.new parent = Thread.current @@ -431,8 +442,7 @@ assert_equal 'ok', %{ $result = :ok end end; - end - require "./zzz.rb" + require "./zzz_t2.rb" $result } @@ -484,6 +494,7 @@ assert_equal 'foo', %q{ GC.start f.call.source } + assert_normal_exit %q{ class C def inspect diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb index 30298a820d..be66395190 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -1,3 +1,338 @@ +# To run the tests in this file only, with YJIT enabled: +# make btest BTESTS=bootstraptest/test_yjit.rb RUN_OPTS="--yjit-call-threshold=1" + +# regression test for popping before side exit +assert_equal "ok", %q{ + def foo(a, *) = a + + def call(args, &) + foo(1) # spill at where the block arg will be + foo(*args, &) + end + + call([1, 2]) + + begin + call([]) + rescue ArgumentError + :ok + end +} + +# regression test for send processing before side exit +assert_equal "ok", %q{ + def foo(a, *) = :foo + + def call(args) + send(:foo, *args) + end + + call([1, 2]) + + begin + call([]) + rescue ArgumentError + :ok + end +} + +# test discarding extra yield arguments +assert_equal "22131300500015901015", %q{ + def splat_kw(ary) = yield *ary, a: 1 + + def splat(ary) = yield *ary + + def kw = yield 1, 2, a: 3 + + def kw_only = yield a: 0 + + def simple = yield 0, 1 + + def none = yield + + def calls + [ + splat([1, 1, 2]) { |x, y| x + y }, + splat([1, 1, 2]) { |y, opt = raise| opt + y}, + splat_kw([0, 1]) { |a:| a }, + kw { |a:| a }, + kw { |one| one }, + kw { |one, a:| a }, + kw_only { |a:| a }, + kw_only { |a: 1| a }, + simple { 5.itself }, + simple { |a| a }, + simple { |opt = raise| opt }, + simple { |*rest| rest }, + simple { |opt_kw: 5| opt_kw }, + none { |a: 9| a }, + # autosplat ineractions + [0, 1, 2].yield_self { |a, b| [a, b] }, + [0, 1, 2].yield_self { |a, opt = raise| [a, opt] }, + [1].yield_self { |a, opt = 4| a + opt }, + ] + end + + calls.join +} + +# test autosplat with empty splat +assert_equal "ok", %q{ + def m(pos, splat) = yield pos, *splat + + m([:ok], []) {|v0,| v0 } +} + +# regression test for send stack shifting +assert_normal_exit %q{ + def foo(a, b) + a.singleton_methods(b) + end + + def call_foo + [1, 1, 1, 1, 1, 1, send(:foo, 1, 1)] + end + + call_foo +} + +# regression test for keyword splat with yield +assert_equal 'nil', %q{ + def splat_kw(kwargs) = yield(**kwargs) + + splat_kw({}) { _1 }.inspect +} + +# regression test for arity check with splat +assert_equal '[:ae, :ae]', %q{ + def req_one(a_, b_ = 1) = raise + + def test(args) + req_one *args + rescue ArgumentError + :ae + end + + [test(Array.new 5), test([])] +} + +# regression test for arity check with splat and send +assert_equal '[:ae, :ae]', %q{ + def two_reqs(a, b_, _ = 1) = a.gsub(a, a) + + def test(name, args) + send(name, *args) + rescue ArgumentError + :ae + end + + [test(:two_reqs, ["g", nil, nil, nil]), test(:two_reqs, ["g"])] +} + +# regression test for GC marking stubs in invalidated code +assert_normal_exit %q{ + skip true unless GC.respond_to?(:compact) + garbage = Array.new(10_000) { [] } # create garbage to cause iseq movement + eval(<<~RUBY) + def foo(n, garbage) + if n == 2 + # 1.times.each to create a cfunc frame to preserve the JIT frame + # which will return to a stub housed in an invalidated block + return 1.times.each do + Object.define_method(:foo) {} + garbage.clear + GC.verify_compaction_references(toward: :empty, expand_heap: true) + end + end + + foo(n + 1, garbage) + end + RUBY + + foo(1, garbage) +} + +# regression test for callee block handler overlapping with arguments +assert_equal '3', %q{ + def foo(_req, *args) = args.last + + def call_foo = foo(0, 1, 2, 3, &->{}) + + call_foo +} + +# call leaf builtin with a block argument +assert_equal '0', "0.abs(&nil)" + +# regression test for invokeblock iseq guard +assert_equal 'ok', %q{ + skip :ok unless GC.respond_to?(:compact) + def foo = yield + 10.times do |i| + ret = eval("foo { #{i} }") + raise "failed at #{i}" unless ret == i + GC.compact + end + :ok +} + +# regression test for overly generous guard elision +assert_equal '[0, :sum, 0, :sum]', %q{ + # In faulty versions, the following happens: + # 1. YJIT puts object on the temp stack with type knowledge + # (CArray or CString) about RBASIC_CLASS(object). + # 2. In iter=0, due to the type knowledge, YJIT generates + # a call to sum() without any guard on RBASIC_CLASS(object). + # 3. In iter=1, a singleton class is added to the object, + # changing RBASIC_CLASS(object), falsifying the type knowledge. + # 4. Because the code from (1) has no class guard, it is incorrectly + # reused and the wrong method is invoked. + # Putting a literal is important for gaining type knowledge. + def carray(iter) + array = [] + array.sum(iter.times { def array.sum(_) = :sum }) + end + + def cstring(iter) + string = "".dup + string.sum(iter.times { def string.sum(_) = :sum }) + end + + [carray(0), carray(1), cstring(0), cstring(1)] +} + +# regression test for return type of Integer#/ +# It can return a T_BIGNUM when inputs are T_FIXNUM. +assert_equal 0x3fffffffffffffff.to_s, %q{ + def call(fixnum_min) + (fixnum_min / -1) - 1 + end + + call(-(2**62)) +} + +# regression test for return type of String#<< +assert_equal 'Sub', %q{ + def call(sub) = (sub << sub).itself + + class Sub < String; end + + call(Sub.new('o')).class +} + +# String#dup with generic ivars +assert_equal '["str", "ivar"]', %q{ + def str_dup(str) = str.dup + str = "str" + str.instance_variable_set(:@ivar, "ivar") + str = str_dup(str) + [str, str.instance_variable_get(:@ivar)] +} + +# test splat filling required and feeding rest +assert_equal '[0, 1, 2, [3, 4]]', %q{ + public def lead_rest(a, b, *rest) + [self, a, b, rest] + end + + def call(args) = 0.lead_rest(*args) + + call([1, 2, 3, 4]) +} + +# test missing opts are nil initialized +assert_equal '[[0, 1, nil, 3], [0, 1, nil, 3], [0, 1, nil, 3, []], [0, 1, nil, 3, []]]', %q{ + public def lead_opts(a, b=binding.local_variable_get(:c), c=3) + [self, a, b, c] + end + + public def opts_rest(a=raise, b=binding.local_variable_get(:c), c=3, *rest) + [self, a, b, c, rest] + end + + def call(args) + [ + 0.lead_opts(1), + 0.lead_opts(*args), + + 0.opts_rest(1), + 0.opts_rest(*args), + ] + end + + call([1]) +} + +# test filled optionals with unspecified keyword param +assert_equal 'ok', %q{ + def opt_rest_opt_kw(_=1, *, k: :ok) = k + + def call = opt_rest_opt_kw(0) + + call +} + +# test splat empty array with rest param +assert_equal '[0, 1, 2, []]', %q{ + public def foo(a=1, b=2, *rest) + [self, a, b, rest] + end + + def call(args) = 0.foo(*args) + + call([]) +} + +# Regression test for yielding with autosplat to block with +# optional parameters. https://github.com/Shopify/yjit/issues/313 +assert_equal '[:a, :b, :a, :b]', %q{ + def yielder(arg) = yield(arg) + yield(arg) + + yielder([:a, :b]) do |c = :c, d = :d| + [c, d] + end +} + +# Regression test for GC mishap while doing shape transition +assert_equal '[:ok]', %q{ + # [Bug #19601] + class RegressionTest + def initialize + @a = @b = @fourth_ivar_does_shape_transition = nil + end + + def extender + @first_extended_ivar = [:ok] + end + end + + GC.stress = true + + # Used to crash due to GC run in rb_ensure_iv_list_size() + # not marking the newly allocated [:ok]. + RegressionTest.new.extender.itself +} + +assert_equal 'true', %q{ + # regression test for tracking type of locals for too long + def local_setting_cmp(five) + victim = 5 + five.define_singleton_method(:respond_to?) do |_, _| + victim = nil + end + + # +1 makes YJIT track that victim is a number and + # defined? calls respond_to? from above indirectly + unless (victim + 1) && defined?(five.something) + # Would return wrong result if we still think `five` is a number + victim.nil? + end + end + + local_setting_cmp(Object.new) + local_setting_cmp(Object.new) +} + assert_equal '18374962167983112447', %q{ # regression test for incorrectly discarding 32 bits of a pointer when it # comes to default values. @@ -14,7 +349,7 @@ assert_equal '18374962167983112447', %q{ } assert_normal_exit %q{ - # regression test for a leak caught by an asert on --yjit-call-threshold=2 + # regression test for a leak caught by an assert on --yjit-call-threshold=2 Foo = 1 eval("def foo = [#{(['Foo,']*256).join}]") @@ -25,6 +360,29 @@ assert_normal_exit %q{ Object.send(:remove_const, :Foo) } +assert_normal_exit %q{ + # Test to ensure send on overridden c functions + # doesn't corrupt the stack + class Bar + def bar(x) + x + end + end + + class Foo + def bar + Bar.new + end + end + + foo = Foo.new + # before this change, this line would error + # because "s" would still be on the stack + # String.to_s is the overridden method here + p foo.bar.bar("s".__send__(:to_s)) +} + + assert_equal '[nil, nil, nil, nil, nil, nil]', %q{ [NilClass, TrueClass, FalseClass, Integer, Float, Symbol].each do |klass| klass.class_eval("def foo = @foo") @@ -36,88 +394,77 @@ assert_equal '[nil, nil, nil, nil, nil, nil]', %q{ end } -assert_equal '0', %q{ - # This is a regression test for incomplete invalidation from - # opt_setinlinecache. This test might be brittle, so - # feel free to remove it in the future if it's too annoying. - # This test assumes --yjit-call-threshold=2. - module M - Foo = 1 - def foo - Foo - end - - def pin_self_type_then_foo - _ = @foo - foo - end - - def only_ints - 1 + self - foo - end - end - - class Integer - include M +assert_equal '[nil, nil, nil, nil, nil, nil]', %q{ + # Tests defined? on non-heap objects + [NilClass, TrueClass, FalseClass, Integer, Float, Symbol].each do |klass| + klass.class_eval("def foo = defined?(@foo)") end - class Sub - include M + [nil, true, false, 0xFABCAFE, 0.42, :cake].map do |instance| + instance.foo + instance.foo end +} - foo_method = M.instance_method(:foo) +assert_equal '[nil, "instance-variable", nil, "instance-variable"]', %q{ + # defined? on object that changes shape between calls + class Foo + def foo + defined?(@foo) + end - dbg = ->(message) do - return # comment this out to get printouts + def add + @foo = 1 + end - $stderr.puts RubyVM::YJIT.disasm(foo_method) - $stderr.puts message + def remove + self.remove_instance_variable(:@foo) + end end - 2.times { 42.only_ints } - - dbg["There should be two versions of getinlineache"] - - module M - remove_const(:Foo) - end + obj = Foo.new + [obj.foo, (obj.add; obj.foo), (obj.remove; obj.foo), (obj.add; obj.foo)] +} - dbg["There should be no getinlinecaches"] +assert_equal '["instance-variable", 5]', %q{ + # defined? on object too complex for shape information + class Foo + def initialize + 100.times { |i| instance_variable_set("@foo#{i}", i) } + end - 2.times do - 42.only_ints - rescue NameError => err - _ = "caught name error #{err}" + def foo + [defined?(@foo5), @foo5] + end end - dbg["There should be one version of getinlineache"] - - 2.times do - Sub.new.pin_self_type_then_foo - rescue NameError - _ = 'second specialization' - end + Foo.new.foo +} - dbg["There should be two versions of getinlineache"] +# getinstancevariable with shape too complex +assert_normal_exit %q{ + class Foo + def initialize + @a = 1 + end - module M - Foo = 1 + def getter + @foobar + end end - dbg["There should still be two versions of getinlineache"] - - 42.only_ints - - dbg["There should be no getinlinecaches"] + # Initialize ivars in changing order, making the Foo + # class have shape too complex + 100.times do |x| + foo = Foo.new + foo.instance_variable_set(:"@a#{x}", 1) + foo.instance_variable_set(:"@foobar", 777) - # Find name of the first VM instruction in M#foo. - insns = RubyVM::InstructionSequence.of(foo_method).to_a - if defined?(RubyVM::YJIT.blocks_for) && (insns.last.find { Array === _1 }&.first == :opt_getinlinecache) - RubyVM::YJIT.blocks_for(RubyVM::InstructionSequence.of(foo_method)) - .filter { _1.iseq_start_index == 0 }.count - else - 0 # skip the test + # The getter method eventually sees shape too complex + r = foo.getter + if r != 777 + raise "error" + end end } @@ -178,6 +525,8 @@ assert_equal 'string', %q{ # Check that exceptions work when getting global variable assert_equal 'rescued', %q{ + Warning[:deprecated] = true + module Warning def warn(message) raise @@ -331,6 +680,45 @@ assert_equal 'false', %q{ less_than 2 } +# BOP redefinition works on Integer#<= +assert_equal 'false', %q{ + def le(x, y) = x <= y + + le(2, 2) + + class Integer + def <=(_) = false + end + + le(2, 2) +} + +# BOP redefinition works on Integer#> +assert_equal 'false', %q{ + def gt(x, y) = x > y + + gt(3, 2) + + class Integer + def >(_) = false + end + + gt(3, 2) +} + +# BOP redefinition works on Integer#>= +assert_equal 'false', %q{ + def ge(x, y) = x >= y + + ge(2, 2) + + class Integer + def >=(_) = false + end + + ge(2, 2) +} + # Putobject, less-than operator, fixnums assert_equal '2', %q{ def check_index(index) @@ -774,6 +1162,7 @@ assert_equal 'special', %q{ # Test that object references in generated code get marked and moved assert_equal "good", %q{ + skip :good unless GC.respond_to?(:compact) def bar "good" end @@ -786,7 +1175,7 @@ assert_equal "good", %q{ foo begin - GC.verify_compaction_references(double_heap: true, toward: :empty) + GC.verify_compaction_references(expand_heap: true, toward: :empty) rescue NotImplementedError # in case compaction isn't supported end @@ -797,7 +1186,7 @@ assert_equal "good", %q{ # Test polymorphic getinstancevariable. T_OBJECT -> T_STRING assert_equal 'ok', %q{ @hello = @h1 = @h2 = @h3 = @h4 = 'ok' - str = "" + str = +"" str.instance_variable_set(:@hello, 'ok') public def get @@ -941,6 +1330,18 @@ assert_equal '[42, :default]', %q{ ] } +# Test default value block for Hash +assert_equal "false", <<~RUBY, frozen_string_literal: false + def index_with_string(h) + h["foo"] + end + + h = Hash.new { |h, k| k.frozen? } + + index_with_string(h) + index_with_string(h) +RUBY + # A regression test for making sure cfp->sp is proper when # hitting stubs. See :stub-sp-flush: assert_equal 'ok', %q{ @@ -1098,6 +1499,38 @@ assert_equal '42', %q{ run } +# splatting an empty array on a specialized method +assert_equal 'ok', %q{ + def run + "ok".to_s(*[]) + end + + run + run +} + +# splatting an single element array on a specialized method +assert_equal '[1]', %q{ + def run + [].<<(*[1]) + end + + run + run +} + +# specialized method with wrong args +assert_equal 'ok', %q{ + def run(x) + "bad".to_s(123) if x + rescue + :ok + end + + run(false) + run(true) +} + # getinstancevariable on Symbol assert_equal '[nil, nil]', %q{ # @foo to exercise the getinstancevariable instruction @@ -1204,6 +1637,19 @@ assert_equal '[1, 2, 42]', %q{ [foo {1}, foo {2}, foo {42}] } +# test calling without block param +assert_equal '[1, false, 2, false]', %q{ + def bar + block_given? && yield + end + + def foo(&block) + bar(&block) + end + + [foo { 1 }, foo, foo { 2 }, foo] +} + # test calling block param failing assert_equal '42', %q{ def foo(&block) @@ -1284,7 +1730,7 @@ assert_equal '{}', %q{ } # test building hash with values -assert_equal '{:foo=>:bar}', %q{ +assert_equal '{foo: :bar}', %q{ def build_hash(val) { foo: val } end @@ -1337,6 +1783,46 @@ assert_equal 'foo123', %q{ make_str("foo", 123) } +# test that invalidation of String#to_s doesn't crash +assert_equal 'meh', %q{ + def inval_method + "".to_s + end + + inval_method + + class String + def to_s + "meh" + end + end + + inval_method +} + +# test that overriding to_s on a String subclass works consistently +assert_equal 'meh', %q{ + class MyString < String + def to_s + "meh" + end + end + + def test_to_s(obj) + obj.to_s + end + + OBJ = MyString.new + + # Should return '' both times + test_to_s("") + test_to_s("") + + # Can return '' if YJIT optimises String#to_s too aggressively + test_to_s(OBJ) + test_to_s(OBJ) +} + # test string interpolation with overridden to_s assert_equal 'foo', %q{ class String @@ -1353,6 +1839,228 @@ assert_equal 'foo', %q{ make_str("foo") } +# Test that String unary plus returns the same object ID for an unfrozen string. +assert_equal 'true', <<~RUBY, frozen_string_literal: false + def jittable_method + str = "bar" + + old_obj_id = str.object_id + uplus_str = +str + + uplus_str.object_id == old_obj_id + end + jittable_method +RUBY + +# Test that String unary plus returns a different unfrozen string when given a frozen string +assert_equal 'false', %q{ + # Logic needs to be inside an ISEQ, such as a method, for YJIT to compile it + def jittable_method + frozen_str = "foo".freeze + + old_obj_id = frozen_str.object_id + uplus_str = +frozen_str + + uplus_str.object_id == old_obj_id || uplus_str.frozen? + end + + jittable_method +} + +# String-subclass objects should behave as expected inside string-interpolation via concatstrings +assert_equal 'monkeys / monkeys, yo!', %q{ + class MyString < String + # This is a terrible idea in production code, but we'd like YJIT to match CRuby + def to_s + super + ", yo!" + end + end + + def jittable_method + m = MyString.new('monkeys') + "#{m} / #{m.to_s}" + end + + jittable_method +} + +# String-subclass objects should behave as expected for string equality +assert_equal 'false', %q{ + class MyString < String + # This is a terrible idea in production code, but we'd like YJIT to match CRuby + def ==(b) + "#{self}_" == b + end + end + + def jittable_method + ma = MyString.new("a") + + # Check equality with string-subclass receiver + ma == "a" || ma != "a_" || + # Check equality with string receiver + "a_" == ma || "a" != ma || + # Check equality between string subclasses + ma != MyString.new("a_") || + # Make sure "string always equals itself" check isn't used with overridden equality + ma == ma + end + jittable_method +} + +# Test to_s duplicates a string subclass object but not a string +assert_equal 'false', %q{ + class MyString < String; end + + def jittable_method + a = "a" + ma = MyString.new("a") + + a.object_id != a.to_s.object_id || + ma.object_id == ma.to_s.object_id + end + jittable_method +} + +# Test freeze on string subclass +assert_equal 'true', %q{ + class MyString < String; end + + def jittable_method + fma = MyString.new("a").freeze + + # Freezing a string subclass should not duplicate it + fma.object_id == fma.freeze.object_id + end + jittable_method +} + +# Test unary minus on string subclass +assert_equal 'true', %q{ + class MyString < String; end + + def jittable_method + ma = MyString.new("a") + fma = MyString.new("a").freeze + + # Unary minus on frozen string subclass should not duplicate it + fma.object_id == (-fma).object_id && + # Unary minus on unfrozen string subclass should duplicate it + ma.object_id != (-ma).object_id + end + jittable_method +} + +# Test unary plus on string subclass +assert_equal 'true', %q{ + class MyString < String; end + + def jittable_method + fma = MyString.new("a").freeze + + # Unary plus on frozen string subclass should not duplicate it + fma.object_id != (+fma).object_id + end + jittable_method +} + +# test getbyte on string class +assert_equal '[97, :nil, 97, :nil, :raised]', %q{ + def getbyte(s, i) + byte = begin + s.getbyte(i) + rescue TypeError + :raised + end + + byte || :nil + end + + getbyte("a", 0) + getbyte("a", 0) + + [getbyte("a", 0), getbyte("a", 1), getbyte("a", -1), getbyte("a", -2), getbyte("a", "a")] +} + +# Basic test for String#setbyte +assert_equal 'AoZ', %q{ + s = +"foo" + s.setbyte(0, 65) + s.setbyte(-1, 90) + s +} + +# String#setbyte IndexError +assert_equal 'String#setbyte', %q{ + def ccall = "".setbyte(1, 0) + begin + ccall + rescue => e + e.backtrace.first.split("'").last + end +} + +# String#setbyte TypeError +assert_equal 'String#setbyte', %q{ + def ccall = "".setbyte(nil, 0) + begin + ccall + rescue => e + e.backtrace.first.split("'").last + end +} + +# String#setbyte FrozenError +assert_equal 'String#setbyte', %q{ + def ccall = "a".freeze.setbyte(0, 0) + begin + ccall + rescue => e + e.backtrace.first.split("'").last + end +} + +# non-leaf String#setbyte +assert_equal 'String#setbyte', %q{ + def to_int + @caller = caller + 0 + end + + def ccall = "a".dup.setbyte(self, 98) + ccall + + @caller.first.split("'").last +} + +# non-leaf String#byteslice +assert_equal 'TypeError', %q{ + def ccall = "".byteslice(nil, nil) + begin + ccall + rescue => e + e.class + end +} + +# Test << operator on string subclass +assert_equal 'abab', %q{ + class MyString < String; end + + def jittable_method + a = -"a" + mb = MyString.new("b") + + buf = String.new + mbuf = MyString.new + + buf << a << mb + mbuf << a << mb + + buf + mbuf + end + jittable_method +} # test invokebuiltin as used in struct assignment assert_equal '123', %q{ @@ -1480,6 +2188,34 @@ assert_equal '7', %q{ foo(5,2) } +# regression test for argument registers with invalidation +assert_equal '[0, 1, 2]', %q{ + def test(n) + ret = n + binding + ret + end + + [0, 1, 2].map do |n| + test(n) + end +} + +# regression test for argument registers +assert_equal 'true', %q{ + class Foo + def ==(other) + other == nil + end + end + + def test + [Foo.new].include?(Foo.new) + end + + test +} + # test pattern matching assert_equal '[:ok, :ok]', %q{ class C @@ -1545,6 +2281,20 @@ assert_equal '123', %q{ foo(Foo) } +# Test EP == BP invalidation with moving ISEQs +assert_equal 'ok', %q{ + skip :ok unless GC.respond_to?(:compact) + def entry + ok = proc { :ok } # set #entry as an EP-escaping ISEQ + [nil].reverse_each do # avoid exiting the JIT frame on the constant + GC.compact # move #entry ISEQ + end + ok # should be read off of escaped EP + end + + entry.call +} + # invokesuper edge case assert_equal '[:A, [:A, :B]]', %q{ class B @@ -1701,6 +2451,50 @@ assert_equal '[:A, :Btwo]', %q{ ins.foo } +# invokesuper with a block +assert_equal 'true', %q{ + class A + def foo = block_given? + end + + class B < A + def foo = super() + end + + B.new.foo { } + B.new.foo { } +} + +# invokesuper in a block +assert_equal '[0, 2]', %q{ + class A + def foo(x) = x * 2 + end + + class B < A + def foo + 2.times.map do |x| + super(x) + end + end + end + + B.new.foo + B.new.foo +} + +# invokesuper zsuper in a bmethod +assert_equal 'ok', %q{ + class Foo + define_method(:itself) { super } + end + begin + Foo.new.itself + rescue RuntimeError + :ok + end +} + # Call to fixnum assert_equal '[true, false]', %q{ def is_odd(obj) @@ -1741,6 +2535,16 @@ assert_equal '[true, false, true, false]', %q{ [is_odd(123), is_odd(456), is_odd(bignum), is_odd(bignum+1)] } +# Flonum and Flonum +assert_equal '[2.0, 0.0, 1.0, 4.0]', %q{ + [1.0 + 1.0, 1.0 - 1.0, 1.0 * 1.0, 8.0 / 2.0] +} + +# Flonum and Fixnum +assert_equal '[2.0, 0.0, 1.0, 4.0]', %q{ + [1.0 + 1, 1.0 - 1, 1.0 * 1, 8.0 / 2] +} + # Call to static and dynamic symbol assert_equal 'bar', %q{ def to_string(obj) @@ -1781,6 +2585,30 @@ assert_equal '[1, 2, 3, 4, 5]', %q{ splatarray } +# splatkw +assert_equal '[1, 2]', %q{ + def foo(a:) = [a, yield] + + def entry(&block) + a = { a: 1 } + foo(**a, &block) + end + + entry { 2 } +} +assert_equal '[1, 2]', %q{ + def foo(a:) = [a, yield] + + def entry(obj, &block) + foo(**obj, &block) + end + + entry({ a: 3 }) { 2 } + obj = Object.new + def obj.to_hash = { a: 1 } + entry(obj) { 2 } +} + assert_equal '[1, 1, 2, 1, 2, 3]', %q{ def expandarray arr = [1, 2, 3] @@ -1835,6 +2663,39 @@ assert_equal '[:not_array, nil, nil]', %q{ expandarray_not_array(obj) } +assert_equal '[1, 2]', %q{ + class NilClass + private + def to_ary + [1, 2] + end + end + + def expandarray_redefined_nilclass + a, b = nil + [a, b] + end + + expandarray_redefined_nilclass + expandarray_redefined_nilclass +} + +assert_equal 'not_array', %q{ + def expandarray_not_array(obj) + a, = obj + a + end + + obj = Object.new + def obj.method_missing(m, *args, &block) + return [:not_array] if m == :to_ary + super + end + + expandarray_not_array(obj) + expandarray_not_array(obj) +} + assert_equal '[1, 2, nil]', %q{ def expandarray_rhs_too_small a, b, c = [1, 2] @@ -1845,6 +2706,17 @@ assert_equal '[1, 2, nil]', %q{ expandarray_rhs_too_small } +assert_equal '[nil, 2, nil]', %q{ + def foo(arr) + a, b, c = arr + end + + a, b, c1 = foo([0, 1]) + a, b, c2 = foo([0, 1, 2]) + a, b, c3 = foo([0, 1]) + [c1, c2, c3] +} + assert_equal '[1, [2]]', %q{ def expandarray_splat a, *b = [1, 2] @@ -2033,6 +2905,26 @@ assert_equal '[[:c_return, :itself, main]]', %q{ events } +# test c_call invalidation +assert_equal '[[:c_call, :itself]]', %q{ + # enable the event once to make sure invalidation + # happens the second time we enable it + TracePoint.new(:c_call) {}.enable{} + + def compiled + itself + end + + # assume first call compiles + compiled + + events = [] + tp = TracePoint.new(:c_call) { |tp| events << [tp.event, tp.method_id] } + tp.enable { compiled } + + events +} + # test enabling tracing for a suspended fiber assert_equal '[[:return, 42]]', %q{ def traced_method @@ -2057,16 +2949,16 @@ assert_equal '[:itself]', %q{ itself end - - tracing_ractor = Ractor.new do + port = Ractor::Port.new + tracing_ractor = Ractor.new port do |port| # 1: start tracing events = [] tp = TracePoint.new(:c_call) { events << _1.method_id } tp.enable - Ractor.yield(nil) + port << nil # 3: run compiled method on tracing ractor - Ractor.yield(nil) + port << nil traced_method events @@ -2074,13 +2966,13 @@ assert_equal '[:itself]', %q{ tp&.disable end - tracing_ractor.take + port.receive # 2: compile on non tracing ractor traced_method - tracing_ractor.take - tracing_ractor.take + port.receive + tracing_ractor.value } # Try to hit a lazy branch stub while another ractor enables tracing @@ -2094,17 +2986,18 @@ assert_equal '42', %q{ end end - ractor = Ractor.new do + port = Ractor::Port.new + ractor = Ractor.new port do |port| compiled(false) - Ractor.yield(nil) + port << nil compiled(41) end tp = TracePoint.new(:line) { itself } - ractor.take + port.receive tp.enable - ractor.take + ractor.value } # Test equality with changing types @@ -2180,7 +3073,7 @@ assert_equal '42', %q{ A.foo A.foo - Ractor.new { A.foo }.take + Ractor.new { A.foo }.value } assert_equal '["plain", "special", "sub", "plain"]', %q{ @@ -2431,6 +3324,57 @@ assert_equal '[[1, 2, 3, 4]]', %q{ 5.times.map { foo(specified: 2, required: 1) }.uniq } +# cfunc kwargs +assert_equal '{foo: 123}', %q{ + def foo(bar) + bar.store(:value, foo: 123) + bar[:value] + end + + foo({}) + foo({}) +} + +# cfunc kwargs +assert_equal '{foo: 123}', %q{ + def foo(bar) + bar.replace(foo: 123) + end + + foo({}) + foo({}) +} + +# cfunc kwargs +assert_equal '{foo: 123, bar: 456}', %q{ + def foo(bar) + bar.replace(foo: 123, bar: 456) + end + + foo({}) + foo({}) +} + +# variadic cfunc kwargs +assert_equal '{foo: 123}', %q{ + def foo(bar) + bar.merge(foo: 123) + end + + foo({}) + foo({}) +} + +# optimized cfunc kwargs +assert_equal 'false', %q{ + def foo + :foo.eql?(foo: :foo) + end + + foo + foo +} + # attr_reader on frozen object assert_equal 'false', %q{ class Foo @@ -2536,7 +3480,7 @@ assert_equal "true", %q{ } # duphash -assert_equal '{:foo=>123}', %q{ +assert_equal '{foo: 123}', %q{ def foo {foo: 123} end @@ -2546,7 +3490,7 @@ assert_equal '{:foo=>123}', %q{ } # newhash -assert_equal '{:foo=>2}', %q{ +assert_equal '{foo: 2}', %q{ def foo {foo: 1+1} end @@ -2635,11 +3579,20 @@ assert_equal 'new', %q{ foo end + def bar + :bar + end + + test test RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT) + # Old simulat_omm! leaves one byte of space and this fills it up + bar + bar + def foo :new end @@ -2647,6 +3600,74 @@ assert_equal 'new', %q{ test } +# Bug #21257 (infinite jmp) +assert_equal 'ok', %q{ + Good = :ok + + def first + second + end + + def second + ::Good + end + + # Make `second` side exit on its first instruction + trace = TracePoint.new(:line) { } + trace.enable(target: method(:second)) + + first + # Recompile now that the constant cache is populated, so we get a fallthrough from `first` to `second` + # (this is need to reproduce with --yjit-call-threshold=1) + RubyVM::YJIT.code_gc if defined?(RubyVM::YJIT) + first + + # Trigger a constant cache miss in rb_vm_opt_getconstant_path (in `second`) next time it's called + module InvalidateConstantCache + Good = nil + end + + RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT) + + first + first +} + +assert_equal 'ok', %q{ + # Multiple incoming branches into second + Good = :ok + + def incoming_one + second + end + + def incoming_two + second + end + + def second + ::Good + end + + # Make `second` side exit on its first instruction + trace = TracePoint.new(:line) { } + trace.enable(target: method(:second)) + + incoming_one + # Recompile now that the constant cache is populated, so we get a fallthrough from `incoming_one` to `second` + # (this is need to reproduce with --yjit-call-threshold=1) + RubyVM::YJIT.code_gc if defined?(RubyVM::YJIT) + incoming_one + incoming_two + + # Trigger a constant cache miss in rb_vm_opt_getconstant_path (in `second`) next time it's called + module InvalidateConstantCache + Good = nil + end + + incoming_one +} + assert_equal 'ok', %q{ # Try to compile new method while OOM def foo @@ -2735,3 +3756,1707 @@ assert_equal 'ok', %q{ foo(s) rescue :ok foo(s) rescue :ok } + +# File.join is a cfunc accepting variable arguments as a Ruby array (argc = -2) +assert_equal 'foo/bar', %q{ + def foo + File.join("foo", "bar") + end + + foo + foo +} + +# File.join is a cfunc accepting variable arguments as a Ruby array (argc = -2) +assert_equal '', %q{ + def foo + File.join() + end + + foo + foo +} + +# Make sure we're correctly reading RStruct's as.ary union for embedded RStructs +assert_equal '3,12', %q{ + pt_struct = Struct.new(:x, :y) + p = pt_struct.new(3, 12) + def pt_inspect(pt) + "#{pt.x},#{pt.y}" + end + + # Make sure pt_inspect is JITted + 10.times { pt_inspect(p) } + + # Make sure it's returning '3,12' instead of e.g. '3,false' + pt_inspect(p) +} + +# checktype +assert_equal 'false', %q{ + def function() + [1, 2] in [Integer, String] + end + function() +} + +# opt_send_without_block (VM_METHOD_TYPE_ATTRSET) +assert_equal 'foo', %q{ + class Foo + attr_writer :foo + + def foo() + self.foo = "foo" + end + end + foo = Foo.new + foo.foo +} + +# anytostring, intern +assert_equal 'true', %q{ + def foo() + :"#{true}" + end + foo() +} + +# toregexp, objtostring +assert_equal '/true/', %q{ + def foo() + /#{true}/ + end + foo().inspect +} + +# concatstrings, objtostring +assert_equal '9001', %q{ + def foo() + "#{9001}" + end + foo() +} + +# opt_send_without_block (VM_METHOD_TYPE_CFUNC) +assert_equal 'nil', %q{ + def foo + nil.inspect # argc: 0 + end + foo +} +assert_equal '4', %q{ + def foo + 2.pow(2) # argc: 1 + end + foo +} +assert_equal 'aba', %q{ + def foo + "abc".tr("c", "a") # argc: 2 + end + foo +} +assert_equal 'true', %q{ + def foo + respond_to?(:inspect) # argc: -1 + end + foo +} +assert_equal '["a", "b"]', %q{ + def foo + "a\nb".lines(chomp: true) # kwargs + end + foo +} + +# invokebuiltin +assert_equal '123', %q{ + def foo(obj) + obj.foo = 123 + end + + struct = Struct.new(:foo) + obj = struct.new + foo(obj) +} + +# invokebuiltin_delegate +assert_equal '.', %q{ + def foo(path) + Dir.open(path).path + end + foo(".") +} + +# opt_invokebuiltin_delegate_leave +assert_equal '[0]', %q{"\x00".unpack("c")} + +# opt_send_without_block (VM_METHOD_TYPE_ISEQ) +assert_equal '1', %q{ + def foo = 1 + def bar = foo + bar +} +assert_equal '[1, 2, 3]', %q{ + def foo(a, b) = [1, a, b] + def bar = foo(2, 3) + bar +} +assert_equal '[1, 2, 3, 4, 5, 6]', %q{ + def foo(a, b, c:, d:, e: 0, f: 6) = [a, b, c, d, e, f] + def bar = foo(1, 2, c: 3, d: 4, e: 5) + bar +} +assert_equal '[1, 2, 3, 4]', %q{ + def foo(a, b = 2) = [a, b] + def bar = foo(1) + foo(3, 4) + bar +} + +assert_equal '1', %q{ + def foo(a) = a + def bar = foo(1) { 2 } + bar +} +assert_equal '[1, 2]', %q{ + def foo(a, &block) = [a, block.call] + def bar = foo(1) { 2 } + bar +} + +# opt_send_without_block (VM_METHOD_TYPE_IVAR) +assert_equal 'foo', %q{ + class Foo + attr_reader :foo + + def initialize + @foo = "foo" + end + end + Foo.new.foo +} + +# opt_send_without_block (VM_METHOD_TYPE_OPTIMIZED) +assert_equal 'foo', %q{ + Foo = Struct.new(:bar) + Foo.new("bar").bar = "foo" +} +assert_equal 'foo', %q{ + Foo = Struct.new(:bar) + Foo.new("foo").bar +} + +# getblockparamproxy +assert_equal 'foo', %q{ + def foo(&block) + block.call + end + foo { "foo" } +} + +# getblockparam +assert_equal 'foo', %q{ + def foo(&block) + block + end + foo { "foo" }.call +} + +assert_equal '[1, 2]', %q{ + def foo + x = [2] + [1, *x] + end + + foo + foo +} + +# respond_to? with changing symbol +assert_equal 'false', %q{ + def foo(name) + :sym.respond_to?(name) + end + foo(:to_s) + foo(:to_s) + foo(:not_exist) +} + +# respond_to? with method being defined +assert_equal 'true', %q{ + def foo + :sym.respond_to?(:not_yet_defined) + end + foo + foo + module Kernel + def not_yet_defined = true + end + foo +} + +# respond_to? with undef method +assert_equal 'false', %q{ + module Kernel + def to_be_removed = true + end + def foo + :sym.respond_to?(:to_be_removed) + end + foo + foo + class Object + undef_method :to_be_removed + end + foo +} + +# respond_to? with respond_to_missing? +assert_equal 'true', %q{ + class Foo + end + def foo(x) + x.respond_to?(:bar) + end + foo(Foo.new) + foo(Foo.new) + class Foo + def respond_to_missing?(*) = true + end + foo(Foo.new) +} + +# bmethod +assert_equal '[1, 2, 3]', %q{ + one = 1 + define_method(:foo) do + one + end + + 3.times.map { |i| foo + i } +} + +# return inside bmethod +assert_equal 'ok', %q{ + define_method(:foo) do + 1.tap { return :ok } + end + + foo +} + +# bmethod optional and keywords +assert_equal '[[1, nil, 2]]', %q{ + define_method(:opt_and_kwargs) do |a = {}, b: nil, c: nil| + [a, b, c] + end + + 5.times.map { opt_and_kwargs(1, c: 2) }.uniq +} + +# bmethod with forwarded block +assert_equal '2', %q{ + define_method(:foo) do |&block| + block.call + end + + def bar(&block) + foo(&block) + end + + bar { 1 } + bar { 2 } +} + +# bmethod with forwarded block and arguments +assert_equal '5', %q{ + define_method(:foo) do |n, &block| + n + block.call + end + + def bar(n, &block) + foo(n, &block) + end + + bar(0) { 1 } + bar(3) { 2 } +} + +# bmethod with forwarded unwanted block +assert_equal '1', %q{ + one = 1 + define_method(:foo) do + one + end + + def bar(&block) + foo(&block) + end + + bar { } + bar { } +} + +# unshareable bmethod call through Method#to_proc#call +assert_equal '1000', %q{ + define_method(:bmethod) do + self + end + + Ractor.new do + errors = 0 + 1000.times do + p = method(:bmethod).to_proc + begin + p.call + rescue RuntimeError + errors += 1 + end + end + errors + end.value +} + +# test for return stub lifetime issue +assert_equal '1', %q{ + def foo(n) + if n == 2 + return 1.times { Object.define_method(:foo) {} } + end + + foo(n + 1) + end + + foo(1) +} + +# case-when with redefined === +assert_equal 'ok', %q{ + class Symbol + def ===(a) + true + end + end + + def cw(arg) + case arg + when :b + :ok + when 4 + :ng + end + end + + cw(4) +} + +assert_equal 'threw', %q{ + def foo(args) + wrap(*args) + rescue ArgumentError + 'threw' + end + + def wrap(a) + [a] + end + + foo([Hash.ruby2_keywords_hash({})]) +} + +assert_equal 'threw', %q{ + # C call + def bar(args) + Array(*args) + rescue ArgumentError + 'threw' + end + + bar([Hash.ruby2_keywords_hash({})]) +} + +# Test instance_of? and is_a? +assert_equal 'true', %q{ + 1.instance_of?(Integer) && 1.is_a?(Integer) +} + +# Test instance_of? and is_a? for singleton classes +assert_equal 'true', %q{ + a = [] + def a.test = :test + a.instance_of?(Array) && a.is_a?(Array) +} + +# Test instance_of? for singleton_class +# Yes this does really return false +assert_equal 'false', %q{ + a = [] + def a.test = :test + a.instance_of?(a.singleton_class) +} + +# Test is_a? for singleton_class +assert_equal 'true', %q{ + a = [] + def a.test = :test + a.is_a?(a.singleton_class) +} + +# Test send with splat to a cfunc +assert_equal 'true', %q{ + 1.send(:==, 1, *[]) +} + +# Test empty splat with cfunc +assert_equal '2', %q{ + def foo + Integer.sqrt(4, *[]) + end + # call twice to deal with constant exiting + foo + foo +} + +# Test non-empty splat with cfunc +assert_equal 'Hello World', %q{ + def bar + args = ["Hello "] + greeting = +"World" + greeting.insert(0, *args) + greeting + end + bar +} + +# Regression: this creates a temp stack with > 127 elements +assert_normal_exit %q{ + def foo(a) + [ + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, a, a, + a, a, a, a, a, a, a, a, + ] + end + + def entry + foo(1) + end + + entry +} + +# Test that splat and rest combined +# properly dupe the array +assert_equal "[]", %q{ + def foo(*rest) + rest << 1 + end + + def test(splat) + foo(*splat) + end + + EMPTY = [] + custom = Object.new + def custom.to_a + EMPTY + end + + test(custom) + test(custom) + EMPTY +} + +# Rest with send +assert_equal '[1, 2, 3]', %q{ + def bar(x, *rest) + rest.insert(0, x) + end + send(:bar, 1, 2, 3) +} + +# Fix splat block arg bad compilation +assert_equal "foo", %q{ + def literal(*args, &block) + s = ''.dup + literal_append(s, *args, &block) + s + end + + def literal_append(sql, v) + sql << v + end + + literal("foo") +} + +# regression test for accidentally having a parameter truncated +# due to Rust/C signature mismatch. Used to crash with +# > [BUG] rb_vm_insn_addr2insn: invalid insn address ... +# or +# > ... `Err` value: TryFromIntError(())' +assert_normal_exit %q{ + n = 16384 + eval( + "def foo(arg); " + "_=arg;" * n + '_=1;' + "Object; end" + ) + foo 1 +} + +# Regression test for CantCompile not using starting_ctx +assert_normal_exit %q{ + class Integer + def ===(other) + false + end + end + + def my_func(x) + case x + when 1 + 1 + when 2 + 2 + else + 3 + end + end + + my_func(1) +} + +# Regression test for CantCompile not using starting_ctx +assert_equal "ArgumentError", %q{ + def literal(*args, &block) + s = ''.dup + args = [1, 2, 3] + literal_append(s, *args, &block) + s + end + + def literal_append(sql, v) + [sql.inspect, v.inspect] + end + + begin + literal("foo") + rescue ArgumentError + "ArgumentError" + end +} + +# Rest with block +# Simplified code from railsbench +assert_equal '[{"/a" => "b", as: :c, via: :post}, [], nil]', %q{ + def match(path, *rest, &block) + [path, rest, block] + end + + def map_method(method, args, &block) + options = args.last + args.pop + options[:via] = method + match(*args, options, &block) + end + + def post(*args, &block) + map_method(:post, args, &block) + end + + post "/a" => "b", as: :c +} + +# Test rest and kw_args +assert_equal '[true, true, true, true]', %q{ + def my_func(*args, base: nil, sort: true) + [args, base, sort] + end + + def calling_my_func + results = [] + results << (my_func("test") == [["test"], nil, true]) + results << (my_func("test", base: :base) == [["test"], :base, true]) + results << (my_func("test", sort: false) == [["test"], nil, false]) + results << (my_func("test", "other", base: :base) == [["test", "other"], :base, true]) + results + end + calling_my_func +} + +# Test Integer#[] with 2 args +assert_equal '0', %q{ + 3[0, 0] +} + +# unspecified_bits + checkkeyword +assert_equal '2', %q{ + def callee = 1 + + # checkkeyword should see unspecified_bits=0 (use bar), not Integer 1 (set bar = foo). + def foo(foo, bar: foo) = bar + + def entry(&block) + # write 1 at stack[3]. Calling #callee spills stack[3]. + 1 + (1 + (1 + (1 + callee))) + # &block is written to a register instead of stack[3]. When &block is popped and + # unspecified_bits is pushed, it must be written to stack[3], not to a register. + foo(1, bar: 2, &block) + end + + entry # call branch_stub_hit (spill temps) + entry # doesn't call branch_stub_hit (not spill temps) +} + +# Test rest and optional_params +assert_equal '[true, true, true, true]', %q{ + def my_func(stuff, base=nil, sort=true, *args) + [stuff, base, sort, args] + end + + def calling_my_func + results = [] + results << (my_func("test") == ["test", nil, true, []]) + results << (my_func("test", :base) == ["test", :base, true, []]) + results << (my_func("test", :base, false) == ["test", :base, false, []]) + results << (my_func("test", :base, false, "other", "other") == ["test", :base, false, ["other", "other"]]) + results + end + calling_my_func +} + +# Test rest and optional_params and splat +assert_equal '[true, true, true, true, true]', %q{ + def my_func(stuff, base=nil, sort=true, *args) + [stuff, base, sort, args] + end + + def calling_my_func + results = [] + splat = ["test"] + results << (my_func(*splat) == ["test", nil, true, []]) + splat = [:base] + results << (my_func("test", *splat) == ["test", :base, true, []]) + splat = [:base, false] + results << (my_func("test", *splat) == ["test", :base, false, []]) + splat = [:base, false, "other", "other"] + results << (my_func("test", *splat) == ["test", :base, false, ["other", "other"]]) + splat = ["test", :base, false, "other", "other"] + results << (my_func(*splat) == ["test", :base, false, ["other", "other"]]) + results + end + calling_my_func +} + +# Regression test: rest and optional and splat +assert_equal 'true', %q{ + def my_func(base=nil, *args) + [base, args] + end + + def calling_my_func + array = [] + my_func(:base, :rest1, *array) == [:base, [:rest1]] + end + + calling_my_func +} + +# Fix failed case for large splat +assert_equal 'true', %q{ + def d(a, b=:b) + end + + def calling_func + ary = 1380888.times; + d(*ary) + end + begin + calling_func + rescue ArgumentError + true + end +} + +# Regression test: register allocator on expandarray +assert_equal '[]', %q{ + func = proc { [] } + proc do + _x, _y = func.call + end.call +} + +# Catch TAG_BREAK in a non-FINISH frame with JIT code +assert_equal '1', %q{ + def entry + catch_break + end + + def catch_break + while_true do + break + end + 1 + end + + def while_true + while true + yield + end + end + + entry +} + +assert_equal '6', %q{ + class Base + def number = 1 + yield + end + + class Sub < Base + def number = super + 2 + end + + Sub.new.number { 3 } +} + +# Integer multiplication and overflow +assert_equal '[6, -6, 9671406556917033397649408, -9671406556917033397649408, 21267647932558653966460912964485513216]', %q{ + def foo(a, b) + a * b + end + + r1 = foo(2, 3) + r2 = foo(2, -3) + r3 = foo(2 << 40, 2 << 41) + r4 = foo(2 << 40, -2 << 41) + r5 = foo(1 << 62, 1 << 62) + + [r1, r2, r3, r4, r5] +} + +# Integer multiplication and overflow (minimized regression test from test-basic) +assert_equal '8515157028618240000', %q{2128789257154560000 * 4} + +# Inlined method calls +assert_equal 'nil', %q{ + def putnil = nil + def entry = putnil + entry.inspect +} +assert_equal '1', %q{ + def putobject_1 = 1 + def entry = putobject_1 + entry +} +assert_equal 'false', %q{ + def putobject(_unused_arg1) = false + def entry = putobject(nil) + entry +} +assert_equal 'true', %q{ + def entry = yield + entry { true } +} +assert_equal 'sym', %q{ + def entry = :sym.to_sym + entry +} + +assert_normal_exit %q{ + ivars = 1024.times.map { |i| "@iv_#{i} = #{i}\n" }.join + Foo = Class.new + Foo.class_eval "def initialize() #{ivars} end" + Foo.new +} + +assert_equal '0', %q{ + def spill + 1.to_i # not inlined + end + + def inline(_stack1, _stack2, _stack3, _stack4, _stack5) + 0 # inlined + end + + def entry + # RegTemps is 00111110 prior to the #inline call. + # Its return value goes to stack_idx=0, which conflicts with stack_idx=5. + inline(spill, 2, 3, 4, 5) + end + + entry +} + +# Integer succ and overflow +assert_equal '[2, 4611686018427387904]', %q{ + [1.succ, 4611686018427387903.succ] +} + +# Integer pred and overflow +assert_equal '[0, -4611686018427387905]', %q{ + [1.pred, -4611686018427387904.pred] +} + +# Integer right shift +assert_equal '[0, 1, -4]', %q{ + [0 >> 1, 2 >> 1, -7 >> 1] +} + +# Integer XOR +assert_equal '[0, 0, 4]', %q{ + [0 ^ 0, 1 ^ 1, 7 ^ 3] +} + +assert_equal '[nil, "yield"]', %q{ + def defined_yield = defined?(yield) + [defined_yield, defined_yield {}] +} + +# splat with ruby2_keywords into rest parameter +assert_equal '[[{a: 1}], {}]', %q{ + ruby2_keywords def foo(*args) = args + + def bar(*args, **kw) = [args, kw] + + def pass_bar(*args) = bar(*args) + + def body + args = foo(a: 1) + pass_bar(*args) + end + + body +} + +# concatarray +assert_equal '[1, 2]', %q{ + def foo(a, b) = [a, b] + arr = [2] + foo(*[1], *arr) +} + +# pushtoarray +assert_equal '[1, 2]', %q{ + def foo(a, b) = [a, b] + arr = [1] + foo(*arr, 2) +} + +# pop before fallback +assert_normal_exit %q{ + class Foo + attr_reader :foo + + def try = foo(0, &nil) + end + + Foo.new.try +} + +# a kwrest case +assert_equal '[1, 2, {complete: false}]', %q{ + def rest(foo: 1, bar: 2, **kwrest) + [foo, bar, kwrest] + end + + def callsite = rest(complete: false) + + callsite +} + +# splat+kw_splat+opt+rest +assert_equal '[1, []]', %q{ + def opt_rest(a = 0, *rest) = [a, rest] + + def call_site(args) = opt_rest(*args, **nil) + + call_site([1]) +} + +# splat and nil kw_splat +assert_equal 'ok', %q{ + def identity(x) = x + + def splat_nil_kw_splat(args) = identity(*args, **nil) + + splat_nil_kw_splat([:ok]) +} + +# empty splat and kwsplat into leaf builtins +assert_equal '[1, 1, 1]', %q{ + empty = [] + [1.abs(*empty), 1.abs(**nil), 1.bit_length(*empty, **nil)] +} + +# splat into C methods with -1 arity +assert_equal '[[1, 2, 3], [0, 2, 3], [1, 2, 3], [2, 2, 3], [], [], [{}]]', %q{ + class Foo < Array + def push(args) = super(1, *args) + end + + def test_cfunc_vargs_splat(sub_instance, array_class, empty_kw_hash) + splat = [2, 3] + kw_splat = [empty_kw_hash] + [ + sub_instance.push(splat), + array_class[0, *splat, **nil], + array_class[1, *splat, &nil], + array_class[2, *splat, **nil, &nil], + array_class.send(:[], *kw_splat), + # kw_splat disables keywords hash handling + array_class[*kw_splat], + array_class[*kw_splat, **nil], + ] + end + + test_cfunc_vargs_splat(Foo.new, Array, Hash.ruby2_keywords_hash({})) +} + +# Class#new (arity=-1), splat, and ruby2_keywords +assert_equal '[0, {1 => 1}]', %q{ + class KwInit + attr_reader :init_args + def initialize(x = 0, **kw) + @init_args = [x, kw] + end + end + + def test(klass, args) + klass.new(*args).init_args + end + + test(KwInit, [Hash.ruby2_keywords_hash({1 => 1})]) +} + +# Chilled string setivar trigger warning +assert_match(/literal string will be frozen in the future/, %q{ + Warning[:deprecated] = true + $VERBOSE = true + $warning = "no-warning" + module ::Warning + def self.warn(message) + $warning = message.split("warning: ").last.strip + end + end + + class String + def setivar! + @ivar = 42 + end + end + + def setivar!(str) + str.setivar! + end + + 10.times { setivar!("mutable".dup) } + 10.times do + setivar!("frozen".freeze) + rescue FrozenError + end + + setivar!("chilled") # Emit warning + $warning +}) + +# arity=-2 cfuncs +assert_equal '["", "1/2", [0, [:ok, 1]]]', %q{ + def test_cases(file, chain) + new_chain = chain.allocate # to call initialize directly + new_chain.send(:initialize, [0], ok: 1) + + [ + file.join, + file.join("1", "2"), + new_chain.to_a, + ] + end + + test_cases(File, Enumerator::Chain) +} + +# singleton class should invalidate Type::CString assumption +assert_equal 'foo', %q{ + def define_singleton(str, define) + if define + # Wrap a C method frame to avoid exiting JIT code on defineclass + [nil].reverse_each do + class << str + def +(_) + "foo" + end + end + end + end + "bar" + end + + def entry(define) + str = "" + # When `define` is false, #+ compiles to rb_str_plus() without a class guard. + # When the code is reused with `define` is true, the class of `str` is changed + # to a singleton class, so the block should be invalidated. + str + define_singleton(str, define) + end + + entry(false) + entry(true) +} + +assert_equal 'ok', %q{ + def ok + :ok + end + + def delegator(...) + ok(...) + end + + def caller + send(:delegator) + end + + caller +} + +# test inlining of simple iseqs +assert_equal '[:ok, :ok, :ok]', %q{ + def identity(x) = x + def foo(x, _) = x + def bar(_, _, _, _, x) = x + + def tests + [ + identity(:ok), + foo(:ok, 2), + bar(1, 2, 3, 4, :ok), + ] + end + + tests +} + +# test inlining of simple iseqs with kwargs +assert_equal '[:ok, :ok, :ok, :ok, :ok]', %q{ + def optional_unused(x, opt: :not_ok) = x + def optional_used(x, opt: :ok) = opt + def required_unused(x, req:) = x + def required_used(x, req:) = req + def unknown(x) = x + + def tests + [ + optional_unused(:ok), + optional_used(:not_ok), + required_unused(:ok, req: :not_ok), + required_used(:not_ok, req: :ok), + begin unknown(:not_ok, unknown_kwarg: :not_ok) rescue ArgumentError; :ok end, + ] + end + + tests +} + +# test simple iseqs not eligible for inlining +assert_equal '[:ok, :ok, :ok, :ok, :ok]', %q{ + def identity(x) = x + def arg_splat(x, *args) = x + def kwarg_splat(x, **kwargs) = x + def block_arg(x, &blk) = x + def block_iseq(x) = x + def call_forwarding(...) = identity(...) + + def tests + [ + arg_splat(:ok), + kwarg_splat(:ok), + block_arg(:ok, &proc { :not_ok }), + block_iseq(:ok) { :not_ok }, + call_forwarding(:ok), + ] + end + + tests +} + +# regression test for splat with &proc{} when the target has rest (Bug #21266) +assert_equal '[]', %q{ + def foo(args) = bar(*args, &proc { _1 }) + def bar(_, _, _, _, *rest) = yield rest + + GC.stress = true + foo([1,2,3,4]) + foo([1,2,3,4]) +} + +# regression test for invalidating an empty block +assert_equal '0', %q{ + def foo = (* = 1).pred + + foo # compile it + + class Integer + def to_ary = [] # invalidate + end + + foo # try again +} + +# test integer left shift with constant rhs +assert_equal [0x80000000000, 'a+', :ok].inspect, %q{ + def shift(val) = val << 43 + + def tests + int = shift(1) + str = shift("a") + + Integer.define_method(:<<) { |_| :ok } + redef = shift(1) + + [int, str, redef] + end + + tests +} + +# test integer left shift fusion followed by opt_getconstant_path +assert_equal '33', %q{ + def test(a) + (a << 5) | (Object; a) + end + + test(1) +} + +# test String#stebyte with arguments that need conversion +assert_equal "abc", %q{ + str = +"a00" + def change_bytes(str, one, two) + str.setbyte(one, "b".ord) + str.setbyte(2, two) + end + + to_int_1 = Object.new + to_int_99 = Object.new + def to_int_1.to_int = 1 + def to_int_99.to_int = 99 + + change_bytes(str, to_int_1, to_int_99) + str +} + +# test --yjit-verify-ctx for arrays with a singleton class +assert_equal "ok", %q{ + class Array + def foo + self.singleton_class.define_method(:first) { :ok } + first + end + end + + def test = [].foo + + test +} + +assert_equal '["raised", "Module", "Object"]', %q{ + def foo(obj) + obj.superclass.name + end + + ret = [] + + begin + foo(Class.allocate) + rescue TypeError + ret << 'raised' + end + + ret += [foo(Class), foo(Class.new)] +} + +# test TrueClass#=== before and after redefining TrueClass#== +assert_equal '[[true, false, false], [true, true, false], [true, :error, :error]]', %q{ + def true_eqq(x) + true === x + rescue NoMethodError + :error + end + + def test + [ + # first one is always true because rb_equal does object comparison before calling #== + true_eqq(true), + # these will use TrueClass#== + true_eqq(false), + true_eqq(:truthy), + ] + end + + results = [test] + + class TrueClass + def ==(x) + !x + end + end + + results << test + + class TrueClass + undef_method :== + end + + results << test +} + +# test FalseClass#=== before and after redefining FalseClass#== +assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]]', %q{ + def case_equal(x, y) + x === y + rescue NoMethodError + :error + end + + def test + [ + # first one is always true because rb_equal does object comparison before calling #== + case_equal(false, false), + # these will use #== + case_equal(false, true), + case_equal(false, nil), + ] + end + + results = [test] + + class FalseClass + def ==(x) + !x + end + end + + results << test + + class FalseClass + undef_method :== + end + + results << test +} + +# test NilClass#=== before and after redefining NilClass#== +assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]]', %q{ + def case_equal(x, y) + x === y + rescue NoMethodError + :error + end + + def test + [ + # first one is always true because rb_equal does object comparison before calling #== + case_equal(nil, nil), + # these will use #== + case_equal(nil, true), + case_equal(nil, false), + ] + end + + results = [test] + + class NilClass + def ==(x) + !x + end + end + + results << test + + class NilClass + undef_method :== + end + + results << test +} + +# test struct accessors fire c_call events +assert_equal '[[:c_call, :x=], [:c_call, :x]]', %q{ + c = Struct.new(:x) + obj = c.new + + events = [] + TracePoint.new(:c_call) do + events << [_1.event, _1.method_id] + end.enable do + obj.x = 100 + obj.x + end + + events +} + +# regression test for splatting empty array +assert_equal '1', %q{ + def callee(foo) = foo + + def test_body(args) = callee(1, *args) + + test_body([]) + array = Array.new(100) + array.clear + test_body(array) +} + +# regression test for splatting empty array to cfunc +assert_normal_exit %q{ + def test_body(args) = Array(1, *args) + + test_body([]) + 0x100.times do + array = Array.new(100) + array.clear + test_body(array) + end +} + +# compiling code shouldn't emit warnings as it may call into more Ruby code +assert_equal 'ok', <<~'RUBY' + # [Bug #20522] + $VERBOSE = true + Warning[:performance] = true + + module StrictWarnings + def warn(msg, **) + raise msg + end + end + Warning.singleton_class.prepend(StrictWarnings) + + class A + def compiled_method(is_private) + @some_ivar = is_private + end + end + + shape_max_variations = 8 + if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS) && RubyVM::Shape::SHAPE_MAX_VARIATIONS != shape_max_variations + raise "Expected SHAPE_MAX_VARIATIONS to be #{shape_max_variations}, got: #{RubyVM::Shape::SHAPE_MAX_VARIATIONS}" + end + + 100.times do |i| + klass = Class.new(A) + (shape_max_variations - 1).times do |j| + obj = klass.new + obj.instance_variable_set("@base_#{i}", 42) + obj.instance_variable_set("@ivar_#{j}", 42) + end + obj = klass.new + obj.instance_variable_set("@base_#{i}", 42) + begin + obj.compiled_method(true) + rescue + # expected + end + end + + :ok +RUBY + +assert_equal 'ok', <<~'RUBY' + class MyRelation + def callee(...) + :ok + end + + def uncached(...) + callee(...) + end + + def takes_block(&block) + # push blockhandler + uncached(&block) # CI1 + end + end + + relation = MyRelation.new + relation.takes_block { } +RUBY + +assert_equal 'ok', <<~'RUBY' + def _exec_scope(...) + instance_exec(...) + end + + def ok args, body + _exec_scope(*args, &body) + end + + ok([], -> { "ok" }) +RUBY + +assert_equal 'ok', <<~'RUBY' + def _exec_scope(...) + instance_exec(...) + end + + def ok args, body + _exec_scope(*args, &body) + end + + ok(["ok"], ->(x) { x }) +RUBY + +assert_equal 'ok', <<~'RUBY' +def baz(a, b) + a + b +end + +def bar(...) + baz(...) +end + +def foo(a, ...) + bar(a, ...) +end + +def test + foo("o", "k") +end + +test +RUBY + +# opt_newarray_send pack/buffer +assert_equal '[true, true]', <<~'RUBY' + def pack + v = 1.23 + [v, v*2, v*3].pack("E*").unpack("E*") == [v, v*2, v*3] + end + + def with_buffer + v = 4.56 + b = +"x" + [v, v*2, v*3].pack("E*", buffer: b) + b[1..].unpack("E*") == [v, v*2, v*3] + end + + [pack, with_buffer] +RUBY + +# String#[] / String#slice +assert_equal 'ok', <<~'RUBY' + def error(klass) + yield + rescue klass + true + end + + def test + str = "こんにちは" + substr = "にち" + failures = [] + + # Use many small statements to keep context for each slice call smaller than MAX_CTX_TEMPS + + str[1] == "ん" && str.slice(4) == "は" || failures << :index + str[5].nil? && str.slice(5).nil? || failures << :index_end + + str[1, 2] == "んに" && str.slice(2, 1) == "に" || failures << :beg_len + str[5, 1] == "" && str.slice(5, 1) == "" || failures << :beg_len_end + + str[1..2] == "んに" && str.slice(2..2) == "に" || failures << :range + + str[/に./] == "にち" && str.slice(/に./) == "にち" || failures << :regexp + + str[/に./, 0] == "にち" && str.slice(/に./, 0) == "にち" || failures << :regexp_cap0 + + str[/に(.)/, 1] == "ち" && str.slice(/に(.)/, 1) == "ち" || failures << :regexp_cap1 + + str[substr] == substr && str.slice(substr) == substr || failures << :substr + + error(TypeError) { str[Object.new] } && error(TypeError) { str.slice(Object.new, 1) } || failures << :type_error + error(RangeError) { str[Float::INFINITY] } && error(RangeError) { str.slice(Float::INFINITY) } || failures << :range_error + + return "ok" if failures.empty? + {failures: failures} + end + + test +RUBY + +# opt_duparray_send :include? +assert_equal '[true, false]', <<~'RUBY' + def test(x) + [:a, :b].include?(x) + end + + [ + test(:b), + test(:c), + ] +RUBY + +# opt_newarray_send :include? +assert_equal '[true, false]', <<~'RUBY' + def test(x) + [Object.new, :a, :b].include?(x.to_sym) + end + + [ + test("b"), + test("c"), + ] +RUBY + +# YARV: swap and opt_reverse +assert_equal '["x", "Y", "c", "A", "t", "A", "b", "C", "d"]', <<~'RUBY' + class Swap + def initialize(s) + @a, @b, @c, @d = s.split("") + end + + def swap + a, b = @a, @b + b = b.upcase + @a, @b = a, b + end + + def reverse_odd + a, b, c = @a, @b, @c + b = b.upcase + @a, @b, @c = a, b, c + end + + def reverse_even + a, b, c, d = @a, @b, @c, @d + a = a.upcase + c = c.upcase + @a, @b, @c, @d = a, b, c, d + end + end + + Swap.new("xy").swap + Swap.new("cat").reverse_odd + Swap.new("abcd").reverse_even +RUBY + +assert_normal_exit %{ + class Bug20997 + def foo(&) = self.class.name(&) + + new.foo + end +} + +# This used to trigger a "try to mark T_NONE" +# due to an uninitialized local in foo. +assert_normal_exit %{ + def foo(...) + _local_that_should_nil_on_call = GC.start + end + + def test_bug21021 + puts [], [], [], [], [], [] + foo [] + end + + GC.stress = true + test_bug21021 +} + +assert_equal 'nil', %{ + def foo(...) + _a = _b = _c = binding.local_variable_get(:_c) + + _c + end + + # [Bug #21021] + def test_local_fill_in_forwardable + puts [], [], [], [], [] + foo [] + end + + test_local_fill_in_forwardable.inspect +} + +# Test defined?(yield) and block_given? in non-method context. +# It's good that the body of this runs at true top level and isn't wrapped in a block. +assert_equal 'false', %{ + RESULT = [] + RESULT << defined?(yield) + RESULT << block_given? + + 1.times do + RESULT << defined?(yield) + RESULT << block_given? + end + + module ModuleContext + 1.times do + RESULT << defined?(yield) + RESULT << block_given? + end + end + + class << self + RESULT << defined?(yield) + RESULT << block_given? + end + + RESULT.any? +} + +# throw and String#dup with GC stress +assert_equal 'foo', %{ + GC.stress = true + + def foo + 1.times { return "foo".dup } + end + + 10.times.map { foo.dup }.last +} + +# regression test for [Bug #21772] +# local variable type tracking desync +assert_normal_exit %q{ + def some_method = 0 + + def test_body(key) + some_method + key = key.to_s # setting of local relevant + + key == "symbol" + end + + def jit_caller = test_body("session_id") + + jit_caller # first iteration, non-escaped environment + alias some_method binding # induce environment escape + test_body(:symbol) +} + +# regression test for missing check in identity method inlining +assert_normal_exit %q{ + # Use dead code (if false) to create a local + # without initialization instructions. + def foo(a) + if false + x = nil + end + x + end + def test = foo(1) + test + test +} diff --git a/bootstraptest/test_yjit_rust_port.rb b/bootstraptest/test_yjit_rust_port.rb new file mode 100644 index 0000000000..2dbcebc03a --- /dev/null +++ b/bootstraptest/test_yjit_rust_port.rb @@ -0,0 +1,422 @@ +# Simple tests that we know we can pass +# To keep track of what we got working during the Rust port +# And avoid breaking/losing functionality +# +# Say "Thread" here to dodge WASM CI check. We use ractors here +# which WASM doesn't support and it only greps for "Thread". + +# Test for opt_mod +assert_equal '2', %q{ + def mod(a, b) + a % b + end + + mod(7, 5) + mod(7, 5) +} + +# Test for opt_mult +assert_equal '12', %q{ + def mult(a, b) + a * b + end + + mult(6, 2) + mult(6, 2) +} + +# Test for opt_div +assert_equal '3', %q{ + def div(a, b) + a / b + end + + div(6, 2) + div(6, 2) +} + +assert_equal '5', %q{ + def plus(a, b) + a + b + end + + plus(3, 2) +} + +assert_equal '1', %q{ + def foo(a, b) + a - b + end + + foo(3, 2) +} + +assert_equal 'true', %q{ + def foo(a, b) + a < b + end + + foo(2, 3) +} + +# Bitwise left shift +assert_equal '4', %q{ + def foo(a, b) + 1 << 2 + end + + foo(1, 2) +} + +assert_equal '-7', %q{ + def foo(a, b) + -7 + end + + foo(1, 2) +} + +# Putstring +assert_equal 'foo', %q{ + def foo(a, b) + "foo" + end + + foo(1, 2) +} + +assert_equal '-6', %q{ + def foo(a, b) + a + -7 + end + + foo(1, 2) +} + +assert_equal 'true', %q{ + def foo(a, b) + a == b + end + + foo(3, 3) +} + +assert_equal 'true', %q{ + def foo(a, b) + a < b + end + + foo(3, 5) +} + +assert_equal '777', %q{ + def foo(a) + if a + 777 + else + 333 + end + end + + foo(true) +} + +assert_equal '5', %q{ + def foo(a, b) + while a < b + a += 1 + end + a + end + + foo(1, 5) +} + +# opt_aref +assert_equal '2', %q{ + def foo(a, b) + a[b] + end + + foo([0, 1, 2], 2) +} + +# Simple function calls with 0, 1, 2 arguments +assert_equal '-2', %q{ + def bar() + -2 + end + + def foo(a, b) + bar() + end + + foo(3, 2) +} +assert_equal '2', %q{ + def bar(a) + a + end + + def foo(a, b) + bar(b) + end + + foo(3, 2) +} +assert_equal '1', %q{ + def bar(a, b) + a - b + end + + def foo(a, b) + bar(a, b) + end + + foo(3, 2) +} + +# Regression test for assembler bug +assert_equal '1', %q{ + def check_index(index) + if 0x40000000 < index + return -1 + end + 1 + end + + check_index 2 +} + +# Setivar test +assert_equal '2', %q{ + class Klass + attr_accessor :a + + def set() + @a = 2 + end + + def get() + @a + end + end + + o = Klass.new + o.set() + o.a +} + +# Regression for putobject bug +assert_equal '1.5', %q{ + def foo(x) + x + end + + def bar + foo(1.5) + end + + bar() +} + +# Getivar with an extended ivar table +assert_equal '3', %q{ + class Foo + def initialize + @x1 = 1 + @x2 = 1 + @x3 = 1 + @x4 = 3 + end + + def bar + @x4 + end + end + + f = Foo.new + f.bar +} + +assert_equal 'true', %q{ + x = [[false, true]] + for i, j in x + ; + end + j +} + +# Regression for getivar +assert_equal '[nil]', %q{ + [TrueClass].each do |klass| + klass.class_eval("def foo = @foo") + end + + [true].map do |instance| + instance.foo + end +} + +# Regression for send +assert_equal 'ok', %q{ + def bar(baz: 2) + baz + end + + def foo + bar(1, baz: 123) + end + + begin + foo + foo + rescue ArgumentError => e + print "ok" + end +} + +# Array access regression test +assert_equal '[0, 1, 2, 3, 4, 5]', %q{ + def expandarray_useless_splat + arr = [0, 1, 2, 3, 4, 5] + a, * = arr + end + + expandarray_useless_splat +} + +# Make sure we're correctly reading RStruct's as.ary union for embedded RStructs +assert_equal '3,12', %q{ + pt_struct = Struct.new(:x, :y) + p = pt_struct.new(3, 12) + def pt_inspect(pt) + "#{pt.x},#{pt.y}" + end + + # Make sure pt_inspect is JITted + 10.times { pt_inspect(p) } + + # Make sure it's returning '3,12' instead of e.g. '3,false' + pt_inspect(p) +} + +assert_equal '2', %q{ + def foo(s) + s.foo + end + + S = Struct.new(:foo) + foo(S.new(1)) + foo(S.new(2)) +} + +# Try to compile new method while OOM +assert_equal 'ok', %q{ + def foo + :ok + end + + RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT) + + foo +} + +# test hitting a branch stub when out of memory +assert_equal 'ok', %q{ + def nimai(jita) + if jita + :ng + else + :ok + end + end + + nimai(true) + nimai(true) + + RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT) + + nimai(false) +} + +# Ractor.current returns a current ractor +assert_equal 'Ractor', %q{ + Ractor.current.class +} + +# Ractor.new returns new Ractor +assert_equal 'Ractor', %q{ + Ractor.new{}.class +} + +# Ractor.allocate is not supported +assert_equal "[:ok, :ok]", %q{ + rs = [] + begin + Ractor.allocate + rescue => e + rs << :ok if e.message == 'allocator undefined for Ractor' + end + + begin + Ractor.new{}.dup + rescue + rs << :ok if e.message == 'allocator undefined for Ractor' + end + + rs +} + +# A return value of a Ractor block will be a message from the Ractor. +assert_equal 'ok', %q{ + # join + r = Ractor.new do + 'ok' + end + r.value +} + +# Passed arguments to Ractor.new will be a block parameter +# The values are passed with Ractor-communication pass. +assert_equal 'ok', %q{ + # ping-pong with arg + r = Ractor.new 'ok' do |msg| + msg + end + r.value +} + +# Pass multiple arguments to Ractor.new +assert_equal 'ok', %q{ + # ping-pong with two args + r = Ractor.new 'ping', 'pong' do |msg, msg2| + [msg, msg2] + end + 'ok' if r.value == ['ping', 'pong'] +} + +# Ractor#send passes an object with copy to a Ractor +# and Ractor.receive in the Ractor block can receive the passed value. +assert_equal 'ok', %q{ + r = Ractor.new do + msg = Ractor.receive + end + r.send 'ok' + r.value +} + +assert_equal '[1, 2, 3]', %q{ + def foo(arr) + arr << 1 + arr << 2 + arr << 3 + arr + end + + def bar() + foo([]) + end + + bar() +} @@ -0,0 +1,1220 @@ +/* indent-tabs-mode: nil */ + +#include "eval_intern.h" +#include "internal.h" +#include "internal/box.h" +#include "internal/class.h" +#include "internal/eval.h" +#include "internal/error.h" +#include "internal/file.h" +#include "internal/gc.h" +#include "internal/hash.h" +#include "internal/io.h" +#include "internal/load.h" +#include "internal/st.h" +#include "internal/variable.h" +#include "iseq.h" +#include "ruby/internal/globals.h" +#include "ruby/util.h" +#include "vm_core.h" +#include "darray.h" + +#include <stdio.h> + +#ifdef HAVE_SYS_SENDFILE_H +# include <sys/sendfile.h> +#endif +#ifdef HAVE_COPYFILE_H +#include <copyfile.h> +#endif + +VALUE rb_cBox = 0; +VALUE rb_cBoxEntry = 0; +VALUE rb_mBoxLoader = 0; + +static rb_box_t root_box[1]; /* Initialize in initialize_root_box() */ +static rb_box_t *main_box; +static char *tmp_dir; +static bool tmp_dir_has_dirsep; + +#define BOX_TMP_PREFIX "_ruby_box_" + +#ifndef MAXPATHLEN +# define MAXPATHLEN 1024 +#endif + +#if defined(_WIN32) +# define DIRSEP "\\" +#else +# define DIRSEP "/" +#endif + +bool ruby_box_enabled = false; // extern +bool ruby_box_init_done = false; // extern +bool ruby_box_crashed = false; // extern, changed only in vm.c + +VALUE rb_resolve_feature_path(VALUE klass, VALUE fname); +static VALUE rb_box_inspect(VALUE obj); +static void cleanup_all_local_extensions(VALUE libmap); + +void +rb_box_init_done(void) +{ + ruby_box_init_done = true; +} + +const rb_box_t * +rb_root_box(void) +{ + return root_box; +} + +const rb_box_t * +rb_main_box(void) +{ + return main_box; +} + +const rb_box_t * +rb_current_box(void) +{ + /* + * If RUBY_BOX is not set, the root box is the only available one. + * + * Until the main_box is not initialized, the root box is + * the only valid box. + * This early return is to avoid accessing EC before its setup. + */ + if (!main_box) + return root_box; + + return rb_vm_current_box(GET_EC()); +} + +const rb_box_t * +rb_loading_box(void) +{ + if (!main_box) + return root_box; + + return rb_vm_loading_box(GET_EC()); +} + +const rb_box_t * +rb_current_box_in_crash_report(void) +{ + if (ruby_box_crashed) + return NULL; + return rb_current_box(); +} + +static long box_id_counter = 0; + +static long +box_generate_id(void) +{ + long id; + RB_VM_LOCKING() { + id = ++box_id_counter; + } + return id; +} + +static VALUE +box_main_to_s(VALUE obj) +{ + return rb_str_new2("main"); +} + +static void +box_entry_initialize(rb_box_t *box) +{ + const rb_box_t *root = rb_root_box(); + + // These will be updated immediately + box->box_object = 0; + box->box_id = 0; + + box->top_self = rb_obj_alloc(rb_cObject); + rb_define_singleton_method(box->top_self, "to_s", box_main_to_s, 0); + rb_define_alias(rb_singleton_class(box->top_self), "inspect", "to_s"); + box->load_path = rb_ary_dup(root->load_path); + box->expanded_load_path = rb_ary_dup(root->expanded_load_path); + box->load_path_snapshot = rb_ary_new(); + box->load_path_check_cache = 0; + box->loaded_features = rb_ary_dup(root->loaded_features); + box->loaded_features_snapshot = rb_ary_new(); + box->loaded_features_index = st_init_numtable(); + box->loaded_features_realpaths = rb_hash_dup(root->loaded_features_realpaths); + box->loaded_features_realpath_map = rb_hash_dup(root->loaded_features_realpath_map); + box->loading_table = st_init_strtable(); + box->ruby_dln_libmap = rb_hash_new_with_size(0); + box->gvar_tbl = rb_hash_new_with_size(0); + box->classext_cow_classes = st_init_numtable(); + + box->is_user = true; + box->is_optional = true; +} + +void +rb_box_gc_update_references(void *ptr) +{ + rb_box_t *box = (rb_box_t *)ptr; + if (!box) return; + + if (box->box_object) + box->box_object = rb_gc_location(box->box_object); + if (box->top_self) + box->top_self = rb_gc_location(box->top_self); + box->load_path = rb_gc_location(box->load_path); + box->expanded_load_path = rb_gc_location(box->expanded_load_path); + box->load_path_snapshot = rb_gc_location(box->load_path_snapshot); + if (box->load_path_check_cache) { + box->load_path_check_cache = rb_gc_location(box->load_path_check_cache); + } + box->loaded_features = rb_gc_location(box->loaded_features); + box->loaded_features_snapshot = rb_gc_location(box->loaded_features_snapshot); + box->loaded_features_realpaths = rb_gc_location(box->loaded_features_realpaths); + box->loaded_features_realpath_map = rb_gc_location(box->loaded_features_realpath_map); + box->ruby_dln_libmap = rb_gc_location(box->ruby_dln_libmap); + box->gvar_tbl = rb_gc_location(box->gvar_tbl); +} + +void +rb_box_entry_mark(void *ptr) +{ + const rb_box_t *box = (rb_box_t *)ptr; + if (!box) return; + + rb_gc_mark(box->box_object); + rb_gc_mark(box->top_self); + rb_gc_mark(box->load_path); + rb_gc_mark(box->expanded_load_path); + rb_gc_mark(box->load_path_snapshot); + rb_gc_mark(box->load_path_check_cache); + rb_gc_mark(box->loaded_features); + rb_gc_mark(box->loaded_features_snapshot); + rb_gc_mark(box->loaded_features_realpaths); + rb_gc_mark(box->loaded_features_realpath_map); + if (box->loading_table) { + rb_mark_tbl(box->loading_table); + } + rb_gc_mark(box->ruby_dln_libmap); + rb_gc_mark(box->gvar_tbl); + if (box->classext_cow_classes) { + rb_mark_tbl(box->classext_cow_classes); + } +} + +static int +free_loading_table_entry(st_data_t key, st_data_t value, st_data_t arg) +{ + xfree((char *)key); + return ST_DELETE; +} + +static int +free_loaded_feature_index_i(st_data_t key, st_data_t value, st_data_t arg) +{ + if (!FIXNUM_P(value)) { + rb_darray_free((void *)value); + } + return ST_CONTINUE; +} + +static void +box_root_free(void *ptr) +{ + rb_box_t *box = (rb_box_t *)ptr; + if (box->loading_table) { + st_foreach(box->loading_table, free_loading_table_entry, 0); + st_free_table(box->loading_table); + box->loading_table = 0; + } + + if (box->loaded_features_index) { + st_foreach(box->loaded_features_index, free_loaded_feature_index_i, 0); + st_free_table(box->loaded_features_index); + } +} + +static int +free_classext_for_box(st_data_t _key, st_data_t obj_value, st_data_t box_arg) +{ + rb_classext_t *ext; + VALUE obj = (VALUE)obj_value; + const rb_box_t *box = (const rb_box_t *)box_arg; + + if (RB_TYPE_P(obj, T_CLASS) || RB_TYPE_P(obj, T_MODULE)) { + ext = rb_class_unlink_classext(obj, box); + rb_class_classext_free(obj, ext, false); + } + else if (RB_TYPE_P(obj, T_ICLASS)) { + ext = rb_class_unlink_classext(obj, box); + rb_iclass_classext_free(obj, ext, false); + } + else { + rb_bug("Invalid type of object in classext_cow_classes: %s", rb_type_str(BUILTIN_TYPE(obj))); + } + return ST_CONTINUE; +} + +static void +box_entry_free(void *ptr) +{ + const rb_box_t *box = (const rb_box_t *)ptr; + + if (box->classext_cow_classes) { + st_foreach(box->classext_cow_classes, free_classext_for_box, (st_data_t)box); + } + + cleanup_all_local_extensions(box->ruby_dln_libmap); + + box_root_free(ptr); + xfree(ptr); +} + +static size_t +box_entry_memsize(const void *ptr) +{ + size_t size = sizeof(rb_box_t); + const rb_box_t *box = (const rb_box_t *)ptr; + if (box->loaded_features_index) { + size += rb_st_memsize(box->loaded_features_index); + } + if (box->loading_table) { + size += rb_st_memsize(box->loading_table); + } + return size; +} + +static const rb_data_type_t rb_box_data_type = { + "Ruby::Box::Entry", + { + rb_box_entry_mark, + box_entry_free, + box_entry_memsize, + rb_box_gc_update_references, + }, + 0, 0, RUBY_TYPED_FREE_IMMEDIATELY // TODO: enable RUBY_TYPED_WB_PROTECTED when inserting write barriers +}; + +static const rb_data_type_t rb_root_box_data_type = { + "Ruby::Box::Root", + { + rb_box_entry_mark, + box_root_free, + box_entry_memsize, + rb_box_gc_update_references, + }, + &rb_box_data_type, 0, RUBY_TYPED_FREE_IMMEDIATELY // TODO: enable RUBY_TYPED_WB_PROTECTED when inserting write barriers +}; + +VALUE +rb_box_entry_alloc(VALUE klass) +{ + rb_box_t *entry; + VALUE obj = TypedData_Make_Struct(klass, rb_box_t, &rb_box_data_type, entry); + box_entry_initialize(entry); + return obj; +} + +static rb_box_t * +get_box_struct_internal(VALUE entry) +{ + rb_box_t *sval; + TypedData_Get_Struct(entry, rb_box_t, &rb_box_data_type, sval); + return sval; +} + +rb_box_t * +rb_get_box_t(VALUE box) +{ + VALUE entry; + ID id_box_entry; + + VM_ASSERT(box); + + if (NIL_P(box)) + return root_box; + + VM_ASSERT(BOX_OBJ_P(box)); + + CONST_ID(id_box_entry, "__box_entry__"); + entry = rb_attr_get(box, id_box_entry); + return get_box_struct_internal(entry); +} + +VALUE +rb_get_box_object(rb_box_t *box) +{ + VM_ASSERT(box && box->box_object); + return box->box_object; +} + +/* + * call-seq: + * Ruby::Box.new -> new_box + * + * Returns a new Ruby::Box object. + */ +static VALUE +box_initialize(VALUE box_value) +{ + rb_box_t *box; + rb_classext_t *object_classext; + VALUE entry; + ID id_box_entry; + CONST_ID(id_box_entry, "__box_entry__"); + + if (!rb_box_available()) { + rb_raise(rb_eRuntimeError, "Ruby Box is disabled. Set RUBY_BOX=1 environment variable to use Ruby::Box."); + } + + entry = rb_class_new_instance_pass_kw(0, NULL, rb_cBoxEntry); + box = get_box_struct_internal(entry); + + box->box_object = box_value; + box->box_id = box_generate_id(); + rb_define_singleton_method(box->load_path, "resolve_feature_path", rb_resolve_feature_path, 1); + + // Set the Ruby::Box object unique/consistent from any boxes to have just single + // constant table from any view of every (including main) box. + // If a code in the box adds a constant, the constant will be visible even from root/main. + RCLASS_SET_PRIME_CLASSEXT_WRITABLE(box_value, true); + + // Get a clean constant table of Object even by writable one + // because ns was just created, so it has not touched any constants yet. + object_classext = RCLASS_EXT_WRITABLE_IN_BOX(rb_cObject, box); + RCLASS_SET_CONST_TBL(box_value, RCLASSEXT_CONST_TBL(object_classext), true); + + rb_ivar_set(box_value, id_box_entry, entry); + + return box_value; +} + +/* + * call-seq: + * Ruby::Box.enabled? -> true or false + * + * Returns +true+ if Ruby::Box is enabled. + */ +static VALUE +rb_box_s_getenabled(VALUE recv) +{ + return RBOOL(rb_box_available()); +} + +/* + * call-seq: + * Ruby::Box.current -> box, nil or false + * + * Returns the current box. + * Returns +nil+ if Ruby Box is not enabled. + */ +static VALUE +rb_box_s_current(VALUE recv) +{ + const rb_box_t *box; + + if (!rb_box_available()) + return Qnil; + + box = rb_vm_current_box(GET_EC()); + VM_ASSERT(box && box->box_object); + return box->box_object; +} + +/* + * call-seq: + * load_path -> array + * + * Returns box local load path. + */ +static VALUE +rb_box_load_path(VALUE box) +{ + VM_ASSERT(BOX_OBJ_P(box)); + return rb_get_box_t(box)->load_path; +} + +#ifdef _WIN32 +UINT rb_w32_system_tmpdir(WCHAR *path, UINT len); +#endif + +/* Copied from mjit.c Ruby 3.0.3 */ +static char * +system_default_tmpdir(void) +{ + // c.f. ext/etc/etc.c:etc_systmpdir() +#ifdef _WIN32 + WCHAR tmppath[_MAX_PATH]; + UINT len = rb_w32_system_tmpdir(tmppath, numberof(tmppath)); + if (len) { + int blen = WideCharToMultiByte(CP_UTF8, 0, tmppath, len, NULL, 0, NULL, NULL); + char *tmpdir = xmalloc(blen + 1); + WideCharToMultiByte(CP_UTF8, 0, tmppath, len, tmpdir, blen, NULL, NULL); + tmpdir[blen] = '\0'; + return tmpdir; + } +#elif defined _CS_DARWIN_USER_TEMP_DIR + char path[MAXPATHLEN]; + size_t len = confstr(_CS_DARWIN_USER_TEMP_DIR, path, sizeof(path)); + if (len > 0) { + char *tmpdir = xmalloc(len); + if (len > sizeof(path)) { + confstr(_CS_DARWIN_USER_TEMP_DIR, tmpdir, len); + } + else { + memcpy(tmpdir, path, len); + } + return tmpdir; + } +#endif + return 0; +} + +static int +check_tmpdir(const char *dir) +{ + struct stat st; + + if (!dir) return FALSE; + if (stat(dir, &st)) return FALSE; +#ifndef S_ISDIR +# define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) +#endif + if (!S_ISDIR(st.st_mode)) return FALSE; +#ifndef _WIN32 +# ifndef S_IWOTH +# define S_IWOTH 002 +# endif + if (st.st_mode & S_IWOTH) { +# ifdef S_ISVTX + if (!(st.st_mode & S_ISVTX)) return FALSE; +# else + return FALSE; +# endif + } + if (access(dir, W_OK)) return FALSE; +#endif + return TRUE; +} + +static char * +system_tmpdir(void) +{ + char *tmpdir; +# define RETURN_ENV(name) \ + if (check_tmpdir(tmpdir = getenv(name))) return ruby_strdup(tmpdir) + RETURN_ENV("TMPDIR"); + RETURN_ENV("TMP"); + tmpdir = system_default_tmpdir(); + if (check_tmpdir(tmpdir)) return tmpdir; + return ruby_strdup("/tmp"); +# undef RETURN_ENV +} + +/* end of copy */ + +static int +sprint_ext_filename(char *str, size_t size, long box_id, const char *prefix, const char *basename) +{ + if (tmp_dir_has_dirsep) { + return snprintf(str, size, "%s%sp%"PRI_PIDT_PREFIX"u_%ld_%s", tmp_dir, prefix, getpid(), box_id, basename); + } + return snprintf(str, size, "%s%s%sp%"PRI_PIDT_PREFIX"u_%ld_%s", tmp_dir, DIRSEP, prefix, getpid(), box_id, basename); +} + +enum copy_error_type { + COPY_ERROR_NONE, + COPY_ERROR_SRC_OPEN, + COPY_ERROR_DST_OPEN, + COPY_ERROR_SRC_READ, + COPY_ERROR_DST_WRITE, + COPY_ERROR_SRC_STAT, + COPY_ERROR_DST_CHMOD, + COPY_ERROR_SYSERR +}; + +static const char * +copy_ext_file_error(char *message, size_t size, int copy_retvalue) +{ +#ifdef _WIN32 + int error = GetLastError(); + char *p = message; + size_t len = snprintf(message, size, "%d: ", error); + +#define format_message(sublang) FormatMessage(\ + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, \ + NULL, error, MAKELANGID(LANG_NEUTRAL, (sublang)), \ + message + len, size - len, NULL) + if (format_message(SUBLANG_ENGLISH_US) == 0) + format_message(SUBLANG_DEFAULT); + for (p = message + len; *p; p++) { + if (*p == '\n' || *p == '\r') + *p = ' '; + } +#else + switch (copy_retvalue) { + case COPY_ERROR_SRC_OPEN: + strlcpy(message, "can't open the extension path", size); + break; + case COPY_ERROR_DST_OPEN: + strlcpy(message, "can't open the file to write", size); + break; + case COPY_ERROR_SRC_READ: + strlcpy(message, "failed to read the extension path", size); + break; + case COPY_ERROR_DST_WRITE: + strlcpy(message, "failed to write the extension path", size); + break; + case COPY_ERROR_SRC_STAT: + strlcpy(message, "failed to stat the extension path to copy permissions", size); + break; + case COPY_ERROR_DST_CHMOD: + strlcpy(message, "failed to set permissions to the copied extension path", size); + break; + case COPY_ERROR_SYSERR: + strlcpy(message, strerror(errno), size); + break; + case COPY_ERROR_NONE: /* shouldn't be called */ + default: + rb_bug("unknown return value of copy_ext_file: %d", copy_retvalue); + } +#endif + return message; +} + +#ifndef _WIN32 +static enum copy_error_type +copy_stream(int src_fd, int dst_fd) +{ + char buffer[1024]; + ssize_t rsize; + + while ((rsize = read(src_fd, buffer, sizeof(buffer))) != 0) { + if (rsize < 0) return COPY_ERROR_SRC_READ; + for (size_t written = 0; written < (size_t)rsize;) { + ssize_t wsize = write(dst_fd, buffer+written, rsize-written); + if (wsize < 0) return COPY_ERROR_DST_WRITE; + written += (size_t)wsize; + } + } + return COPY_ERROR_NONE; +} +#endif + +static enum copy_error_type +copy_ext_file(const char *src_path, const char *dst_path) +{ +#if defined(_WIN32) + WCHAR *w_src = rb_w32_mbstr_to_wstr(CP_UTF8, src_path, -1, NULL); + WCHAR *w_dst = rb_w32_mbstr_to_wstr(CP_UTF8, dst_path, -1, NULL); + if (!w_src || !w_dst) { + free(w_src); + free(w_dst); + rb_memerror(); + } + + enum copy_error_type rvalue = CopyFileW(w_src, w_dst, TRUE) ? + COPY_ERROR_NONE : COPY_ERROR_SYSERR; + free(w_src); + free(w_dst); + return rvalue; +#else +# ifdef O_BINARY + const int bin = O_BINARY; +# else + const int bin = 0; +# endif +# ifdef O_CLOEXEC + const int cloexec = O_CLOEXEC; +# else + const int cloexec = 0; +# endif + const int src_fd = open(src_path, O_RDONLY|cloexec|bin); + if (src_fd < 0) return COPY_ERROR_SRC_OPEN; + if (!cloexec) rb_maygvl_fd_fix_cloexec(src_fd); + + struct stat src_st; + if (fstat(src_fd, &src_st)) { + close(src_fd); + return COPY_ERROR_SRC_STAT; + } + + const int dst_fd = open(dst_path, O_WRONLY|O_CREAT|O_EXCL|cloexec|bin, S_IRWXU); + if (dst_fd < 0) { + close(src_fd); + return COPY_ERROR_DST_OPEN; + } + if (!cloexec) rb_maygvl_fd_fix_cloexec(dst_fd); + + enum copy_error_type ret = COPY_ERROR_NONE; + + if (fchmod(dst_fd, src_st.st_mode & 0777)) { + ret = COPY_ERROR_DST_CHMOD; + goto done; + } + + const size_t count_max = (SIZE_MAX >> 1) + 1; + (void)count_max; + +# ifdef HAVE_COPY_FILE_RANGE + for (;;) { + ssize_t written = copy_file_range(src_fd, NULL, dst_fd, NULL, count_max, 0); + if (written == 0) goto done; + if (written < 0) break; + } +# endif +# ifdef HAVE_FCOPYFILE + if (fcopyfile(src_fd, dst_fd, NULL, COPYFILE_DATA) == 0) { + goto done; + } +# endif +# ifdef USE_SENDFILE + for (;;) { + ssize_t written = sendfile(src_fd, dst_fd, NULL count_max); + if (written == 0) goto done; + if (written < 0) break; + } +# endif + ret = copy_stream(src_fd, dst_fd); + + done: + close(src_fd); + if (dst_fd >= 0) close(dst_fd); + if (ret != COPY_ERROR_NONE) unlink(dst_path); + return ret; +#endif +} + +#if defined __CYGWIN__ || defined DOSISH +#define isdirsep(x) ((x) == '/' || (x) == '\\') +#else +#define isdirsep(x) ((x) == '/') +#endif + +#define IS_SOEXT(e) (strcmp((e), ".so") == 0 || strcmp((e), ".o") == 0) +#define IS_DLEXT(e) (strcmp((e), DLEXT) == 0) + +static void +fname_without_suffix(const char *fname, char *rvalue, size_t rsize) +{ + size_t len = strlen(fname); + const char *pos; + for (pos = fname + len; pos > fname; pos--) { + if (IS_SOEXT(pos) || IS_DLEXT(pos)) { + len = pos - fname; + break; + } + if (fname + len - pos > DLEXT_MAXLEN) break; + } + if (len > rsize - 1) len = rsize - 1; + memcpy(rvalue, fname, len); + rvalue[len] = '\0'; +} + +static void +escaped_basename(const char *path, const char *fname, char *rvalue, size_t rsize) +{ + char *pos; + const char *leaf = path, *found; + // `leaf + 1` looks uncomfortable (when leaf == path), but fname must not be the top-dir itself + while ((found = strstr(leaf + 1, fname)) != NULL) { + leaf = found; // find the last occurrence for the path like /etc/my-crazy-lib-dir/etc.so + } + strlcpy(rvalue, leaf, rsize); + for (pos = rvalue; *pos; pos++) { + if (isdirsep(*pos)) { + *pos = '+'; + } + } +} + +static void +box_ext_cleanup_mark(void *p) +{ + rb_gc_mark((VALUE)p); +} + +static void +box_ext_cleanup_free(void *p) +{ + VALUE path = (VALUE)p; + unlink(RSTRING_PTR(path)); +} + +static const rb_data_type_t box_ext_cleanup_type = { + "box_ext_cleanup", + {box_ext_cleanup_mark, box_ext_cleanup_free}, + .flags = RUBY_TYPED_FREE_IMMEDIATELY, +}; + +void +rb_box_cleanup_local_extension(VALUE cleanup) +{ + void *p = DATA_PTR(cleanup); + DATA_PTR(cleanup) = NULL; +#ifndef _WIN32 + if (p) box_ext_cleanup_free(p); +#endif + (void)p; +} + +static int +cleanup_local_extension_i(VALUE key, VALUE value, VALUE arg) +{ +#if defined(_WIN32) + HMODULE h = (HMODULE)NUM2PTR(value); + WCHAR module_path[MAXPATHLEN]; + DWORD len = GetModuleFileNameW(h, module_path, numberof(module_path)); + + FreeLibrary(h); + if (len > 0 && len < numberof(module_path)) DeleteFileW(module_path); +#endif + return ST_DELETE; +} + +static void +cleanup_all_local_extensions(VALUE libmap) +{ + rb_hash_foreach(libmap, cleanup_local_extension_i, 0); +} + +VALUE +rb_box_local_extension(VALUE box_value, VALUE fname, VALUE path, VALUE *cleanup) +{ + char ext_path[MAXPATHLEN], fname2[MAXPATHLEN], basename[MAXPATHLEN]; + int wrote; + const char *src_path = RSTRING_PTR(path), *fname_ptr = RSTRING_PTR(fname); + rb_box_t *box = rb_get_box_t(box_value); + + fname_without_suffix(fname_ptr, fname2, sizeof(fname2)); + escaped_basename(src_path, fname2, basename, sizeof(basename)); + + wrote = sprint_ext_filename(ext_path, sizeof(ext_path), box->box_id, BOX_TMP_PREFIX, basename); + if (wrote >= (int)sizeof(ext_path)) { + rb_bug("Extension file path in the box was too long"); + } + VALUE new_path = rb_str_new_cstr(ext_path); + *cleanup = TypedData_Wrap_Struct(0, &box_ext_cleanup_type, NULL); + enum copy_error_type copy_error = copy_ext_file(src_path, ext_path); + if (copy_error) { + char message[1024]; + copy_ext_file_error(message, sizeof(message), copy_error); + rb_raise(rb_eLoadError, "can't prepare the extension file for Ruby Box (%s from %"PRIsVALUE"): %s", ext_path, path, message); + } + DATA_PTR(*cleanup) = (void *)new_path; + return new_path; +} + +static VALUE +rb_box_load(int argc, VALUE *argv, VALUE box) +{ + VALUE fname, wrap; + rb_scan_args(argc, argv, "11", &fname, &wrap); + + rb_vm_frame_flag_set_box_require(GET_EC()); + + VALUE args = rb_ary_new_from_args(2, fname, wrap); + return rb_load_entrypoint(args); +} + +static VALUE +rb_box_require(VALUE box, VALUE fname) +{ + rb_vm_frame_flag_set_box_require(GET_EC()); + + return rb_require_string(fname); +} + +static VALUE +rb_box_require_relative(VALUE box, VALUE fname) +{ + rb_vm_frame_flag_set_box_require(GET_EC()); + + return rb_require_relative_entrypoint(fname); +} + +static void +initialize_root_box(void) +{ + rb_vm_t *vm = GET_VM(); + rb_box_t *root = (rb_box_t *)rb_root_box(); + + root->load_path = rb_ary_new(); + root->expanded_load_path = rb_ary_hidden_new(0); + root->load_path_snapshot = rb_ary_hidden_new(0); + root->load_path_check_cache = 0; + rb_define_singleton_method(root->load_path, "resolve_feature_path", rb_resolve_feature_path, 1); + + root->loaded_features = rb_ary_new(); + root->loaded_features_snapshot = rb_ary_hidden_new(0); + root->loaded_features_index = st_init_numtable(); + root->loaded_features_realpaths = rb_hash_new(); + rb_obj_hide(root->loaded_features_realpaths); + root->loaded_features_realpath_map = rb_hash_new(); + rb_obj_hide(root->loaded_features_realpath_map); + + root->ruby_dln_libmap = rb_hash_new_with_size(0); + root->gvar_tbl = rb_hash_new_with_size(0); + root->classext_cow_classes = NULL; // classext CoW never happen on the root box + + vm->root_box = root; + + if (rb_box_available()) { + VALUE root_box, entry; + ID id_box_entry; + CONST_ID(id_box_entry, "__box_entry__"); + + root_box = rb_obj_alloc(rb_cBox); + RCLASS_SET_PRIME_CLASSEXT_WRITABLE(root_box, true); + RCLASS_SET_CONST_TBL(root_box, RCLASSEXT_CONST_TBL(RCLASS_EXT_PRIME(rb_cObject)), true); + + root->box_id = box_generate_id(); + root->box_object = root_box; + + entry = TypedData_Wrap_Struct(rb_cBoxEntry, &rb_root_box_data_type, root); + rb_ivar_set(root_box, id_box_entry, entry); + } + else { + root->box_id = 1; + root->box_object = Qnil; + } +} + +static VALUE +rb_box_eval(VALUE box_value, VALUE str) +{ + const rb_iseq_t *iseq; + const rb_box_t *box; + + StringValue(str); + + iseq = rb_iseq_compile_iseq(str, rb_str_new_cstr("eval")); + VM_ASSERT(iseq); + + box = (const rb_box_t *)rb_get_box_t(box_value); + + return rb_iseq_eval(iseq, box); +} + +static int box_experimental_warned = 0; + +RUBY_EXTERN const char ruby_api_version_name[]; + +void +rb_initialize_main_box(void) +{ + rb_box_t *box; + VALUE main_box_value; + rb_vm_t *vm = GET_VM(); + + VM_ASSERT(rb_box_available()); + + if (!box_experimental_warned) { + rb_category_warn(RB_WARN_CATEGORY_EXPERIMENTAL, + "Ruby::Box is experimental, and the behavior may change in the future!\n" + "See https://docs.ruby-lang.org/en/%s/Ruby/Box.html for known issues, etc.", + ruby_api_version_name); + box_experimental_warned = 1; + } + + main_box_value = rb_class_new_instance(0, NULL, rb_cBox); + VM_ASSERT(BOX_OBJ_P(main_box_value)); + box = rb_get_box_t(main_box_value); + box->box_object = main_box_value; + box->is_user = true; + box->is_optional = false; + + rb_const_set(rb_cBox, rb_intern("MAIN"), main_box_value); + + vm->main_box = main_box = box; + + // create the writable classext of ::Object explicitly to finalize the set of visible top-level constants + RCLASS_EXT_WRITABLE_IN_BOX(rb_cObject, box); +} + +static VALUE +rb_box_inspect(VALUE obj) +{ + rb_box_t *box; + VALUE r; + if (obj == Qfalse) { + r = rb_str_new_cstr("#<Ruby::Box:root>"); + return r; + } + box = rb_get_box_t(obj); + r = rb_str_new_cstr("#<Ruby::Box:"); + rb_str_concat(r, rb_funcall(LONG2NUM(box->box_id), rb_intern("to_s"), 0)); + if (BOX_ROOT_P(box)) { + rb_str_cat_cstr(r, ",root"); + } + if (BOX_USER_P(box)) { + rb_str_cat_cstr(r, ",user"); + } + if (BOX_MAIN_P(box)) { + rb_str_cat_cstr(r, ",main"); + } + else if (BOX_OPTIONAL_P(box)) { + rb_str_cat_cstr(r, ",optional"); + } + rb_str_cat_cstr(r, ">"); + return r; +} + +static VALUE +rb_box_loading_func(int argc, VALUE *argv, VALUE _self) +{ + rb_vm_frame_flag_set_box_require(GET_EC()); + return rb_call_super(argc, argv); +} + +static void +box_define_loader_method(const char *name) +{ + rb_define_private_method(rb_mBoxLoader, name, rb_box_loading_func, -1); + rb_define_singleton_method(rb_mBoxLoader, name, rb_box_loading_func, -1); +} + +void +Init_root_box(void) +{ + root_box->loading_table = st_init_strtable(); +} + +void +Init_enable_box(void) +{ + const char *env = getenv("RUBY_BOX"); + if (env && strlen(env) == 1 && env[0] == '1') { + ruby_box_enabled = true; + } + else { + ruby_box_init_done = true; + } +} + +/* :nodoc: */ +static VALUE +rb_box_s_root(VALUE recv) +{ + return root_box->box_object; +} + +/* :nodoc: */ +static VALUE +rb_box_s_main(VALUE recv) +{ + return main_box->box_object; +} + +/* :nodoc: */ +static VALUE +rb_box_root_p(VALUE box_value) +{ + const rb_box_t *box = (const rb_box_t *)rb_get_box_t(box_value); + return RBOOL(BOX_ROOT_P(box)); +} + +/* :nodoc: */ +static VALUE +rb_box_main_p(VALUE box_value) +{ + const rb_box_t *box = (const rb_box_t *)rb_get_box_t(box_value); + return RBOOL(BOX_MAIN_P(box)); +} + +#if RUBY_DEBUG + +static const char * +classname(VALUE klass) +{ + VALUE p; + if (!klass) { + return "Qfalse"; + } + p = RCLASSEXT_CLASSPATH(RCLASS_EXT_PRIME(klass)); + if (RTEST(p)) + return RSTRING_PTR(p); + if (RB_TYPE_P(klass, T_CLASS) || RB_TYPE_P(klass, T_MODULE) || RB_TYPE_P(klass, T_ICLASS)) + return "AnyClassValue"; + return "NonClassValue"; +} + +static enum rb_id_table_iterator_result +dump_classext_methods_i(ID mid, VALUE _val, void *data) +{ + VALUE ary = (VALUE)data; + rb_ary_push(ary, rb_id2str(mid)); + return ID_TABLE_CONTINUE; +} + +static enum rb_id_table_iterator_result +dump_classext_constants_i(ID mid, VALUE _val, void *data) +{ + VALUE ary = (VALUE)data; + rb_ary_push(ary, rb_id2str(mid)); + return ID_TABLE_CONTINUE; +} + +static void +dump_classext_i(rb_classext_t *ext, bool is_prime, VALUE _recv, void *data) +{ + char buf[4096]; + struct rb_id_table *tbl; + VALUE ary, res = (VALUE)data; + + snprintf(buf, 4096, "Ruby::Box %ld:%s classext %p\n", + RCLASSEXT_BOX(ext)->box_id, is_prime ? " prime" : "", (void *)ext); + rb_str_cat_cstr(res, buf); + + snprintf(buf, 2048, " Super: %s\n", classname(RCLASSEXT_SUPER(ext))); + rb_str_cat_cstr(res, buf); + + tbl = RCLASSEXT_M_TBL(ext); + if (tbl) { + ary = rb_ary_new_capa((long)rb_id_table_size(tbl)); + rb_id_table_foreach(RCLASSEXT_M_TBL(ext), dump_classext_methods_i, (void *)ary); + rb_ary_sort_bang(ary); + snprintf(buf, 4096, " Methods(%ld): ", RARRAY_LEN(ary)); + rb_str_cat_cstr(res, buf); + rb_str_concat(res, rb_ary_join(ary, rb_str_new_cstr(","))); + rb_str_cat_cstr(res, "\n"); + } + else { + rb_str_cat_cstr(res, " Methods(0): .\n"); + } + + tbl = RCLASSEXT_CONST_TBL(ext); + if (tbl) { + ary = rb_ary_new_capa((long)rb_id_table_size(tbl)); + rb_id_table_foreach(tbl, dump_classext_constants_i, (void *)ary); + rb_ary_sort_bang(ary); + snprintf(buf, 4096, " Constants(%ld): ", RARRAY_LEN(ary)); + rb_str_cat_cstr(res, buf); + rb_str_concat(res, rb_ary_join(ary, rb_str_new_cstr(","))); + rb_str_cat_cstr(res, "\n"); + } + else { + rb_str_cat_cstr(res, " Constants(0): .\n"); + } +} + +/* :nodoc: */ +static VALUE +rb_f_dump_classext(VALUE recv, VALUE klass) +{ + /* + * The desired output String value is: + * Class: 0x88800932 (String) [singleton] + * Prime classext box(2,main), readable(t), writable(f) + * Non-prime classexts: 3 + * Box 2: prime classext 0x88800933 + * Super: Object + * Methods(43): aaaaa, bbbb, cccc, dddd, eeeee, ffff, gggg, hhhhh, ... + * Constants(12): FOO, Bar, ... + * Box 5: classext 0x88800934 + * Super: Object + * Methods(43): aaaaa, bbbb, cccc, dddd, eeeee, ffff, gggg, hhhhh, ... + * Constants(12): FOO, Bar, ... + */ + char buf[2048]; + VALUE res; + const rb_classext_t *ext; + const rb_box_t *box; + st_table *classext_tbl; + + if (!(RB_TYPE_P(klass, T_CLASS) || RB_TYPE_P(klass, T_MODULE))) { + snprintf(buf, 2048, "Non-class/module value: %p (%s)\n", (void *)klass, rb_type_str(BUILTIN_TYPE(klass))); + return rb_str_new_cstr(buf); + } + + if (RB_TYPE_P(klass, T_CLASS)) { + snprintf(buf, 2048, "Class: %p (%s)%s\n", + (void *)klass, classname(klass), RCLASS_SINGLETON_P(klass) ? " [singleton]" : ""); + } + else { + snprintf(buf, 2048, "Module: %p (%s)\n", (void *)klass, classname(klass)); + } + res = rb_str_new_cstr(buf); + + ext = RCLASS_EXT_PRIME(klass); + box = RCLASSEXT_BOX(ext); + snprintf(buf, 2048, "Prime classext box(%ld,%s), readable(%s), writable(%s)\n", + box->box_id, + BOX_ROOT_P(box) ? "root" : (BOX_MAIN_P(box) ? "main" : "optional"), + RCLASS_PRIME_CLASSEXT_READABLE_P(klass) ? "t" : "f", + RCLASS_PRIME_CLASSEXT_WRITABLE_P(klass) ? "t" : "f"); + rb_str_cat_cstr(res, buf); + + classext_tbl = RCLASS_CLASSEXT_TBL(klass); + if (!classext_tbl) { + rb_str_cat_cstr(res, "Non-prime classexts: 0\n"); + } + else { + snprintf(buf, 2048, "Non-prime classexts: %zu\n", st_table_size(classext_tbl)); + rb_str_cat_cstr(res, buf); + } + + rb_class_classext_foreach(klass, dump_classext_i, (void *)res); + + return res; +} + +#endif /* RUBY_DEBUG */ + +/* + * Document-class: Ruby::Box + * + * :markup: markdown + * :include: doc/language/box.md + */ +void +Init_Box(void) +{ + tmp_dir = system_tmpdir(); + tmp_dir_has_dirsep = (strcmp(tmp_dir + (strlen(tmp_dir) - strlen(DIRSEP)), DIRSEP) == 0); + + VALUE mRuby = rb_define_module("Ruby"); + + rb_cBox = rb_define_class_under(mRuby, "Box", rb_cModule); + rb_define_method(rb_cBox, "initialize", box_initialize, 0); + + /* :nodoc: */ + rb_cBoxEntry = rb_define_class_under(rb_cBox, "Entry", rb_cObject); + rb_define_alloc_func(rb_cBoxEntry, rb_box_entry_alloc); + + initialize_root_box(); + + /* :nodoc: */ + rb_mBoxLoader = rb_define_module_under(rb_cBox, "Loader"); + box_define_loader_method("require"); + box_define_loader_method("require_relative"); + box_define_loader_method("load"); + + if (rb_box_available()) { + rb_include_module(rb_cObject, rb_mBoxLoader); + + rb_define_singleton_method(rb_cBox, "root", rb_box_s_root, 0); + rb_define_singleton_method(rb_cBox, "main", rb_box_s_main, 0); + rb_define_method(rb_cBox, "root?", rb_box_root_p, 0); + rb_define_method(rb_cBox, "main?", rb_box_main_p, 0); + +#if RUBY_DEBUG + rb_define_global_function("dump_classext", rb_f_dump_classext, 1); +#endif + } + + rb_define_singleton_method(rb_cBox, "enabled?", rb_box_s_getenabled, 0); + rb_define_singleton_method(rb_cBox, "current", rb_box_s_current, 0); + + rb_define_method(rb_cBox, "load_path", rb_box_load_path, 0); + rb_define_method(rb_cBox, "load", rb_box_load, -1); + rb_define_method(rb_cBox, "require", rb_box_require, 1); + rb_define_method(rb_cBox, "require_relative", rb_box_require_relative, 1); + rb_define_method(rb_cBox, "eval", rb_box_eval, 1); + + rb_define_method(rb_cBox, "inspect", rb_box_inspect, 0); +} @@ -3,15 +3,15 @@ #include "iseq.h" #include "builtin.h" -#ifdef CROSS_COMPILING +#include "builtin_binary.rbbin" -#define INCLUDED_BY_BUILTIN_C 1 +#ifndef BUILTIN_BINARY_SIZE + +#define BUILTIN_LOADED(feature_name, iseq) ((void)0) #include "mini_builtin.c" #else -#include "builtin_binary.inc" - static const unsigned char * bin4feature(const struct builtin_binary *bb, const char *feature, size_t *psize) { @@ -32,31 +32,43 @@ builtin_lookup(const char *feature, size_t *psize) return bin; } -void -rb_load_with_builtin_functions(const char *feature_name, const struct rb_builtin_function *table) +static void +load_with_builtin_functions(const char *feature_name, const struct rb_builtin_function *table) { // search binary size_t size; const unsigned char *bin = builtin_lookup(feature_name, &size); if (! bin) { - rb_bug("builtin_lookup: can not find %s\n", feature_name); + rb_bug("builtin_lookup: can not find %s", feature_name); } // load binary rb_vm_t *vm = GET_VM(); if (vm->builtin_function_table != NULL) rb_bug("vm->builtin_function_table should be NULL."); vm->builtin_function_table = table; - vm->builtin_inline_index = 0; const rb_iseq_t *iseq = rb_iseq_ibf_load_bytes((const char *)bin, size); + ASSUME(iseq); // otherwise an exception should have raised vm->builtin_function_table = NULL; // exec - rb_iseq_eval(rb_iseq_check(iseq)); + rb_iseq_eval(rb_iseq_check(iseq), rb_root_box()); // builtin functions are loaded in the root box +} + +void +rb_load_with_builtin_functions(const char *feature_name, const struct rb_builtin_function *table) +{ + load_with_builtin_functions(feature_name, table); } #endif void +rb_free_loaded_builtin_table(void) +{ + // do nothing +} + +void Init_builtin(void) { // nothing @@ -65,5 +77,11 @@ Init_builtin(void) void Init_builtin_features(void) { - rb_load_with_builtin_functions("gem_prelude", NULL); + +#ifdef BUILTIN_BINARY_SIZE + + load_with_builtin_functions("gem_prelude", NULL); + +#endif + } diff --git a/builtin.h b/builtin.h |