diff --git a/.cirrus.yml b/.cirrus.yml index 800abb96388aa8..c496d5941bcede 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -129,5 +129,5 @@ yjit_task: full_build_script: source $HOME/.cargo/env && make cargo_test_script: source $HOME/.cargo/env && cd yjit && cargo test make_test_script: source $HOME/.cargo/env && make test RUN_OPTS="--yjit-call-threshold=1 --yjit-verify-ctx" - make_test_all_script: source $HOME/.cargo/env && make test-all RUN_OPTS="--yjit-call-threshold=1" TESTOPTS="$RUBY_TESTOPTS" - make_test_spec_script: source $HOME/.cargo/env && make test-spec RUN_OPTS="--yjit-call-threshold=1" + make_test_all_script: source $HOME/.cargo/env && make test-all RUN_OPTS="--yjit-call-threshold=1 --yjit-verify-ctx" TESTOPTS="$RUBY_TESTOPTS" + make_test_spec_script: source $HOME/.cargo/env && make test-spec RUN_OPTS="--yjit-call-threshold=1 --yjit-verify-ctx" diff --git a/.github/workflows/baseruby.yml b/.github/workflows/baseruby.yml index 65d2813ac23a5c..8152cad7685082 100644 --- a/.github/workflows/baseruby.yml +++ b/.github/workflows/baseruby.yml @@ -57,7 +57,7 @@ jobs: - run: make incs - run: make all - run: make test - - uses: k0kubun/action-slack@v2.0.0 + - uses: ruby/action-slack@v3.0.0 with: payload: | { @@ -65,7 +65,7 @@ jobs: "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] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index 1753f4657409cb..16e2829f734353 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -56,7 +56,7 @@ jobs: - run: make all golf - run: ruby tool/update-deps --fix - run: git diff --no-ext-diff --ignore-submodules --exit-code - - uses: k0kubun/action-slack@v2.0.0 + - uses: ruby/action-slack@v3.0.0 with: payload: | { @@ -64,7 +64,7 @@ jobs: "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] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 71ac9566955300..e897015d564c72 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -98,7 +98,7 @@ jobs: GIT_COMMITTER_NAME: git if: ${{ github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull') && steps.diff.outcome == 'failure' }} - - uses: k0kubun/action-slack@v2.0.0 + - uses: ruby/action-slack@v3.0.0 with: payload: | { @@ -106,7 +106,7 @@ jobs: "env": "${{ github.workflow }}", "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/cirrus-notify.yml b/.github/workflows/cirrus-notify.yml new file mode 100644 index 00000000000000..fff717f1d6a386 --- /dev/null +++ b/.github/workflows/cirrus-notify.yml @@ -0,0 +1,42 @@ +on: + check_suite: + type: ['completed'] +name: Cirrus CI failure notification +jobs: + cirrus-notify: + name: After Cirrus CI Failure + if: >- + github.event.check_suite.app.name == 'Cirrus CI' + && github.event.check_suite.conclusion != 'success' + && github.event.check_suite.conclusion != 'cancelled' + && github.event.check_suite.conclusion != 'skipped' + && github.event.check_suite.head_branch == 'master' + runs-on: ubuntu-latest + steps: + - uses: octokit/request-action@v2.x + id: get_failed_check_run + with: + route: GET /repos/${{ github.repository }}/check-suites/${{ github.event.check_suite.id }}/check-runs?status=completed + mediaType: '{"previews": ["antiope"]}' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Dump check_runs + env: + CHECK_RUNS: ${{ steps.get_failed_check_run.outputs.data }} + run: echo "$CHECK_RUNS" + - uses: ruby/action-slack@v3.0.0 + with: + payload: | + { + "ci": "Cirrus CI", + "env": "Cirrus CI", + "url": "${{ fromJson(steps.get_failed_check_run.outputs.data).check_runs[0].html_url }}", + "commit": "${{ github.event.check_suite.head_commit.id }}", + "branch": "${{ github.event.check_suite.head_branch }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 85b11e90e72ea2..42f1494b1de475 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,24 +1,24 @@ name: "Code scanning - action" on: - push: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' - - '**/.document' - - '**.[1-8]' - - '**.ronn' - pull_request: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' - - '**/.document' - - '**.[1-8]' - - '**.ronn' + # push: + # paths-ignore: + # - 'doc/**' + # - '**.md' + # - '**.rdoc' + # - '**/.document' + # - '**.[1-8]' + # - '**.ronn' + # pull_request: + # paths-ignore: + # - 'doc/**' + # - '**.md' + # - '**.rdoc' + # - '**/.document' + # - '**.[1-8]' + # - '**.ronn' schedule: - - cron: '0 12 * * 4' + - cron: '0 12 * * *' concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} diff --git a/.github/workflows/compilers.yml b/.github/workflows/compilers.yml index 8080963fa6303a..60fcc94c5ec3fa 100644 --- a/.github/workflows/compilers.yml +++ b/.github/workflows/compilers.yml @@ -268,7 +268,7 @@ jobs: - run: make test-annocheck if: ${{ matrix.entry.check && endsWith(matrix.entry.name, 'annocheck') }} - - uses: k0kubun/action-slack@v2.0.0 + - uses: ruby/action-slack@v3.0.0 with: payload: | { @@ -276,7 +276,7 @@ jobs: "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] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 04446d3f053bcd..e5f055f8c43242 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -85,7 +85,7 @@ jobs: PRECHECK_BUNDLED_GEMS: "no" if: ${{ matrix.test_task == 'check' && matrix.skipped_tests != '' }} continue-on-error: ${{ matrix.continue-on-skipped_tests || false }} - - uses: k0kubun/action-slack@v2.0.0 + - uses: ruby/action-slack@v3.0.0 with: payload: | { @@ -93,7 +93,7 @@ jobs: "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] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/mingw.yml b/.github/workflows/mingw.yml index 5c8189428ca177..6f7af8ccf829d5 100644 --- a/.github/workflows/mingw.yml +++ b/.github/workflows/mingw.yml @@ -32,10 +32,10 @@ jobs: MSYSTEM: ${{ matrix.msystem }} MSYS2_ARCH: x86_64 CHOST: "x86_64-w64-mingw32" - CFLAGS: "-march=x86-64 -mtune=generic -O3 -pipe -fstack-protector-strong" + CFLAGS: "-march=x86-64 -mtune=generic -O3 -pipe" 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" + LDFLAGS: "-pipe" UPDATE_UNICODE: "UNICODE_FILES=. UNICODE_PROPERTY_FILES=. UNICODE_AUXILIARY_FILES=. UNICODE_EMOJI_FILES=." GITPULLOPTIONS: --no-tags origin ${{github.ref}} strategy: @@ -151,7 +151,7 @@ jobs: 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: ruby/action-slack@v3.0.0 with: payload: | { @@ -159,7 +159,7 @@ jobs: "env": "${{ github.workflow }} ${{ matrix.msystem }} / ${{ matrix.test_task }}", "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/mjit-bindgen.yml b/.github/workflows/mjit-bindgen.yml index e1d22d91f2d92f..1252d3b50987c6 100644 --- a/.github/workflows/mjit-bindgen.yml +++ b/.github/workflows/mjit-bindgen.yml @@ -83,7 +83,7 @@ jobs: - run: $SETARCH make ${{ matrix.task }} - run: git diff --exit-code working-directory: src - - uses: k0kubun/action-slack@v2.0.0 + - uses: ruby/action-slack@v3.0.0 with: payload: | { @@ -91,7 +91,7 @@ jobs: "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] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/mjit.yml b/.github/workflows/mjit.yml index 89bc0a226becff..c468e0181173fe 100644 --- a/.github/workflows/mjit.yml +++ b/.github/workflows/mjit.yml @@ -84,7 +84,7 @@ jobs: ulimit -c unlimited make -s test-spec RUN_OPTS="$RUN_OPTS" timeout-minutes: 60 - - uses: k0kubun/action-slack@v2.0.0 + - uses: ruby/action-slack@v3.0.0 with: payload: | { @@ -92,7 +92,7 @@ jobs: "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] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/spec_guards.yml b/.github/workflows/spec_guards.yml index 6d51320c2f969e..1991e016a3d596 100644 --- a/.github/workflows/spec_guards.yml +++ b/.github/workflows/spec_guards.yml @@ -2,21 +2,13 @@ name: Rubyspec Version Guards Check on: push: - paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' - - '**/.document' - - '**.[1-8]' - - '**.ronn' + paths: + - 'spec/**' + - '!spec/*.md' pull_request: paths-ignore: - - 'doc/**' - - '**.md' - - '**.rdoc' - - '**/.document' - - '**.[1-8]' - - '**.ronn' + - 'spec/**' + - '!spec/*.md' concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} @@ -46,7 +38,7 @@ jobs: working-directory: spec/ruby env: CHECK_LEAKS: true - - uses: k0kubun/action-slack@v2.0.0 + - uses: ruby/action-slack@v3.0.0 with: payload: | { @@ -54,7 +46,7 @@ jobs: "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] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 1a5b6661fe1f8a..508d2c7733a575 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -120,7 +120,7 @@ jobs: TESTS: ${{ matrix.skipped_tests }} if: ${{ matrix.test_task == 'check' && matrix.skipped_tests != '' }} continue-on-error: ${{ matrix.continue-on-skipped_tests || false }} - - uses: k0kubun/action-slack@v2.0.0 + - uses: ruby/action-slack@v3.0.0 with: payload: | { @@ -128,7 +128,7 @@ jobs: "env": "${{ github.workflow }} / ${{ matrix.test_task }} ${{ matrix.configure }}${{ matrix.arch }}", "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", "commit": "${{ github.sha }}", - "branch": "${{ github.ref }}".split('/').reverse()[0] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 6f5106e815639c..7f96d3464bbd7f 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -138,7 +138,7 @@ jobs: env: RUBY_TESTOPTS: -j${{env.TEST_JOBS}} --job-status=normal timeout-minutes: 60 - - uses: k0kubun/action-slack@v2.0.0 + - uses: ruby/action-slack@v3.0.0 with: payload: | { @@ -146,7 +146,7 @@ jobs: "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] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/.github/workflows/yjit-ubuntu.yml b/.github/workflows/yjit-ubuntu.yml index 176181f11c5b3e..d0e50154d2ca05 100644 --- a/.github/workflows/yjit-ubuntu.yml +++ b/.github/workflows/yjit-ubuntu.yml @@ -56,7 +56,7 @@ jobs: - test_task: "check" configure: "--enable-yjit=dev" - yjit_opts: "--yjit-call-threshold=1" + yjit_opts: "--yjit-call-threshold=1 --yjit-verify-ctx" - test_task: "test-all TESTS=--repeat-count=2" configure: "--enable-yjit=dev" @@ -135,7 +135,7 @@ jobs: BASE_REPO: ${{ github.event.pull_request.base.repo.full_name }} BASE_SHA: ${{ github.event.pull_request.base.sha }} if: ${{ matrix.test_task == 'yjit-bench' && startsWith(github.event_name, 'pull') }} - - uses: k0kubun/action-slack@v2.0.0 + - uses: ruby/action-slack@v3.0.0 with: payload: | { @@ -143,7 +143,7 @@ jobs: "env": "${{ github.workflow }} / ${{ 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] + "branch": "${{ github.ref_name }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/NEWS.md b/NEWS.md index dd40d55e21ee13..0367e8bfd0eb1b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -231,6 +231,7 @@ Note: We're only listing outstanding class updates. * bundler 2.4.0.dev * cgi 0.3.3 * date 3.2.3 + * erb 3.0.0 * error_highlight 0.4.0 * etc 1.4.0 * fiddle 1.1.1 @@ -259,7 +260,7 @@ Note: We're only listing outstanding class updates. * net-ftp 0.2.0 * net-imap 0.3.1 * net-pop 0.1.2 - * net-smtp 0.3.2 + * net-smtp 0.3.3 * rbs 2.7.0 * typeprof 0.21.3 * debug 1.6.3 @@ -323,9 +324,24 @@ The following deprecated APIs are removed. * Support arm64 / aarch64 on UNIX platforms. * Building YJIT requires Rust 1.58.1+. [[Feature #18481]] +* Physical memory for JIT code is lazily allocated. Unlike Ruby 3.1, + the RSS of a Ruby process is minimized because virtual memory pages + allocated by `--yjit-exec-mem-size` will not be mapped to physical + memory pages until actually utilized by JIT code. +* Introduce Code GC that frees all code pages when the memory consumption + by JIT code reaches `--yjit-exec-mem-size`. +* `RubyVM::YJIT.runtime_stats` returns Code GC metrics in addition to + existing `inline_code_size` and `outlined_code_size` keys: + `code_gc_count`, `live_page_count`, `freed_page_count`, and `freed_code_size`. ### MJIT +* The MJIT compiler is re-implemented in Ruby as a standard library `mjit`. +* MJIT compiler is executed under a forked Ruby process instead of + doing it in a native thread called MJIT worker. [[Feature #18968]] + * As a result, Microsoft Visual Studio (MSWIN) is no longer supported. +* MinGW is no longer supported. [[Feature #18824]] + ## Static analysis ### RBS @@ -349,6 +365,7 @@ The following deprecated APIs are removed. [Feature #15231]: https://bugs.ruby-lang.org/issues/15231 [Feature #15357]: https://bugs.ruby-lang.org/issues/15357 [Bug #15928]: https://bugs.ruby-lang.org/issues/15928 +[Feature #16122]: https://bugs.ruby-lang.org/issues/16122 [Feature #16131]: https://bugs.ruby-lang.org/issues/16131 [Bug #16466]: https://bugs.ruby-lang.org/issues/16466 [Feature #16806]: https://bugs.ruby-lang.org/issues/16806 @@ -362,21 +379,24 @@ The following deprecated APIs are removed. [Feature #18037]: https://bugs.ruby-lang.org/issues/18037 [Feature #18159]: https://bugs.ruby-lang.org/issues/18159 [Feature #18351]: https://bugs.ruby-lang.org/issues/18351 +[Feature #18481]: https://bugs.ruby-lang.org/issues/18481 [Bug #18487]: https://bugs.ruby-lang.org/issues/18487 [Feature #18571]: https://bugs.ruby-lang.org/issues/18571 [Feature #18585]: https://bugs.ruby-lang.org/issues/18585 +[Feature #18589]: https://bugs.ruby-lang.org/issues/18589 [Feature #18598]: https://bugs.ruby-lang.org/issues/18598 [Bug #18625]: https://bugs.ruby-lang.org/issues/18625 +[Feature #18630]: https://bugs.ruby-lang.org/issues/18630 [Bug #18633]: https://bugs.ruby-lang.org/issues/18633 [Feature #18685]: https://bugs.ruby-lang.org/issues/18685 [Bug #18782]: https://bugs.ruby-lang.org/issues/18782 [Feature #18788]: https://bugs.ruby-lang.org/issues/18788 [Feature #18809]: https://bugs.ruby-lang.org/issues/18809 -[Feature #18481]: https://bugs.ruby-lang.org/issues/18481 +[Feature #18821]: https://bugs.ruby-lang.org/issues/18821 +[Feature #18824]: https://bugs.ruby-lang.org/issues/18824 [Feature #18949]: https://bugs.ruby-lang.org/issues/18949 +[Feature #18968]: https://bugs.ruby-lang.org/issues/18968 [Feature #19008]: https://bugs.ruby-lang.org/issues/19008 +[Feature #19013]: https://bugs.ruby-lang.org/issues/19013 [Feature #19026]: https://bugs.ruby-lang.org/issues/19026 -[Feature #16122]: https://bugs.ruby-lang.org/issues/16122 -[Feature #18630]: https://bugs.ruby-lang.org/issues/18630 -[Feature #18589]: https://bugs.ruby-lang.org/issues/18589 [Feature #19060]: https://bugs.ruby-lang.org/issues/19060 diff --git a/ast.c b/ast.c index c9d0b41facdc99..93c33a1f8dd89e 100644 --- a/ast.c +++ b/ast.c @@ -195,6 +195,18 @@ script_lines(VALUE path) return lines; } +static VALUE +node_id_for_backtrace_location(rb_execution_context_t *ec, VALUE module, VALUE location) +{ + int node_id; + 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, VALUE error_tolerant) { diff --git a/ast.rb b/ast.rb index 24fd8e55269ada..1bc1168b806172 100644 --- a/ast.rb +++ b/ast.rb @@ -67,6 +67,21 @@ def self.of body, keep_script_lines: false, error_tolerant: false Primitive.ast_s_of body, keep_script_lines, error_tolerant 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 # RubyVM::AbstractSyntaxTree. # diff --git a/benchmark/cgi_escape_html.yml b/benchmark/cgi_escape_html.yml index af6abd08ac7b9c..655be9d7d8fde9 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

Example Domain

@@ -36,5 +27,5 @@ benchmark:
HTML - script: CGI.escapeHTML(str) + script: CGI.escapeHTML(example_html) loop_count: 1000000 diff --git a/benchmark/erb_escape_html.yml b/benchmark/erb_escape_html.yml new file mode 100644 index 00000000000000..ca28d756e7ddb6 --- /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 + +
+

Example Domain

+

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.

+

More information...

+
+ + HTML + script: ERB::Util.html_escape(example_html) + loop_count: 1000000 diff --git a/benchmark/numeric_methods.yml b/benchmark/numeric_methods.yml index 433c2268a37027..138490293538b4 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/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index bfe245116eabd2..491c700a1c1c7c 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -1620,4 +1620,10 @@ def initialize "ok" } +assert_match /\Atest_ractor\.rb:1:\s+warning:\s+Ractor is experimental/, %q{ + Warning[:experimental] = $VERBOSE = true + STDERR.reopen(STDOUT) + eval("Ractor.new{}.take", nil, "test_ractor.rb", 1) +} + end # if !ENV['GITHUB_WORKFLOW'] diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb index 66ec4e7973dfb5..8c48cee94f5d9c 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -3339,3 +3339,46 @@ def respond_to_missing?(*) = true 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 { } +} diff --git a/ccan/check_type/check_type.h b/ccan/check_type/check_type.h index e795ad71d0e80e..659e1a5a830e9a 100644 --- a/ccan/check_type/check_type.h +++ b/ccan/check_type/check_type.h @@ -44,7 +44,7 @@ * ((encl_type *) \ * ((char *)(mbr_ptr) - offsetof(enclosing_type, mbr)))) */ -#if HAVE_TYPEOF +#if defined(HAVE_TYPEOF) && HAVE_TYPEOF #define ccan_check_type(expr, type) \ ((typeof(expr) *)0 != (type *)0) diff --git a/ccan/container_of/container_of.h b/ccan/container_of/container_of.h index b30c347d57fbda..872bb6ea6ec5b0 100644 --- a/ccan/container_of/container_of.h +++ b/ccan/container_of/container_of.h @@ -112,7 +112,7 @@ static inline char *container_of_or_null_(void *member_ptr, size_t offset) * return i; * } */ -#if HAVE_TYPEOF +#if defined(HAVE_TYPEOF) && HAVE_TYPEOF #define ccan_container_of_var(member_ptr, container_var, member) \ ccan_container_of(member_ptr, typeof(*container_var), member) #else @@ -131,7 +131,7 @@ static inline char *container_of_or_null_(void *member_ptr, size_t offset) * structure memory layout. * */ -#if HAVE_TYPEOF +#if defined(HAVE_TYPEOF) && HAVE_TYPEOF #define ccan_container_off_var(var, member) \ ccan_container_off(typeof(*var), member) #else diff --git a/ccan/list/list.h b/ccan/list/list.h index 91787bfdb32769..30b2af04e954ad 100644 --- a/ccan/list/list.h +++ b/ccan/list/list.h @@ -770,7 +770,7 @@ static inline struct ccan_list_node *ccan_list_node_from_off_(void *ptr, size_t (ccan_container_off_var(var, member) + \ ccan_check_type(var->member, struct ccan_list_node)) -#if HAVE_TYPEOF +#if defined(HAVE_TYPEOF) && HAVE_TYPEOF #define ccan_list_typeof(var) typeof(var) #else #define ccan_list_typeof(var) void * diff --git a/class.c b/class.c index 3d8e205007d6fc..d181fb0b2ee635 100644 --- a/class.c +++ b/class.c @@ -213,7 +213,6 @@ class_alloc(VALUE flags, VALUE klass) #endif /* ZALLOC - RCLASS_IV_TBL(obj) = 0; RCLASS_CONST_TBL(obj) = 0; RCLASS_M_TBL(obj) = 0; RCLASS_IV_INDEX_TBL(obj) = 0; @@ -402,23 +401,19 @@ class_init_copy_check(VALUE clone, VALUE orig) static void copy_tables(VALUE clone, VALUE orig) { - if (RCLASS_IV_TBL(clone)) { - st_free_table(RCLASS_IV_TBL(clone)); - RCLASS_IV_TBL(clone) = 0; - } if (RCLASS_CONST_TBL(clone)) { rb_free_const_table(RCLASS_CONST_TBL(clone)); RCLASS_CONST_TBL(clone) = 0; } RCLASS_M_TBL(clone) = 0; - if (RCLASS_IV_TBL(orig)) { + if (!RB_TYPE_P(clone, T_ICLASS)) { st_data_t id; rb_iv_tbl_copy(clone, orig); CONST_ID(id, "__tmp_classpath__"); - st_delete(RCLASS_IV_TBL(clone), &id, 0); + rb_attr_delete(clone, id); CONST_ID(id, "__classpath__"); - st_delete(RCLASS_IV_TBL(clone), &id, 0); + rb_attr_delete(clone, id); } if (RCLASS_CONST_TBL(orig)) { struct clone_const_arg arg; @@ -520,7 +515,6 @@ rb_mod_init_copy(VALUE clone, VALUE orig) prev_clone_p = clone_p; RCLASS_M_TBL(clone_p) = RCLASS_M_TBL(p); RCLASS_CONST_TBL(clone_p) = RCLASS_CONST_TBL(p); - RCLASS_IV_TBL(clone_p) = RCLASS_IV_TBL(p); RCLASS_ALLOCATOR(clone_p) = RCLASS_ALLOCATOR(p); if (RB_TYPE_P(clone, T_CLASS)) { RCLASS_SET_INCLUDER(clone_p, clone); @@ -607,9 +601,7 @@ rb_singleton_class_clone_and_attach(VALUE obj, VALUE attach) RCLASS_SET_SUPER(clone, RCLASS_SUPER(klass)); RCLASS_ALLOCATOR(clone) = RCLASS_ALLOCATOR(klass); - if (RCLASS_IV_TBL(klass)) { - rb_iv_tbl_copy(clone, klass); - } + rb_iv_tbl_copy(clone, klass); if (RCLASS_CONST_TBL(klass)) { struct clone_const_arg arg; arg.tbl = RCLASS_CONST_TBL(clone) = rb_id_table_create(0); @@ -1062,13 +1054,10 @@ rb_include_class_new(VALUE module, VALUE super) module = METACLASS_OF(module); } RUBY_ASSERT(!RB_TYPE_P(module, T_ICLASS)); - if (!RCLASS_IV_TBL(module)) { - RCLASS_IV_TBL(module) = st_init_numtable(); - } if (!RCLASS_CONST_TBL(module)) { RCLASS_CONST_TBL(module) = rb_id_table_create(0); } - RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module); + RCLASS_CVC_TBL(klass) = RCLASS_CVC_TBL(module); RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module); diff --git a/common.mk b/common.mk index 97df2e12700b8f..e03af24cfcb04a 100644 --- a/common.mk +++ b/common.mk @@ -50,7 +50,7 @@ GEM_PATH = GEM_VENDOR = BENCHMARK_DRIVER_GIT_URL = https://github.com/benchmark-driver/benchmark-driver -BENCHMARK_DRIVER_GIT_REF = v0.15.18 +BENCHMARK_DRIVER_GIT_REF = v0.16.0 SIMPLECOV_GIT_URL = https://github.com/colszowka/simplecov.git SIMPLECOV_GIT_REF = v0.17.0 SIMPLECOV_HTML_GIT_URL = https://github.com/colszowka/simplecov-html.git @@ -303,6 +303,8 @@ showflags: " LC_ALL = $(LC_ALL)" \ " LC_CTYPE = $(LC_CTYPE)" \ " MFLAGS = $(MFLAGS)" \ + " RUST = $(RUST)" \ + " YJIT_RUSTC_ARGS = $(YJIT_RUSTC_ARGS)" \ $(MESSAGE_END) -@$(CC_VERSION) @@ -1228,13 +1230,8 @@ $(BUILTIN_RB_INCS): $(top_srcdir)/tool/mk_builtin_loader.rb $(srcdir)/revision.h: $(REVISION_H) -revision.$(HAVE_BASERUBY:no=tmp):: - $(Q) $(NULLCMD) > $@ -revision.$(HAVE_BASERUBY:yes=tmp):: $(srcdir)/version.h $(tooldir)/file2lastrev.rb $(REVISION_FORCE) - $(Q) $(BASERUBY) $(tooldir)/file2lastrev.rb -q --revision.h --srcdir="$(srcdir)" > $@ - -$(REVISION_H): revision.tmp - $(Q)$(IFCHANGE) "--timestamp=$@" "$(srcdir)/revision.h" revision.tmp +$(REVISION_H): + $(Q) $(BASERUBY) $(tooldir)/file2lastrev.rb -q --revision.h --srcdir="$(srcdir)" --output=revision.h --timestamp=$@ $(srcdir)/ext/ripper/ripper.c: $(srcdir)/ext/ripper/tools/preproc.rb $(srcdir)/parse.y $(srcdir)/defs/id.def $(srcdir)/ext/ripper/depend $(ECHO) generating $@ @@ -18010,6 +18007,7 @@ yjit.$(OBJEXT): $(top_srcdir)/internal/array.h yjit.$(OBJEXT): $(top_srcdir)/internal/class.h yjit.$(OBJEXT): $(top_srcdir)/internal/compile.h yjit.$(OBJEXT): $(top_srcdir)/internal/compilers.h +yjit.$(OBJEXT): $(top_srcdir)/internal/cont.h yjit.$(OBJEXT): $(top_srcdir)/internal/fixnum.h yjit.$(OBJEXT): $(top_srcdir)/internal/gc.h yjit.$(OBJEXT): $(top_srcdir)/internal/hash.h @@ -18207,6 +18205,7 @@ yjit.$(OBJEXT): {$(VPATH)}probes.h yjit.$(OBJEXT): {$(VPATH)}probes_helper.h yjit.$(OBJEXT): {$(VPATH)}ruby_assert.h yjit.$(OBJEXT): {$(VPATH)}ruby_atomic.h +yjit.$(OBJEXT): {$(VPATH)}shape.h yjit.$(OBJEXT): {$(VPATH)}st.h yjit.$(OBJEXT): {$(VPATH)}subst.h yjit.$(OBJEXT): {$(VPATH)}thread_$(THREAD_MODEL).h diff --git a/complex.c b/complex.c index db114cd9142262..d927f62d5578ec 100644 --- a/complex.c +++ b/complex.c @@ -2159,31 +2159,6 @@ nucomp_s_convert(int argc, VALUE *argv, VALUE klass) return nucomp_convert(klass, a1, a2, TRUE); } -/* - * call-seq: - * num.real -> self - * - * Returns self. - */ -static VALUE -numeric_real(VALUE self) -{ - return self; -} - -/* - * call-seq: - * num.imag -> 0 - * num.imaginary -> 0 - * - * Returns zero. - */ -static VALUE -numeric_imag(VALUE self) -{ - return INT2FIX(0); -} - /* * call-seq: * num.abs2 -> real @@ -2255,19 +2230,6 @@ numeric_polar(VALUE self) return rb_assoc_new(abs, arg); } -/* - * call-seq: - * num.conj -> self - * num.conjugate -> self - * - * Returns self. - */ -static VALUE -numeric_conj(VALUE self) -{ - return self; -} - /* * call-seq: * flo.arg -> 0 or float @@ -2433,9 +2395,6 @@ Init_Complex(void) rb_define_private_method(CLASS_OF(rb_cComplex), "convert", nucomp_s_convert, -1); - rb_define_method(rb_cNumeric, "real", numeric_real, 0); - rb_define_method(rb_cNumeric, "imaginary", numeric_imag, 0); - rb_define_method(rb_cNumeric, "imag", numeric_imag, 0); rb_define_method(rb_cNumeric, "abs2", numeric_abs2, 0); rb_define_method(rb_cNumeric, "arg", numeric_arg, 0); rb_define_method(rb_cNumeric, "angle", numeric_arg, 0); @@ -2443,8 +2402,6 @@ Init_Complex(void) rb_define_method(rb_cNumeric, "rectangular", numeric_rect, 0); rb_define_method(rb_cNumeric, "rect", numeric_rect, 0); rb_define_method(rb_cNumeric, "polar", numeric_polar, 0); - rb_define_method(rb_cNumeric, "conjugate", numeric_conj, 0); - rb_define_method(rb_cNumeric, "conj", numeric_conj, 0); rb_define_method(rb_cFloat, "arg", float_arg, 0); rb_define_method(rb_cFloat, "angle", float_arg, 0); diff --git a/configure.ac b/configure.ac index 596d915a0f2ac5..93fdb5fed7ebd7 100644 --- a/configure.ac +++ b/configure.ac @@ -786,7 +786,8 @@ AS_IF([test "$GCC" = yes], [ [disable -D_FORTIFY_SOURCE=2 option, which causes link error on mingw]), [fortify_source=$enableval]) AS_IF([test "x$fortify_source" != xno], [ - RUBY_TRY_CFLAGS([$optflags -D_FORTIFY_SOURCE=2], [RUBY_APPEND_OPTION(XCFLAGS, -D_FORTIFY_SOURCE=2)], [], + RUBY_TRY_CFLAGS([$optflags -D_FORTIFY_SOURCE=2], + [RUBY_APPEND_OPTION(XCFLAGS, -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2)], [], [@%:@include ]) ]) @@ -794,7 +795,7 @@ AS_IF([test "$GCC" = yes], [ # -fstack-protector AS_CASE(["$target_os"], - [mingw*|emscripten*|wasi*], [ + [emscripten*|wasi*], [ stack_protector=no ]) AS_IF([test -z "${stack_protector+set}"], [ @@ -806,6 +807,8 @@ AS_IF([test "$GCC" = yes], [ AS_IF([test "x$stack_protector" = xyes], [stack_protector=option; break]) ]) ]) + AC_MSG_CHECKING([for -fstack-protector]) + AC_MSG_RESULT(["$stack_protector"]) AS_CASE(["$stack_protector"], [-*], [ RUBY_APPEND_OPTION(XCFLAGS, $stack_protector) RUBY_APPEND_OPTION(XLDFLAGS, $stack_protector) @@ -3733,10 +3736,37 @@ AS_IF([test x"$MJIT_SUPPORT" = "xyes"], AC_SUBST(MJIT_SUPPORT) +AC_CHECK_PROG(RUSTC, [rustc], [rustc], [no]) dnl no ac_tool_prefix + +dnl check if we can build YJIT on this target platform +AS_CASE(["$target_cpu-$target_os"], + [arm64-darwin*|aarch64-darwin*|x86_64-darwin*], [ + YJIT_TARGET_OK=yes + ], + [arm64-*linux*|aarch64-*linux*|x86_64-*linux*], [ + YJIT_TARGET_OK=yes + ], + [arm64-*bsd*|aarch64-*bsd*|x86_64-*bsd*], [ + YJIT_TARGET_OK=yes + ], + [YJIT_TARGET_OK=no] +) + +dnl build YJIT in release mode if rustc >= 1.58.0 is present and we are on a supported platform AC_ARG_ENABLE(yjit, - AS_HELP_STRING([--enable-yjit], - [enable experimental in-process JIT compiler that requires Rust build tools [default=no]]), - [YJIT_SUPPORT=$enableval], [YJIT_SUPPORT=no]) + AS_HELP_STRING([--enable-yjit], + [enable experimental in-process JIT compiler that requires Rust build tools [default=no]]), + [YJIT_SUPPORT=$enableval], + [ + AS_IF([test x"$RUSTC" != "xno"], + AS_IF([ echo "fn main() { let x = 1; format!(\"{x}\"); }" | $RUSTC - --emit asm=/dev/null ], + [YJIT_SUPPORT="$YJIT_TARGET_OK"], + [YJIT_SUPPORT=no] + ), + [YJIT_SUPPORT=no] + ) + ] +) CARGO= CARGO_BUILD_ARGS= @@ -3746,10 +3776,10 @@ AS_CASE(["${YJIT_SUPPORT}"], AS_IF([test x"$enable_jit_support" = "xno"], AC_MSG_ERROR([--disable-jit-support but --enable-yjit. YJIT requires JIT support]) ) - AC_CHECK_TOOL(RUSTC, [rustc], [no]) AS_IF([test x"$RUSTC" = "xno"], AC_MSG_ERROR([rustc is required. Installation instructions available at https://www.rust-lang.org/tools/install]) ) + AS_IF([test "$cross_compiling" = yes], [RUSTC="$RUSTC --target=$target"]) AS_CASE(["${YJIT_SUPPORT}"], [yes], [ @@ -3784,7 +3814,6 @@ AS_CASE(["${YJIT_SUPPORT}"], AC_DEFINE(USE_YJIT, 1) ], [AC_DEFINE(USE_YJIT, 0)]) - dnl These variables end up in ::RbConfig::CONFIG AC_SUBST(YJIT_SUPPORT)dnl what flavor of YJIT the Ruby build includes AC_SUBST(RUSTC)dnl Rust compiler command diff --git a/cont.c b/cont.c index b3c84d82ac5cac..a85b7fc2d1338a 100644 --- a/cont.c +++ b/cont.c @@ -69,7 +69,7 @@ static VALUE rb_cFiberPool; #define FIBER_POOL_ALLOCATION_FREE #endif -#define jit_cont_enabled mjit_enabled // To be used by YJIT later +#define jit_cont_enabled (mjit_enabled || rb_yjit_enabled_p()) enum context_type { CONTINUATION_CONTEXT = 0, @@ -693,21 +693,36 @@ fiber_pool_stack_free(struct fiber_pool_stack * stack) if (DEBUG) fprintf(stderr, "fiber_pool_stack_free: %p+%"PRIuSIZE" [base=%p, size=%"PRIuSIZE"]\n", base, size, stack->base, stack->size); -#if VM_CHECK_MODE > 0 && defined(MADV_DONTNEED) + // The pages being used by the stack can be returned back to the system. + // That doesn't change the page mapping, but it does allow the system to + // reclaim the physical memory. + // Since we no longer care about the data itself, we don't need to page + // out to disk, since that is costly. Not all systems support that, so + // we try our best to select the most efficient implementation. + // In addition, it's actually slightly desirable to not do anything here, + // but that results in higher memory usage. + +#ifdef __wasi__ + // WebAssembly doesn't support madvise, so we just don't do anything. +#elif VM_CHECK_MODE > 0 && defined(MADV_DONTNEED) // This immediately discards the pages and the memory is reset to zero. madvise(base, size, MADV_DONTNEED); -#elif defined(POSIX_MADV_DONTNEED) - posix_madvise(base, size, POSIX_MADV_DONTNEED); #elif defined(MADV_FREE_REUSABLE) + // Darwin / macOS / iOS. // Acknowledge the kernel down to the task info api we make this // page reusable for future use. // As for MADV_FREE_REUSE below we ensure in the rare occasions the task was not // completed at the time of the call to re-iterate. while (madvise(base, size, MADV_FREE_REUSABLE) == -1 && errno == EAGAIN); #elif defined(MADV_FREE) + // Recent Linux. madvise(base, size, MADV_FREE); #elif defined(MADV_DONTNEED) + // Old Linux. madvise(base, size, MADV_DONTNEED); +#elif defined(POSIX_MADV_DONTNEED) + // Solaris? + posix_madvise(base, size, POSIX_MADV_DONTNEED); #elif defined(_WIN32) VirtualAlloc(base, size, MEM_RESET, PAGE_READWRITE); // Not available in all versions of Windows. @@ -2269,7 +2284,7 @@ root_fiber_alloc(rb_thread_t *th) VM_ASSERT(DATA_PTR(fiber_value) == NULL); VM_ASSERT(fiber->cont.type == FIBER_CONTEXT); - VM_ASSERT(fiber->status == FIBER_RESUMED); + VM_ASSERT(FIBER_RESUMED_P(fiber)); th->root_fiber = fiber; DATA_PTR(fiber_value) = fiber; diff --git a/doc/contributing/building_ruby.md b/doc/contributing/building_ruby.md index 52d6042ec3a41d..ace5fbed37c5e8 100644 --- a/doc/contributing/building_ruby.md +++ b/doc/contributing/building_ruby.md @@ -18,6 +18,7 @@ * libffi * libyaml * libexecinfo (FreeBSD) + * rustc - 1.58.1 or later (if you wish to build [YJIT](/doc/yjit/yjit.md)) 3. Checkout the CRuby source code: @@ -38,7 +39,7 @@ 5. Optional: If you are frequently building Ruby, disabling documentation will reduce the time it takes to `make`: ``` shell - ../configure --disable-install-doc + ../configure --prefix="${HOME}/.rubies/ruby-master" --disable-install-doc ``` 6. [Run tests](testing_ruby.md) to confirm your build succeeded diff --git a/doc/io_streams.rdoc b/doc/io_streams.rdoc index 3ee85926871ce5..c8ce9991cfcce9 100644 --- a/doc/io_streams.rdoc +++ b/doc/io_streams.rdoc @@ -189,14 +189,13 @@ The relevant methods: A new \IO stream may be open for reading, open for writing, or both. -You can close a stream using these methods: +A stream is automatically closed when claimed by the garbage collector. -- IO#close: Closes the stream for both reading and writing. -- IO#close_read (not in \ARGF): Closes the stream for reading. -- IO#close_write (not in \ARGF): Closes the stream for writing. - -You can query whether a stream is closed using this method: +Attempted reading or writing on a closed stream raises an exception. +- IO#close: Closes the stream for both reading and writing. +- IO#close_read: Closes the stream for reading; not in ARGF. +- IO#close_write: Closes the stream for writing; not in ARGF. - IO#closed?: Returns whether the stream is closed. ==== End-of-Stream diff --git a/doc/regexp.rdoc b/doc/regexp.rdoc index d623487a98e9ef..92c7ecf66e8d7b 100644 --- a/doc/regexp.rdoc +++ b/doc/regexp.rdoc @@ -28,7 +28,7 @@ Specifically, /st/ requires that the string contains the letter _s_ followed by the letter _t_, so it matches _haystack_, also. Note that any Regexp matching will raise a RuntimeError if timeout is set and -exceeded. See "Timeout" section in detail. +exceeded. See {"Timeout"}[#label-Timeout] section in detail. == \Regexp Interpolation @@ -781,7 +781,7 @@ with a{0,29}: == Timeout -There are two APIs to set timeout. One is Timeout.timeout=, which is +There are two APIs to set timeout. One is Regexp.timeout=, which is process-global configuration of timeout for Regexp matching. Regexp.timeout = 3 diff --git a/doc/yjit/yjit.md b/doc/yjit/yjit.md index d4a15d0c7795e7..bf11eb523d5b79 100644 --- a/doc/yjit/yjit.md +++ b/doc/yjit/yjit.md @@ -41,13 +41,9 @@ To cite this repository in your publications, please use this bibtex snippet: ## Current Limitations -YJIT is a work in progress and as such may not yet be mature enough for mission-critical software. Below is a list of known limitations, all of which we plan to eventually address: - -- No garbage collection for generated code. -- Currently supports only macOS and Linux. -- Supports x86-64 and arm64/aarch64 CPUs only. - -Because there is no GC for generated code yet, your software could run out of executable memory if it is large enough. You can change how much executable memory is allocated using [YJIT's command-line options](#command-line-options). +YJIT may not be suitable for certain applications. It currently only supports macOS and Linux on x86-64 and arm64/aarch64 CPUs. YJIT will use more memory than the Ruby interpreter because the JIT compiler needs to generate machine code in memory and maintain additional state +information. +You can change how much executable memory is allocated using [YJIT's command-line options](#command-line-options). There is a slight performance tradeoff because allocating less executable memory could result in the generated machine code being collected more often. ## Installation diff --git a/ext/bigdecimal/missing.h b/ext/bigdecimal/missing.h index 307147c0fdc6bf..325554b5f536d3 100644 --- a/ext/bigdecimal/missing.h +++ b/ext/bigdecimal/missing.h @@ -35,10 +35,10 @@ extern "C" { #endif /* RB_UNUSED_VAR */ #if defined(_MSC_VER) && _MSC_VER >= 1310 -# define HAVE___ASSUME +# define HAVE___ASSUME 1 #elif defined(__INTEL_COMPILER) && __INTEL_COMPILER >= 1300 -# define HAVE___ASSUME +# define HAVE___ASSUME 1 #endif #ifndef UNREACHABLE diff --git a/ext/date/date_core.c b/ext/date/date_core.c index 96653e0a7879e2..e58da719e0c23a 100644 --- a/ext/date/date_core.c +++ b/ext/date/date_core.c @@ -761,6 +761,8 @@ c_valid_civil_p(int y, int m, int d, double sg, if (m < 0) m += 13; + if (m < 1 || m > 12) + return 0; if (d < 0) { if (!c_find_ldom(y, m, sg, rjd, ns)) return 0; diff --git a/ext/digest/digest_conf.rb b/ext/digest/digest_conf.rb index 1b929d87322577..36a7d75289d081 100644 --- a/ext/digest/digest_conf.rb +++ b/ext/digest/digest_conf.rb @@ -3,7 +3,7 @@ def digest_conf(name) unless with_config("bundled-#{name}") cc = with_config("common-digest") - if cc == true or /\b#{name}\b/ =~ cc + if cc != false or /\b#{name}\b/ =~ cc if File.exist?("#$srcdir/#{name}cc.h") and have_header("CommonCrypto/CommonDigest.h") $defs << "-D#{name.upcase}_USE_COMMONDIGEST" diff --git a/ext/digest/md5/depend b/ext/digest/md5/depend index 0353e7a40dde30..ea1ceec7fd36cd 100644 --- a/ext/digest/md5/depend +++ b/ext/digest/md5/depend @@ -326,5 +326,6 @@ md5init.o: $(hdrdir)/ruby/subst.h md5init.o: $(srcdir)/../defs.h md5init.o: $(srcdir)/../digest.h md5init.o: md5.h +md5init.o: md5cc.h md5init.o: md5init.c # AUTOGENERATED DEPENDENCIES END diff --git a/ext/digest/sha1/depend b/ext/digest/sha1/depend index a4e454d21470fb..48aaef158b3c39 100644 --- a/ext/digest/sha1/depend +++ b/ext/digest/sha1/depend @@ -326,5 +326,6 @@ sha1init.o: $(hdrdir)/ruby/subst.h sha1init.o: $(srcdir)/../defs.h sha1init.o: $(srcdir)/../digest.h sha1init.o: sha1.h +sha1init.o: sha1cc.h sha1init.o: sha1init.c # AUTOGENERATED DEPENDENCIES END diff --git a/ext/digest/sha2/depend b/ext/digest/sha2/depend index 2fb598aa4827f6..47a859068cce0d 100644 --- a/ext/digest/sha2/depend +++ b/ext/digest/sha2/depend @@ -325,5 +325,6 @@ sha2init.o: $(hdrdir)/ruby/st.h sha2init.o: $(hdrdir)/ruby/subst.h sha2init.o: $(srcdir)/../digest.h sha2init.o: sha2.h +sha2init.o: sha2cc.h sha2init.o: sha2init.c # AUTOGENERATED DEPENDENCIES END diff --git a/ext/erb/erb.c b/ext/erb/erb.c new file mode 100644 index 00000000000000..b72c295a816e91 --- /dev/null +++ b/ext/erb/erb.c @@ -0,0 +1,95 @@ +#include "ruby.h" +#include "ruby/encoding.h" + +static VALUE rb_cERB, rb_mUtil, rb_cCGI; +static ID id_escapeHTML; + +#define HTML_ESCAPE_MAX_LEN 6 + +static const struct { + uint8_t len; + char str[HTML_ESCAPE_MAX_LEN+1]; +} html_escape_table[UCHAR_MAX+1] = { +#define HTML_ESCAPE(c, str) [c] = {rb_strlen_lit(str), str} + HTML_ESCAPE('\'', "'"), + HTML_ESCAPE('&', "&"), + HTML_ESCAPE('"', """), + HTML_ESCAPE('<', "<"), + HTML_ESCAPE('>', ">"), +#undef HTML_ESCAPE +}; + +static inline void +preserve_original_state(VALUE orig, VALUE dest) +{ + rb_enc_associate(dest, rb_enc_get(orig)); +} + +static inline long +escaped_length(VALUE str) +{ + const long len = RSTRING_LEN(str); + if (len >= LONG_MAX / HTML_ESCAPE_MAX_LEN) { + ruby_malloc_size_overflow(len, HTML_ESCAPE_MAX_LEN); + } + return len * HTML_ESCAPE_MAX_LEN; +} + +static VALUE +optimized_escape_html(VALUE str) +{ + VALUE vbuf; + char *buf = ALLOCV_N(char, vbuf, escaped_length(str)); + const char *cstr = RSTRING_PTR(str); + const char *end = cstr + RSTRING_LEN(str); + + char *dest = buf; + while (cstr < end) { + const unsigned char c = *cstr++; + uint8_t len = html_escape_table[c].len; + if (len) { + memcpy(dest, html_escape_table[c].str, len); + dest += len; + } + else { + *dest++ = c; + } + } + + VALUE escaped = str; + if (RSTRING_LEN(str) < (dest - buf)) { + escaped = rb_str_new(buf, dest - buf); + preserve_original_state(str, escaped); + } + ALLOCV_END(vbuf); + return escaped; +} + +// ERB::Util.html_escape is different from CGI.escapeHTML in the following two parts: +// * ERB::Util.html_escape converts an argument with #to_s first (only if it's not T_STRING) +// * ERB::Util.html_escape does not allocate a new string when nothing needs to be escaped +static VALUE +erb_escape_html(VALUE self, VALUE str) +{ + if (!RB_TYPE_P(str, T_STRING)) { + str = rb_convert_type(str, T_STRING, "String", "to_s"); + } + + if (rb_enc_str_asciicompat_p(str)) { + return optimized_escape_html(str); + } + else { + return rb_funcall(rb_cCGI, id_escapeHTML, 1, str); + } +} + +void +Init_erb(void) +{ + rb_cERB = rb_define_class("ERB", rb_cObject); + rb_mUtil = rb_define_module_under(rb_cERB, "Util"); + rb_define_method(rb_mUtil, "html_escape", erb_escape_html, 1); + + rb_cCGI = rb_define_class("CGI", rb_cObject); + id_escapeHTML = rb_intern("escapeHTML"); +} diff --git a/ext/erb/extconf.rb b/ext/erb/extconf.rb new file mode 100644 index 00000000000000..00a7e92aea10a2 --- /dev/null +++ b/ext/erb/extconf.rb @@ -0,0 +1,2 @@ +require 'mkmf' +create_makefile 'erb' diff --git a/ext/objspace/objspace_dump.c b/ext/objspace/objspace_dump.c index 2917d493310ea9..7c7cae34887f9c 100644 --- a/ext/objspace/objspace_dump.c +++ b/ext/objspace/objspace_dump.c @@ -14,6 +14,7 @@ #include "gc.h" #include "internal.h" +#include "internal/array.h" #include "internal/hash.h" #include "internal/string.h" #include "internal/sanitizers.h" @@ -456,7 +457,7 @@ dump_object(VALUE obj, struct dump_config *dc) case T_ARRAY: dump_append(dc, ", \"length\":"); dump_append_ld(dc, RARRAY_LEN(obj)); - if (RARRAY_LEN(obj) > 0 && FL_TEST(obj, ELTS_SHARED)) + if (RARRAY_LEN(obj) > 0 && FL_TEST(obj, RARRAY_SHARED_FLAG)) dump_append(dc, ", \"shared\":true"); if (FL_TEST(obj, RARRAY_EMBED_FLAG)) dump_append(dc, ", \"embedded\":true"); diff --git a/ext/socket/extconf.rb b/ext/socket/extconf.rb index b70a86241497a1..8998bb5c2f2869 100644 --- a/ext/socket/extconf.rb +++ b/ext/socket/extconf.rb @@ -655,12 +655,20 @@ def %(s) s || self end end hdr = "netinet6/in6.h" - if /darwin/ =~ RUBY_PLATFORM and !try_compile(<<"SRC", nil, :werror=>true) + /darwin/ =~ RUBY_PLATFORM and + checking_for("if apple's #{hdr} needs s6_addr patch") {!try_compile(<<"SRC", nil, :werror=>true)} and #include int t(struct in6_addr *addr) {return IN6_IS_ADDR_UNSPECIFIED(addr);} SRC - print "fixing apple's netinet6/in6.h ..."; $stdout.flush - in6 = File.read("/usr/include/#{hdr}") + checking_for("fixing apple's #{hdr}", "%s") do + file = xpopen(%w"clang -include netinet/in.h -E -xc -", in: IO::NULL) do |f| + re = %r[^# *\d+ *"(.*/netinet/in\.h)"] + Logging.message " grep(#{re})\n" + f.read[re, 1] + end + Logging.message "Substitute from #{file}\n" + + in6 = File.read(file) if in6.gsub!(/\*\(const\s+__uint32_t\s+\*\)\(const\s+void\s+\*\)\(&(\(\w+\))->s6_addr\[(\d+)\]\)/) do i, r = $2.to_i.divmod(4) if r.zero? @@ -673,9 +681,9 @@ def %(s) s || self end open(hdr, "w") {|f| f.write(in6)} $distcleanfiles << hdr $distcleandirs << File.dirname(hdr) - puts "done" + "done" else - puts "not needed" + "not needed" end end create_makefile("socket") diff --git a/gc.c b/gc.c index c36bdedbc0c400..0309081d2c166b 100644 --- a/gc.c +++ b/gc.c @@ -3684,16 +3684,6 @@ obj_free(rb_objspace_t *objspace, VALUE obj) RB_DEBUG_COUNTER_INC(obj_obj_transient); } else { - rb_shape_t *shape = rb_shape_get_shape_by_id(ROBJECT_SHAPE_ID(obj)); - if (shape) { - VALUE klass = RBASIC_CLASS(obj); - - // Increment max_iv_count if applicable, used to determine size pool allocation - uint32_t num_of_ivs = shape->next_iv_index; - if (RCLASS_EXT(klass)->max_iv_count < num_of_ivs) { - RCLASS_EXT(klass)->max_iv_count = num_of_ivs; - } - } xfree(RANY(obj)->as.object.as.heap.ivptr); RB_DEBUG_COUNTER_INC(obj_obj_ptr); } @@ -3702,8 +3692,8 @@ obj_free(rb_objspace_t *objspace, VALUE obj) case T_CLASS: rb_id_table_free(RCLASS_M_TBL(obj)); cc_table_free(objspace, obj, FALSE); - if (RCLASS_IV_TBL(obj)) { - st_free_table(RCLASS_IV_TBL(obj)); + if (RCLASS_IVPTR(obj)) { + xfree(RCLASS_IVPTR(obj)); } if (RCLASS_CONST_TBL(obj)) { rb_free_const_table(RCLASS_CONST_TBL(obj)); @@ -5231,15 +5221,11 @@ obj_memsize_of(VALUE obj, int use_all_types) if (RCLASS_M_TBL(obj)) { size += rb_id_table_memsize(RCLASS_M_TBL(obj)); } - if (RCLASS_IV_TBL(obj)) { - size += st_memsize(RCLASS_IV_TBL(obj)); - } + // class IV sizes are allocated as powers of two + size += SIZEOF_VALUE << bit_length(RCLASS_IV_COUNT(obj)); if (RCLASS_CVC_TBL(obj)) { size += rb_id_table_memsize(RCLASS_CVC_TBL(obj)); } - if (RCLASS_EXT(obj)->iv_tbl) { - size += st_memsize(RCLASS_EXT(obj)->iv_tbl); - } if (RCLASS_EXT(obj)->const_tbl) { size += rb_id_table_memsize(RCLASS_EXT(obj)->const_tbl); } @@ -5594,7 +5580,9 @@ try_move(rb_objspace_t *objspace, rb_heap_t *heap, struct heap_page *free_page, if (gc_is_moveable_obj(objspace, src)) { GC_ASSERT(MARKED_IN_BITMAP(GET_HEAP_MARK_BITS(src), src)); + asan_unlock_freelist(free_page); VALUE dest = (VALUE)free_page->freelist; + asan_lock_freelist(free_page); asan_unpoison_object(dest, false); if (!dest) { /* if we can't get something from the freelist then the page must be @@ -7145,8 +7133,10 @@ mark_current_machine_context(rb_objspace_t *objspace, rb_execution_context_t *ec static void mark_current_machine_context(rb_objspace_t *objspace, rb_execution_context_t *ec) { - rb_wasm_scan_stack(rb_mark_locations); - each_stack_location(objspace, ec, rb_stack_range_tmp[0], rb_stack_range_tmp[1], gc_mark_maybe); + VALUE *stack_start, *stack_end; + SET_STACK_END; + GET_STACK_BOUNDS(stack_start, stack_end, 1); + each_stack_location(objspace, ec, stack_start, stack_end, gc_mark_maybe); rb_wasm_scan_locals(rb_mark_locations); each_stack_location(objspace, ec, rb_stack_range_tmp[0], rb_stack_range_tmp[1], gc_mark_maybe); @@ -7616,7 +7606,9 @@ gc_mark_children(rb_objspace_t *objspace, VALUE obj) mark_m_tbl(objspace, RCLASS_M_TBL(obj)); cc_table_mark(objspace, obj); - mark_tbl_no_pin(objspace, RCLASS_IV_TBL(obj)); + for (attr_index_t i = 0; i < RCLASS_IV_COUNT(obj); i++) { + gc_mark(objspace, RCLASS_IVPTR(obj)[i]); + } mark_const_tbl(objspace, RCLASS_CONST_TBL(obj)); break; @@ -7687,6 +7679,17 @@ gc_mark_children(rb_objspace_t *objspace, VALUE obj) gc_mark(objspace, ptr[i]); } + rb_shape_t *shape = rb_shape_get_shape_by_id(ROBJECT_SHAPE_ID(obj)); + if (shape) { + VALUE klass = RBASIC_CLASS(obj); + + // Increment max_iv_count if applicable, used to determine size pool allocation + uint32_t num_of_ivs = shape->next_iv_index; + if (RCLASS_EXT(klass)->max_iv_count < num_of_ivs) { + RCLASS_EXT(klass)->max_iv_count = num_of_ivs; + } + } + if (LIKELY(during_gc) && ROBJ_TRANSIENT_P(obj)) { rb_transient_heap_mark(obj, ptr); @@ -10899,7 +10902,9 @@ gc_update_object_references(rb_objspace_t *objspace, VALUE obj) update_cvc_tbl(objspace, obj); update_superclasses(objspace, obj); - gc_update_tbl_refs(objspace, RCLASS_IV_TBL(obj)); + for (attr_index_t i = 0; i < RCLASS_IV_COUNT(obj); i++) { + UPDATE_IF_MOVED(objspace, RCLASS_IVPTR(obj)[i]); + } update_class_ext(objspace, RCLASS_EXT(obj)); update_const_tbl(objspace, RCLASS_CONST_TBL(obj)); @@ -10914,9 +10919,6 @@ gc_update_object_references(rb_objspace_t *objspace, VALUE obj) UPDATE_IF_MOVED(objspace, RCLASS(obj)->super); } if (!RCLASS_EXT(obj)) break; - if (RCLASS_IV_TBL(obj)) { - gc_update_tbl_refs(objspace, RCLASS_IV_TBL(obj)); - } update_class_ext(objspace, RCLASS_EXT(obj)); update_m_tbl(objspace, RCLASS_CALLABLE_M_TBL(obj)); update_cc_tbl(objspace, obj); diff --git a/gems/bundled_gems b/gems/bundled_gems index e01e71225034d3..b97fa0343320a6 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -8,7 +8,7 @@ rss 0.2.9 https://github.com/ruby/rss net-ftp 0.2.0 https://github.com/ruby/net-ftp net-imap 0.3.1 https://github.com/ruby/net-imap net-pop 0.1.2 https://github.com/ruby/net-pop -net-smtp 0.3.2 https://github.com/ruby/net-smtp +net-smtp 0.3.3 https://github.com/ruby/net-smtp matrix 0.4.2 https://github.com/ruby/matrix prime 0.1.2 https://github.com/ruby/prime rbs 2.7.0 https://github.com/ruby/rbs diff --git a/include/ruby/internal/intern/gc.h b/include/ruby/internal/intern/gc.h index e7b8008729b058..ae79498cf372ed 100644 --- a/include/ruby/internal/intern/gc.h +++ b/include/ruby/internal/intern/gc.h @@ -26,7 +26,7 @@ # include /* size_t */ #endif -#if HAVE_SYS_TYPES_H +#ifdef HAVE_SYS_TYPES_H # include /* ssize_t */ #endif diff --git a/include/ruby/memory_view.h b/include/ruby/memory_view.h index 83931038a05605..1ddca2d46f0663 100644 --- a/include/ruby/memory_view.h +++ b/include/ruby/memory_view.h @@ -16,7 +16,7 @@ # include /* size_t */ #endif -#if HAVE_SYS_TYPES_H +#ifdef HAVE_SYS_TYPES_H # include /* ssize_t */ #endif diff --git a/include/ruby/onigmo.h b/include/ruby/onigmo.h index a7ef59c7c881e6..d71dfb80fb03a0 100644 --- a/include/ruby/onigmo.h +++ b/include/ruby/onigmo.h @@ -356,9 +356,9 @@ int onigenc_ascii_only_case_map(OnigCaseFoldType* flagP, const OnigUChar** pp, c #define ONIGENC_PRECISE_MBC_ENC_LEN(enc,p,e) (enc)->precise_mbc_enc_len(p,e,enc) ONIG_EXTERN -int onigenc_mbclen_approximate(const OnigUChar* p,const OnigUChar* e, const struct OnigEncodingTypeST* enc); +int onigenc_mbclen(const OnigUChar* p,const OnigUChar* e, const struct OnigEncodingTypeST* enc); -#define ONIGENC_MBC_ENC_LEN(enc,p,e) onigenc_mbclen_approximate(p,e,enc) +#define ONIGENC_MBC_ENC_LEN(enc,p,e) onigenc_mbclen(p,e,enc) #define ONIGENC_MBC_MAXLEN(enc) ((enc)->max_enc_len) #define ONIGENC_MBC_MAXLEN_DIST(enc) ONIGENC_MBC_MAXLEN(enc) #define ONIGENC_MBC_MINLEN(enc) ((enc)->min_enc_len) diff --git a/include/ruby/util.h b/include/ruby/util.h index f0ea874322c338..e8727a32005648 100644 --- a/include/ruby/util.h +++ b/include/ruby/util.h @@ -19,7 +19,7 @@ # include /* size_t */ #endif -#if HAVE_SYS_TYPES_H +#ifdef HAVE_SYS_TYPES_H # include /* ssize_t */ #endif diff --git a/internal/class.h b/internal/class.h index 63fe84c6ab0013..784d508e20d194 100644 --- a/internal/class.h +++ b/internal/class.h @@ -33,7 +33,7 @@ struct rb_cvar_class_tbl_entry { }; struct rb_classext_struct { - struct st_table *iv_tbl; + VALUE *iv_ptr; struct rb_id_table *const_tbl; struct rb_id_table *callable_m_tbl; struct rb_id_table *cc_tbl; /* ID -> [[ci, cc1], cc2, ...] */ @@ -75,9 +75,9 @@ typedef struct rb_classext_struct rb_classext_t; #else # define RCLASS_EXT(c) (RCLASS(c)->ptr) #endif -#define RCLASS_IV_TBL(c) (RCLASS_EXT(c)->iv_tbl) #define RCLASS_CONST_TBL(c) (RCLASS_EXT(c)->const_tbl) #define RCLASS_M_TBL(c) (RCLASS(c)->m_tbl) +#define RCLASS_IVPTR(c) (RCLASS_EXT(c)->iv_ptr) #define RCLASS_CALLABLE_M_TBL(c) (RCLASS_EXT(c)->callable_m_tbl) #define RCLASS_CC_TBL(c) (RCLASS_EXT(c)->cc_tbl) #define RCLASS_CVC_TBL(c) (RCLASS_EXT(c)->cvc_tbl) diff --git a/internal/string.h b/internal/string.h index 43b716f9b25f4b..12edbff2b12c30 100644 --- a/internal/string.h +++ b/internal/string.h @@ -106,7 +106,7 @@ STR_EMBED_P(VALUE str) static inline bool STR_SHARED_P(VALUE str) { - return FL_ALL_RAW(str, STR_NOEMBED | ELTS_SHARED); + return FL_ALL_RAW(str, STR_NOEMBED | STR_SHARED); } static inline bool diff --git a/internal/variable.h b/internal/variable.h index bb24f5129ff6a3..734884a5f6baa1 100644 --- a/internal/variable.h +++ b/internal/variable.h @@ -24,7 +24,6 @@ void rb_gc_update_global_tbl(void); size_t rb_generic_ivar_memsize(VALUE); VALUE rb_search_class_path(VALUE); VALUE rb_attr_delete(VALUE, ID); -VALUE rb_ivar_lookup(VALUE obj, ID id, VALUE undef); void rb_autoload_str(VALUE mod, ID id, VALUE file); VALUE rb_autoload_at_p(VALUE, ID, int); NORETURN(VALUE rb_mod_const_missing(VALUE,VALUE)); @@ -49,6 +48,7 @@ void rb_iv_tbl_copy(VALUE dst, VALUE src); RUBY_SYMBOL_EXPORT_END MJIT_SYMBOL_EXPORT_BEGIN +VALUE rb_ivar_lookup(VALUE obj, ID id, VALUE undef); VALUE rb_gvar_get(ID); VALUE rb_gvar_set(ID, VALUE); VALUE rb_gvar_defined(ID); diff --git a/io.c b/io.c index a7da551a6a0a98..62b25c2ebbcaa6 100644 --- a/io.c +++ b/io.c @@ -5637,13 +5637,31 @@ rb_io_close(VALUE io) * call-seq: * close -> nil * - * Closes the stream, if it is open, after flushing any buffered writes - * to the operating system; does nothing if the stream is already closed. - * A stream is automatically closed when claimed by the garbage collector. + * Closes the stream for both reading and writing + * if open for either or both; returns +nil+. * - * If the stream was opened by IO.popen, #close sets global variable $?. + * If the stream is open for writing, flushes any buffered writes + * to the operating system before closing. * - * See also {Open and Closed Streams}[rdoc-ref:io_streams.rdoc@Open+and+Closed+Streams]. + * If the stream was opened by IO.popen, sets global variable $? + * (child exit status). + * + * Example: + * + * IO.popen('ruby', 'r+') do |pipe| + * puts pipe.closed? + * pipe.close + * puts $? + * puts pipe.closed? + * end + * + * Output: + * + * false + * pid 13760 exit 0 + * true + * + * Related: IO#close_read, IO#close_write, IO#closed?. */ static VALUE @@ -5694,17 +5712,23 @@ io_close(VALUE io) * Returns +true+ if the stream is closed for both reading and writing, * +false+ otherwise: * - * f = File.new('t.txt') - * f.close # => nil - * f.closed? # => true - * f = IO.popen('/bin/sh','r+') - * f.close_write # => nil - * f.closed? # => false - * f.close_read # => nil - * f.closed? # => true + * IO.popen('ruby', 'r+') do |pipe| + * puts pipe.closed? + * pipe.close_read + * puts pipe.closed? + * pipe.close_write + * puts pipe.closed? + * end * + * Output: + * + * false + * false + * true * * See also {Open and Closed Streams}[rdoc-ref:io_streams.rdoc@Open+and+Closed+Streams]. + * + * Related: IO#close_read, IO#close_write, IO#close. */ @@ -5731,17 +5755,33 @@ rb_io_closed(VALUE io) * call-seq: * close_read -> nil * - * Closes the read end of a duplexed stream (i.e., one that is both readable - * and writable, such as a pipe); does nothing if already closed: + * Closes the stream for reading if open for reading; + * returns +nil+. * - * f = IO.popen('/bin/sh','r+') - * f.close_read - * f.readlines # Raises IOError + * If the stream was opened by IO.popen and is also closed for writing, + * sets global variable $? (child exit status). * - * See also {Open and Closed Streams}[rdoc-ref:io_streams.rdoc@Open+and+Closed+Streams]. + * Example: + * + * IO.popen('ruby', 'r+') do |pipe| + * puts pipe.closed? + * pipe.close_write + * puts pipe.closed? + * pipe.close_read + * puts $? + * puts pipe.closed? + * end * - * Raises an exception if the stream is not duplexed. + * Output: + * + * false + * false + * pid 14748 exit 0 + * true + * + * See also {Open and Closed Streams}[rdoc-ref:io_streams.rdoc@Open+and+Closed+Streams]. * + * Related: IO#close, IO#close_write, IO#closed?. */ static VALUE @@ -5789,14 +5829,33 @@ rb_io_close_read(VALUE io) * call-seq: * close_write -> nil * - * Closes the write end of a duplexed stream (i.e., one that is both readable - * and writable, such as a pipe); does nothing if already closed: + * Closes the stream for writing if open for writing; + * returns +nil+: * - * f = IO.popen('/bin/sh', 'r+') - * f.close_write - * f.print 'nowhere' # Raises IOError. + * Flushes any buffered writes to the operating system before closing. + * + * If the stream was opened by IO.popen and is also closed for reading, + * sets global variable $? (child exit status). + * + * IO.popen('ruby', 'r+') do |pipe| + * puts pipe.closed? + * pipe.close_read + * puts pipe.closed? + * pipe.close_write + * puts $? + * puts pipe.closed? + * end + * + * Output: + * + * false + * false + * pid 15044 exit 0 + * true * * See also {Open and Closed Streams}[rdoc-ref:io_streams.rdoc@Open+and+Closed+Streams]. + * + * Related: IO#close, IO#close_read, IO#closed?. */ static VALUE diff --git a/io_buffer.c b/io_buffer.c index e252af35130371..cbf51250c4b22d 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -1101,6 +1101,27 @@ io_buffer_validate_range(struct rb_io_buffer *data, size_t offset, size_t length } } +static VALUE +rb_io_buffer_slice(struct rb_io_buffer *data, VALUE self, size_t offset, size_t length) +{ + io_buffer_validate_range(data, offset, length); + + VALUE instance = rb_io_buffer_type_allocate(rb_class_of(self)); + struct rb_io_buffer *slice = NULL; + TypedData_Get_Struct(instance, struct rb_io_buffer, &rb_io_buffer_type, slice); + + slice->base = (char*)data->base + offset; + slice->size = length; + + // The source should be the root buffer: + if (data->source != Qnil) + slice->source = data->source; + else + slice->source = self; + + return instance; +} + /* * call-seq: slice([offset = 0, [length]]) -> io_buffer * @@ -1157,27 +1178,6 @@ io_buffer_validate_range(struct rb_io_buffer *data, size_t offset, size_t length * string * # => tost */ -static VALUE -rb_io_buffer_slice(struct rb_io_buffer *data, VALUE self, size_t offset, size_t length) -{ - io_buffer_validate_range(data, offset, length); - - VALUE instance = rb_io_buffer_type_allocate(rb_class_of(self)); - struct rb_io_buffer *slice = NULL; - TypedData_Get_Struct(instance, struct rb_io_buffer, &rb_io_buffer_type, slice); - - slice->base = (char*)data->base + offset; - slice->size = length; - - // The source should be the root buffer: - if (data->source != Qnil) - slice->source = data->source; - else - slice->source = self; - - return instance; -} - static VALUE io_buffer_slice(int argc, VALUE *argv, VALUE self) { @@ -1462,7 +1462,7 @@ static void io_buffer_validate_type(size_t size, size_t offset) { if (offset > size) { - rb_raise(rb_eArgError, "Type extends beyond end of buffer! (offset=%ld > size=%ld)", offset, size); + rb_raise(rb_eArgError, "Type extends beyond end of buffer! (offset=%"PRIdSIZE" > size=%"PRIdSIZE")", offset, size); } } diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5 index 957ba19ff16a61..6fcffb9cc7bdd1 100644 --- a/lib/bundler/man/gemfile.5 +++ b/lib/bundler/man/gemfile.5 @@ -73,13 +73,13 @@ Credentials in the source URL will take precedence over credentials set using \f If your application requires a specific Ruby version or engine, specify your requirements using the \fBruby\fR method, with the following arguments\. All parameters are \fBOPTIONAL\fR unless otherwise specified\. . .SS "VERSION (required)" -The version of Ruby that your application requires\. If your application requires an alternate Ruby engine, such as JRuby, Rubinius or TruffleRuby, this should be the Ruby version that the engine is compatible with\. +The version of Ruby that your application requires\. If your application requires an alternate Ruby engine, such as JRuby, TruffleRuby, etc\., this should be the Ruby version that the engine is compatible with\. . .IP "" 4 . .nf -ruby "1\.9\.3" +ruby "3\.1\.2" . .fi . @@ -95,7 +95,7 @@ What exactly is an Engine? \- A Ruby engine is an implementation of the Ruby lan For background: the reference or original implementation of the Ruby programming language is called Matz\'s Ruby Interpreter \fIhttps://en\.wikipedia\.org/wiki/Ruby_MRI\fR, or MRI for short\. This is named after Ruby creator Yukihiro Matsumoto, also known as Matz\. MRI is also known as CRuby, because it is written in C\. MRI is the most widely used Ruby engine\. . .IP "\(bu" 4 -Other implementations \fIhttps://www\.ruby\-lang\.org/en/about/\fR of Ruby exist\. Some of the more well\-known implementations include Rubinius \fIhttps://rubinius\.com/\fR, and JRuby \fIhttp://jruby\.org/\fR\. Rubinius is an alternative implementation of Ruby written in Ruby\. JRuby is an implementation of Ruby on the JVM, short for Java Virtual Machine\. +Other implementations \fIhttps://www\.ruby\-lang\.org/en/about/\fR of Ruby exist\. Some of the more well\-known implementations include JRuby \fIhttp://jruby\.org/\fR and TruffleRuby \fIhttps://www\.graalvm\.org/ruby/\fR\. Rubinius is an alternative implementation of Ruby written in Ruby\. JRuby is an implementation of Ruby on the JVM, short for Java Virtual Machine\. TruffleRuby is a Ruby implementation on the GraalVM, a language toolkit built on the JVM\. . .IP "" 0 . @@ -106,20 +106,23 @@ Each application \fImay\fR specify a Ruby engine version\. If an engine version . .nf -ruby "1\.8\.7", engine: "jruby", engine_version: "1\.6\.7" +ruby "2\.6\.8", engine: "jruby", engine_version: "9\.3\.8\.0" . .fi . .IP "" 0 . .SS "PATCHLEVEL" -Each application \fImay\fR specify a Ruby patchlevel\. +Each application \fImay\fR specify a Ruby patchlevel\. Specifying the patchlevel has been meaningless since Ruby 2\.1\.0 was released as the patchlevel is now uniquely determined by a combination of major, minor, and teeny version numbers\. +. +.P +This option was implemented in Bundler 1\.4\.0 for Ruby 2\.0 or earlier\. . .IP "" 4 . .nf -ruby "2\.0\.0", patchlevel: "247" +ruby "3\.1\.2", patchlevel: "20" . .fi . @@ -265,6 +268,14 @@ C Ruby (MRI) only, but not Windows Windows C Ruby (MRI), including RubyInstaller 32\-bit and 64\-bit versions . .TP +\fBmswin\fR +Windows C Ruby (MRI), including RubyInstaller 32\-bit versions +. +.TP +\fBmswin64\fR +Windows C Ruby (MRI), including RubyInstaller 64\-bit versions +. +.TP \fBrbx\fR Rubinius . @@ -277,13 +288,13 @@ JRuby TruffleRuby . .P -On platforms \fBruby\fR, \fBmri\fR, and \fBwindows\fR, you may additionally specify a version by appending the major and minor version numbers without a delimiter\. For example, to specify that a gem should only be used on platform \fBruby\fR version 2\.3, use: +On platforms \fBruby\fR, \fBmri\fR, \fBmswin\fR, \fBmswin64\fR, and \fBwindows\fR, you may additionally specify a version by appending the major and minor version numbers without a delimiter\. For example, to specify that a gem should only be used on platform \fBruby\fR version 3\.1, use: . .IP "" 4 . .nf -ruby_23 +ruby_31 . .fi . @@ -297,8 +308,8 @@ As with groups (above), you may specify one or more platforms: .nf gem "weakling", platforms: :jruby -gem "ruby\-debug", platforms: :mri_18 -gem "nokogiri", platforms: [:windows_26, :jruby] +gem "ruby\-debug", platforms: :mri_31 +gem "nokogiri", platforms: [:windows_31, :jruby] . .fi . diff --git a/lib/bundler/man/gemfile.5.ronn b/lib/bundler/man/gemfile.5.ronn index d27849ae6aa34f..a3affc30ccfcfc 100644 --- a/lib/bundler/man/gemfile.5.ronn +++ b/lib/bundler/man/gemfile.5.ronn @@ -64,10 +64,10 @@ All parameters are `OPTIONAL` unless otherwise specified. ### VERSION (required) The version of Ruby that your application requires. If your application -requires an alternate Ruby engine, such as JRuby, Rubinius or TruffleRuby, this +requires an alternate Ruby engine, such as JRuby, TruffleRuby, etc., this should be the Ruby version that the engine is compatible with. - ruby "1.9.3" + ruby "3.1.2" ### ENGINE @@ -86,9 +86,10 @@ What exactly is an Engine? - [Other implementations](https://www.ruby-lang.org/en/about/) of Ruby exist. Some of the more well-known implementations include - [Rubinius](https://rubinius.com/), and [JRuby](http://jruby.org/). + [JRuby](http://jruby.org/) and [TruffleRuby](https://www.graalvm.org/ruby/). Rubinius is an alternative implementation of Ruby written in Ruby. JRuby is an implementation of Ruby on the JVM, short for Java Virtual Machine. + TruffleRuby is a Ruby implementation on the GraalVM, a language toolkit built on the JVM. ### ENGINE VERSION @@ -96,13 +97,17 @@ Each application _may_ specify a Ruby engine version. If an engine version is specified, an engine _must_ also be specified. If the engine is "ruby" the engine version specified _must_ match the Ruby version. - ruby "1.8.7", engine: "jruby", engine_version: "1.6.7" + ruby "2.6.8", engine: "jruby", engine_version: "9.3.8.0" ### PATCHLEVEL -Each application _may_ specify a Ruby patchlevel. +Each application _may_ specify a Ruby patchlevel. Specifying the patchlevel has +been meaningless since Ruby 2.1.0 was released as the patchlevel is now +uniquely determined by a combination of major, minor, and teeny version numbers. - ruby "2.0.0", patchlevel: "247" +This option was implemented in Bundler 1.4.0 for Ruby 2.0 or earlier. + + ruby "3.1.2", patchlevel: "20" ## GEMS @@ -195,6 +200,10 @@ There are a number of `Gemfile` platforms: C Ruby (MRI) only, but not Windows * `windows`: Windows C Ruby (MRI), including RubyInstaller 32-bit and 64-bit versions + * `mswin`: + Windows C Ruby (MRI), including RubyInstaller 32-bit versions + * `mswin64`: + Windows C Ruby (MRI), including RubyInstaller 64-bit versions * `rbx`: Rubinius * `jruby`: @@ -202,17 +211,18 @@ There are a number of `Gemfile` platforms: * `truffleruby`: TruffleRuby -On platforms `ruby`, `mri`, and `windows`, you may additionally specify a version -by appending the major and minor version numbers without a delimiter. For example, -to specify that a gem should only be used on platform `ruby` version 2.3, use: +On platforms `ruby`, `mri`, `mswin`, `mswin64`, and `windows`, you may +additionally specify a version by appending the major and minor version numbers +without a delimiter. For example, to specify that a gem should only be used on +platform `ruby` version 3.1, use: - ruby_23 + ruby_31 As with groups (above), you may specify one or more platforms: gem "weakling", platforms: :jruby - gem "ruby-debug", platforms: :mri_18 - gem "nokogiri", platforms: [:windows_26, :jruby] + gem "ruby-debug", platforms: :mri_31 + gem "nokogiri", platforms: [:windows_31, :jruby] All operations involving groups ([`bundle install`](bundle-install.1.html), `Bundler.setup`, `Bundler.require`) behave exactly the same as if any groups not diff --git a/lib/erb.gemspec b/lib/erb.gemspec index 43ffc89c691a25..419685c31890c2 100644 --- a/lib/erb.gemspec +++ b/lib/erb.gemspec @@ -27,5 +27,12 @@ Gem::Specification.new do |spec| spec.executables = ['erb'] spec.require_paths = ['lib'] - spec.add_dependency 'cgi' + if RUBY_ENGINE == 'jruby' + spec.platform = 'java' + else + spec.required_ruby_version = '>= 2.7.0' + spec.extensions = ['ext/erb/extconf.rb'] + end + + spec.add_dependency 'cgi', '>= 0.3.3' end diff --git a/lib/erb.rb b/lib/erb.rb index 0e42425a6056e6..48dcca71aa6490 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -986,7 +986,6 @@ def def_class(superklass=Object, methodname='result') class ERB # A utility module for conversion routines, often handy in HTML generation. module Util - public # # A utility method for escaping HTML tag characters in _s_. # @@ -999,8 +998,13 @@ module Util # # is a > 0 & a < 10? # - def html_escape(s) - CGI.escapeHTML(s.to_s) + begin + # ERB::Util.html_escape + require 'erb.so' + rescue LoadError + def html_escape(s) + CGI.escapeHTML(s.to_s) + end end alias h html_escape module_function :h @@ -1019,9 +1023,7 @@ def html_escape(s) # Programming%20Ruby%3A%20%20The%20Pragmatic%20Programmer%27s%20Guide # def url_encode(s) - s.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/n) { |m| - sprintf("%%%02X", m.unpack1("C")) - } + CGI.escapeURIComponent(s.to_s) end alias u url_encode module_function :u diff --git a/lib/erb/version.rb b/lib/erb/version.rb index 0aaa38258fcec0..572b29258320df 100644 --- a/lib/erb/version.rb +++ b/lib/erb/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class ERB - VERSION = '2.2.3' + VERSION = '3.0.0' private_constant :VERSION end diff --git a/lib/error_highlight/base.rb b/lib/error_highlight/base.rb index 4c115cc8285c6b..062871ee165a8a 100644 --- a/lib/error_highlight/base.rb +++ b/lib/error_highlight/base.rb @@ -59,8 +59,7 @@ def self.spot(obj, **opts) Spotter.new(node, **opts).spot when RubyVM::AbstractSyntaxTree::Node - # Just for compatibility - Spotter.new(node, **opts).spot + Spotter.new(obj, **opts).spot else raise TypeError, "Exception is expected" diff --git a/lib/irb.rb b/lib/irb.rb index 749f3ee167efc0..57ec9ebaebc9d9 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -426,6 +426,9 @@ class Irb def initialize(workspace = nil, input_method = nil) @context = Context.new(self, workspace, input_method) @context.main.extend ExtendCommandBundle + @context.command_aliases.each do |alias_name, cmd_name| + @context.main.install_alias_method(alias_name, cmd_name) + end @signal_status = :IN_IRB @scanner = RubyLex.new end diff --git a/lib/irb/context.rb b/lib/irb/context.rb index d238da9350b0ef..d1ae2cb605b936 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -149,6 +149,8 @@ def initialize(irb, workspace = nil, input_method = nil) if @newline_before_multiline_output.nil? @newline_before_multiline_output = true end + + @command_aliases = IRB.conf[:COMMAND_ALIASES] end # The top-level workspace, see WorkSpace#main @@ -326,6 +328,9 @@ def main # See IRB@Command+line+options for more command line options. attr_accessor :back_trace_limit + # User-defined IRB command aliases + attr_accessor :command_aliases + # Alias for #use_multiline alias use_multiline? use_multiline # Alias for #use_singleline @@ -477,6 +482,13 @@ def evaluate(line, line_no, exception: nil) # :nodoc: line = "begin ::Kernel.raise _; rescue _.class\n#{line}\n""end" @workspace.local_variable_set(:_, exception) end + + # Transform a non-identifier alias (ex: @, $) + command = line.split(/\s/, 2).first + if original = symbol_alias(command) + line = line.gsub(/\A#{Regexp.escape(command)}/, original.to_s) + end + set_last_value(@workspace.evaluate(self, line, irb_path, line_no)) end @@ -522,5 +534,11 @@ def inspect # :nodoc: def local_variables # :nodoc: workspace.binding.local_variables end + + # Return a command name if it's aliased from the argument and it's not an identifier. + def symbol_alias(command) + return nil if command.match?(/\A\w+\z/) + command_aliases[command.to_sym] + end end end diff --git a/lib/irb/init.rb b/lib/irb/init.rb index d9c4353f39ced4..09099f88b77346 100644 --- a/lib/irb/init.rb +++ b/lib/irb/init.rb @@ -44,7 +44,7 @@ def IRB.init_config(ap_path) @CONF[:IRB_RC] = nil @CONF[:USE_SINGLELINE] = false unless defined?(ReadlineInputMethod) - @CONF[:USE_COLORIZE] = !ENV['NO_COLOR'] + @CONF[:USE_COLORIZE] = (nc = ENV['NO_COLOR']).nil? || nc.empty? @CONF[:USE_AUTOCOMPLETE] = true @CONF[:INSPECT_MODE] = true @CONF[:USE_TRACER] = false @@ -158,6 +158,8 @@ def IRB.init_config(ap_path) @CONF[:LC_MESSAGES] = Locale.new @CONF[:AT_EXIT] = [] + + @CONF[:COMMAND_ALIASES] = {} end def IRB.set_measure_callback(type = nil, arg = nil, &block) @@ -379,11 +381,9 @@ def IRB.rc_file_generators end if xdg_config_home = ENV["XDG_CONFIG_HOME"] irb_home = File.join(xdg_config_home, "irb") - unless File.exist? irb_home - require 'fileutils' - FileUtils.mkdir_p irb_home + if File.directory?(irb_home) + yield proc{|rc| irb_home + "/irb#{rc}"} end - yield proc{|rc| irb_home + "/irb#{rc}"} end if home = ENV["HOME"] yield proc{|rc| home+"/.irb#{rc}"} diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb index 54ea2a9e7b390a..28029bbf4ce6c3 100644 --- a/lib/irb/ruby-lex.rb +++ b/lib/irb/ruby-lex.rb @@ -48,7 +48,7 @@ def self.compile_with_errors_suppressed(code, line_no: 1) end # io functions - def set_input(io, p = nil, context: nil, &block) + def set_input(io, p = nil, context:, &block) @io = io if @io.respond_to?(:check_termination) @io.check_termination do |code| @@ -65,6 +65,12 @@ def set_input(io, p = nil, context: nil, &block) false end else + # Accept any single-line input starting with a non-identifier alias (ex: @, $) + command = code.split(/\s/, 2).first + if context.symbol_alias(command) + next true + end + code.gsub!(/\s*\z/, '').concat("\n") ltype, indent, continue, code_block_open = check_state(code, context: context) if ltype or indent > 0 or continue or code_block_open @@ -216,7 +222,7 @@ def check_state(code, tokens = nil, context: nil) ltype = process_literal_type(tokens) indent = process_nesting_level(tokens) continue = process_continue(tokens) - lvars_code = self.class.generate_local_variables_assign_code(context&.local_variables || []) + lvars_code = self.class.generate_local_variables_assign_code(context.local_variables) code = "#{lvars_code}\n#{code}" if lvars_code code_block_open = check_code_block(code, tokens) [ltype, indent, continue, code_block_open] diff --git a/lib/optparse.rb b/lib/optparse.rb index 9dad2e4edaaf8c..238fa1e2195256 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -1903,10 +1903,13 @@ def candidate(word) # directory ~/.options, then the basename with '.options' suffix # under XDG and Haiku standard places. # - def load(filename = nil) + # The optional +into+ keyword argument works exactly like that accepted in + # method #parse. + # + def load(filename = nil, into: nil) unless filename basename = File.basename($0, '.*') - return true if load(File.expand_path(basename, '~/.options')) rescue nil + return true if load(File.expand_path(basename, '~/.options'), into: into) rescue nil basename << ".options" return [ # XDG @@ -1918,11 +1921,11 @@ def load(filename = nil) '~/config/settings', ].any? {|dir| next if !dir or dir.empty? - load(File.expand_path(basename, dir)) rescue nil + load(File.expand_path(basename, dir), into: into) rescue nil } end begin - parse(*IO.readlines(filename).each {|s| s.chomp!}) + parse(*IO.readlines(filename).each {|s| s.chomp!}, into: into) true rescue Errno::ENOENT, Errno::ENOTDIR false diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb index d24f8d5eff7ead..819cff8aa3dd04 100644 --- a/lib/rdoc/ri/driver.rb +++ b/lib/rdoc/ri/driver.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true require 'optparse' -begin - require 'readline' -rescue LoadError -end - require_relative '../../rdoc' require_relative 'formatter' # For RubyGems backwards compatibility @@ -1079,6 +1074,10 @@ def formatter(io) def interactive puts "\nEnter the method name you want to look up." + begin + require 'readline' + rescue LoadError + end if defined? Readline then Readline.completion_proc = method :complete puts "You can use tab to autocomplete." diff --git a/lib/tmpdir.gemspec b/lib/tmpdir.gemspec index 7b76403002be55..8e471bf1071ca5 100644 --- a/lib/tmpdir.gemspec +++ b/lib/tmpdir.gemspec @@ -8,7 +8,7 @@ Gem::Specification.new do |spec| spec.description = %q{Extends the Dir class to manage the OS temporary file path.} spec.homepage = "https://github.com/ruby/tmpdir" spec.licenses = ["Ruby", "BSD-2-Clause"] - spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") + spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0") spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage diff --git a/lib/tmpdir.rb b/lib/tmpdir.rb index 3b67164039216e..55920a4a743ebc 100644 --- a/lib/tmpdir.rb +++ b/lib/tmpdir.rb @@ -19,9 +19,10 @@ class Dir # Returns the operating system's temporary file path. def self.tmpdir - tmp = nil - ['TMPDIR', 'TMP', 'TEMP', ['system temporary path', @@systmpdir], ['/tmp']*2, ['.']*2].each do |name, dir = ENV[name]| - next if !dir + ['TMPDIR', 'TMP', 'TEMP', ['system temporary path', @@systmpdir], ['/tmp']*2, ['.']*2].find do |name, dir| + unless dir + next if !(dir = ENV[name]) or dir.empty? + end dir = File.expand_path(dir) stat = File.stat(dir) rescue next case @@ -32,12 +33,9 @@ def self.tmpdir when stat.world_writable? && !stat.sticky? warn "#{name} is world-writable: #{dir}" else - tmp = dir - break + break dir end - end - raise ArgumentError, "could not find a temporary directory" unless tmp - tmp + end or raise ArgumentError, "could not find a temporary directory" end # Dir.mktmpdir creates a temporary directory. @@ -108,6 +106,7 @@ def self.mktmpdir(prefix_suffix=nil, *rest, **options) end end + # Temporary name generator module Tmpname # :nodoc: module_function @@ -115,16 +114,23 @@ def tmpdir Dir.tmpdir end + # Unusable characters as path name UNUSABLE_CHARS = "^,-.0-9A-Z_a-z~" - class << (RANDOM = Random.new) + # Dedicated random number generator + RANDOM = Random.new + class << RANDOM # :nodoc: + # Maximum random number MAX = 36**6 # < 0x100000000 + + # Returns new random string upto 6 bytes def next rand(MAX).to_s(36) end end private_constant :RANDOM + # Generates and yields random names to create a temporary name def create(basename, tmpdir=nil, max_try: nil, **opts) origdir = tmpdir tmpdir ||= tmpdir() diff --git a/marshal.c b/marshal.c index 59edbfe53a2a88..d956260d96f0a3 100644 --- a/marshal.c +++ b/marshal.c @@ -523,7 +523,7 @@ hash_each(VALUE key, VALUE value, VALUE v) #define SINGLETON_DUMP_UNABLE_P(klass) \ (rb_id_table_size(RCLASS_M_TBL(klass)) > 0 || \ - (RCLASS_IV_TBL(klass) && RCLASS_IV_TBL(klass)->num_entries > 1)) + rb_ivar_count(klass) > 1) static void w_extended(VALUE klass, struct dump_arg *arg, int check) diff --git a/misc/lldb_rb/rb_base_command.py b/misc/lldb_rb/rb_base_command.py index bf98b6761255bc..de90a1617c2602 100644 --- a/misc/lldb_rb/rb_base_command.py +++ b/misc/lldb_rb/rb_base_command.py @@ -19,8 +19,8 @@ def lldb_init(cls, debugger): imemo_types = target.FindFirstType("enum imemo_type") - for member in imemo_types.GetEnumMembers(): - g[member.GetName()] = member.GetValueAsUnsigned() + #for member in imemo_types.GetEnumMembers(): + # g[member.GetName()] = member.GetValueAsUnsigned() for enum in target.FindFirstGlobalVariable("ruby_dummy_gdb_enums"): enum = enum.GetType() diff --git a/numeric.rb b/numeric.rb index c2091465f88fd5..f02667921063c6 100644 --- a/numeric.rb +++ b/numeric.rb @@ -6,7 +6,17 @@ class Numeric # Returns +true+ if +num+ is a real number (i.e. not Complex). # def real? - return true + true + end + + # + # call-seq: + # num.real -> self + # + # Returns self. + # + def real + self end # @@ -19,7 +29,7 @@ def real? # 1.integer? #=> true # def integer? - return false + false end # @@ -29,7 +39,7 @@ def integer? # Returns +true+ if +num+ is a finite number, otherwise returns +false+. # def finite? - return true + true end # @@ -40,8 +50,34 @@ def finite? # finite, -Infinity, or +Infinity. # def infinite? - return nil + nil + end + + # + # call-seq: + # num.imag -> 0 + # num.imaginary -> 0 + # + # Returns zero. + # + def imaginary + 0 + end + + alias imag imaginary + + # + # call-seq: + # num.conj -> self + # num.conjugate -> self + # + # Returns self. + # + def conjugate + self end + + alias conj conjugate end class Integer @@ -146,7 +182,7 @@ def even? # # Since +int+ is already an Integer, this always returns +true+. def integer? - return true + true end alias magnitude abs @@ -178,7 +214,7 @@ def odd? # # For example, ?a.ord returns 97 both in 1.8 and 1.9. def ord - return self + self end # @@ -208,7 +244,7 @@ def size # # #to_int is an alias for #to_i. def to_i - return self + self end # call-seq: @@ -216,7 +252,7 @@ def to_i # # Since +int+ is already an Integer, returns +self+. def to_int - return self + self end # call-seq: @@ -244,6 +280,26 @@ def zero? def ceildiv(other) -div(-other) end + + # + # call-seq: + # int.numerator -> self + # + # Returns self. + # + def numerator + self + end + + # + # call-seq: + # int.denominator -> 1 + # + # Returns 1. + # + def denominator + 1 + end end # call-seq: @@ -276,7 +332,7 @@ class Float # Since +float+ is already a Float, returns +self+. # def to_f - return self + self end # diff --git a/object.c b/object.c index d14bed1fe0d053..307ee866c75b08 100644 --- a/object.c +++ b/object.c @@ -308,20 +308,22 @@ init_copy(VALUE dest, VALUE obj) rb_copy_generic_ivar(dest, obj); rb_gc_copy_finalizer(dest, obj); - rb_shape_t *shape_to_set = rb_shape_get_shape(obj); - - // If the object is frozen, the "dup"'d object will *not* be frozen, - // so we need to copy the frozen shape's parent to the new object. - if (rb_shape_frozen_shape_p(shape_to_set)) { - shape_to_set = rb_shape_get_shape_by_id(shape_to_set->parent_id); - } - if (RB_TYPE_P(obj, T_OBJECT)) { rb_obj_copy_ivar(dest, obj); } - // shape ids are different - rb_shape_set_shape(dest, shape_to_set); + if (!RB_TYPE_P(obj, T_CLASS) && !RB_TYPE_P(obj, T_MODULE)) { + rb_shape_t *shape_to_set = rb_shape_get_shape(obj); + + // If the object is frozen, the "dup"'d object will *not* be frozen, + // so we need to copy the frozen shape's parent to the new object. + if (rb_shape_frozen_shape_p(shape_to_set)) { + shape_to_set = rb_shape_get_shape_by_id(shape_to_set->parent_id); + } + + // shape ids are different + rb_shape_set_shape(dest, shape_to_set); + } } static VALUE immutable_obj_clone(VALUE obj, VALUE kwfreeze); diff --git a/proc.c b/proc.c index 7c76f3647783a0..a25786c0157251 100644 --- a/proc.c +++ b/proc.c @@ -2571,8 +2571,7 @@ convert_umethod_to_method_components(const struct METHOD *data, VALUE recv, VALU VALUE refined_class = rb_refinement_module_get_refined_class(methclass); if (!NIL_P(refined_class)) methclass = refined_class; } - if (!RB_TYPE_P(methclass, T_MODULE) && - methclass != CLASS_OF(recv) && !rb_obj_is_kind_of(recv, methclass)) { + if (!RB_TYPE_P(methclass, T_MODULE) && !RTEST(rb_obj_is_kind_of(recv, methclass))) { if (FL_TEST(methclass, FL_SINGLETON)) { rb_raise(rb_eTypeError, "singleton method called for a different object"); diff --git a/ractor.c b/ractor.c index 7fea312ab0e31b..3569202e58ee27 100644 --- a/ractor.c +++ b/ractor.c @@ -1438,12 +1438,6 @@ cancel_single_ractor_mode(void) } ruby_single_main_ractor = NULL; - - if (rb_warning_category_enabled_p(RB_WARN_CATEGORY_EXPERIMENTAL)) { - rb_category_warn(RB_WARN_CATEGORY_EXPERIMENTAL, - "Ractor is experimental, and the behavior may change in future versions of Ruby! " - "Also there are many implementation issues."); - } } static void diff --git a/ractor.rb b/ractor.rb index 8e229d47005521..1031fe499b9704 100644 --- a/ractor.rb +++ b/ractor.rb @@ -262,6 +262,10 @@ class Ractor def self.new(*args, name: nil, &block) b = block # TODO: builtin bug raise ArgumentError, "must be called with a block" unless block + if __builtin_cexpr!("RBOOL(ruby_single_main_ractor)") + warn("Ractor is experimental, and the behavior may change in future versions of Ruby! " \ + "Also there are many implementation issues.", uplevel: 0, category: :experimental) + end loc = caller_locations(1, 1).first loc = "#{loc.path}:#{loc.lineno}" __builtin_ractor_create(loc, name, args, b) diff --git a/rational.c b/rational.c index e537bd498b072e..48a9ab2ed28932 100644 --- a/rational.c +++ b/rational.c @@ -2059,30 +2059,6 @@ rb_rational_canonicalize(VALUE x) return x; } -/* - * call-seq: - * int.numerator -> self - * - * Returns self. - */ -static VALUE -integer_numerator(VALUE self) -{ - return self; -} - -/* - * call-seq: - * int.denominator -> 1 - * - * Returns 1. - */ -static VALUE -integer_denominator(VALUE self) -{ - return INT2FIX(1); -} - /* * call-seq: * flo.numerator -> integer @@ -2832,9 +2808,6 @@ Init_Rational(void) rb_define_method(rb_cNumeric, "denominator", numeric_denominator, 0); rb_define_method(rb_cNumeric, "quo", rb_numeric_quo, 1); - rb_define_method(rb_cInteger, "numerator", integer_numerator, 0); - rb_define_method(rb_cInteger, "denominator", integer_denominator, 0); - rb_define_method(rb_cFloat, "numerator", rb_float_numerator, 0); rb_define_method(rb_cFloat, "denominator", rb_float_denominator, 0); diff --git a/regenc.c b/regenc.c index 16d62fdf4098c4..fc131d2533cb6a 100644 --- a/regenc.c +++ b/regenc.c @@ -51,6 +51,21 @@ onigenc_set_default_encoding(OnigEncoding enc) return 0; } +extern int +onigenc_mbclen(const OnigUChar* p,const OnigUChar* e, OnigEncoding enc) +{ + int ret = ONIGENC_PRECISE_MBC_ENC_LEN(enc, p, e); + if (ONIGENC_MBCLEN_CHARFOUND_P(ret)) { + ret = ONIGENC_MBCLEN_CHARFOUND_LEN(ret); + if (ret > (int)(e - p)) ret = (int)(e - p); // just for case + return ret; + } + else if (ONIGENC_MBCLEN_NEEDMORE_P(ret)) { + return (int)(e - p); + } + return p < e ? 1 : 0; +} + extern int onigenc_mbclen_approximate(const OnigUChar* p,const OnigUChar* e, OnigEncoding enc) { diff --git a/sample/trick2018/02-mame/entry.rb b/sample/trick2018/02-mame/entry.rb index cc4ef9cbc4e306..ced791aa3d1414 100644 --- a/sample/trick2018/02-mame/entry.rb +++ b/sample/trick2018/02-mame/entry.rb @@ -1,11 +1,11 @@ '';eval(r=%q(->z{r="'';eval(r=\ -%q(#{r}))[%q`#{z}`]";i=-040;30. +%q(#{r}))[%q`#{z}`]";i=-040;31. times{|n|(15+n%2*15-n/2).times{ r<["t]];};o[1,?\n*8];ex"-}eac 1Hl<1[-1]*2*t=n%2];o[14-n,0)mvk 8M$<4,?\n];15.times{|n|;o[35ie2 diff --git a/shape.c b/shape.c index 1f08b9fa22fa49..1de89d3f8f2b42 100644 --- a/shape.c +++ b/shape.c @@ -54,9 +54,10 @@ rb_shape_get_shape_by_id_without_assertion(shape_id_t shape_id) } #if !SHAPE_IN_BASIC_FLAGS -static inline shape_id_t -RCLASS_SHAPE_ID(VALUE obj) +shape_id_t +rb_rclass_shape_id(VALUE obj) { + RUBY_ASSERT(RB_TYPE_P(obj, T_CLASS) || RB_TYPE_P(obj, T_MODULE)); return RCLASS_EXT(obj)->shape_id; } @@ -115,7 +116,9 @@ static rb_shape_t* get_next_shape_internal(rb_shape_t* shape, ID id, VALUE obj, enum shape_type shape_type) { rb_shape_t *res = NULL; - RUBY_ASSERT(SHAPE_FROZEN != (enum shape_type)shape->type); + + RUBY_ASSERT(SHAPE_FROZEN != (enum shape_type)shape->type || RB_TYPE_P(obj, T_MODULE) || RB_TYPE_P(obj, T_CLASS)); + RB_VM_LOCK_ENTER(); { if (rb_shape_lookup_id(shape, id, shape_type)) { diff --git a/shape.h b/shape.h index 1470cdd4aa4af1..8e1bf46ec9cd53 100644 --- a/shape.h +++ b/shape.h @@ -90,6 +90,13 @@ ROBJECT_SET_SHAPE_ID(VALUE obj, shape_id_t shape_id) RBASIC_SET_SHAPE_ID(obj, shape_id); } +static inline shape_id_t +RCLASS_SHAPE_ID(VALUE obj) +{ + RUBY_ASSERT(RB_TYPE_P(obj, T_CLASS) || RB_TYPE_P(obj, T_MODULE)); + return RBASIC_SHAPE_ID(obj); +} + #else static inline shape_id_t @@ -105,6 +112,15 @@ ROBJECT_SET_SHAPE_ID(VALUE obj, shape_id_t shape_id) RBASIC(obj)->flags &= SHAPE_FLAG_MASK; RBASIC(obj)->flags |= ((VALUE)(shape_id) << SHAPE_FLAG_SHIFT); } + +MJIT_SYMBOL_EXPORT_BEGIN +shape_id_t rb_rclass_shape_id(VALUE obj); +MJIT_SYMBOL_EXPORT_END + +static inline shape_id_t RCLASS_SHAPE_ID(VALUE obj) { + return rb_rclass_shape_id(obj); +} + #endif bool rb_shape_root_shape_p(rb_shape_t* shape); @@ -134,6 +150,14 @@ ROBJECT_IV_COUNT(VALUE obj) return ivc; } +static inline uint32_t +RCLASS_IV_COUNT(VALUE obj) +{ + RUBY_ASSERT(RB_TYPE_P(obj, RUBY_T_CLASS) || RB_TYPE_P(obj, RUBY_T_MODULE)); + uint32_t ivc = rb_shape_get_shape_by_id(RCLASS_SHAPE_ID(obj))->next_iv_index; + return ivc; +} + rb_shape_t * rb_shape_alloc(ID edge_name, rb_shape_t * parent); rb_shape_t * rb_shape_alloc_with_parent_id(ID edge_name, shape_id_t parent_id); diff --git a/string.c b/string.c index 64177989018e94..4894cde53ce960 100644 --- a/string.c +++ b/string.c @@ -881,30 +881,29 @@ must_not_null(const char *ptr) } } -static inline VALUE -str_alloc(VALUE klass, size_t size) -{ - assert(size > 0); - RVARGC_NEWOBJ_OF(str, struct RString, klass, - T_STRING | (RGENGC_WB_PROTECTED_STRING ? FL_WB_PROTECTED : 0), size); - return (VALUE)str; -} - static inline VALUE str_alloc_embed(VALUE klass, size_t capa) { size_t size = rb_str_embed_size(capa); + assert(size > 0); assert(rb_gc_size_allocatable_p(size)); #if !USE_RVARGC assert(size <= sizeof(struct RString)); #endif - return str_alloc(klass, size); + + RVARGC_NEWOBJ_OF(str, struct RString, klass, + T_STRING | (RGENGC_WB_PROTECTED_STRING ? FL_WB_PROTECTED : 0), size); + + return (VALUE)str; } static inline VALUE str_alloc_heap(VALUE klass) { - return str_alloc(klass, sizeof(struct RString)); + RVARGC_NEWOBJ_OF(str, struct RString, klass, + T_STRING | STR_NOEMBED | (RGENGC_WB_PROTECTED_STRING ? FL_WB_PROTECTED : 0), sizeof(struct RString)); + + return (VALUE)str; } static inline VALUE @@ -941,7 +940,6 @@ str_new0(VALUE klass, const char *ptr, long len, int termlen) * mul_add_mul can be reverted to a simple ALLOC_N. */ RSTRING(str)->as.heap.ptr = rb_xmalloc_mul_add_mul(sizeof(char), len, sizeof(char), termlen); - STR_SET_NOEMBED(str); } if (ptr) { memcpy(RSTRING_PTR(str), ptr, len); @@ -1048,7 +1046,6 @@ str_new_static(VALUE klass, const char *ptr, long len, int encindex) RSTRING(str)->as.heap.len = len; RSTRING(str)->as.heap.ptr = (char *)ptr; RSTRING(str)->as.heap.aux.capa = len; - STR_SET_NOEMBED(str); RBASIC(str)->flags |= STR_NOFREE; } rb_enc_associate_index(str, encindex); @@ -1445,7 +1442,6 @@ heap_str_make_shared(VALUE klass, VALUE orig) assert(!STR_SHARED_P(orig)); VALUE str = str_alloc_heap(klass); - STR_SET_NOEMBED(str); RSTRING(str)->as.heap.len = RSTRING_LEN(orig); RSTRING(str)->as.heap.ptr = RSTRING_PTR(orig); RSTRING(str)->as.heap.aux.capa = RSTRING(orig)->as.heap.aux.capa; @@ -1546,7 +1542,6 @@ rb_str_buf_new(long capa) capa = STR_BUF_MIN_SIZE; } #endif - FL_SET(str, STR_NOEMBED); RSTRING(str)->as.heap.aux.capa = capa; RSTRING(str)->as.heap.ptr = ALLOC_N(char, (size_t)capa + 1); RSTRING(str)->as.heap.ptr[0] = '\0'; @@ -1725,30 +1720,29 @@ str_replace(VALUE str, VALUE str2) return str; } -static inline VALUE -ec_str_alloc(struct rb_execution_context_struct *ec, VALUE klass, size_t size) -{ - assert(size > 0); - RB_RVARGC_EC_NEWOBJ_OF(ec, str, struct RString, klass, - T_STRING | (RGENGC_WB_PROTECTED_STRING ? FL_WB_PROTECTED : 0), size); - return (VALUE)str; -} - static inline VALUE ec_str_alloc_embed(struct rb_execution_context_struct *ec, VALUE klass, size_t capa) { size_t size = rb_str_embed_size(capa); + assert(size > 0); assert(rb_gc_size_allocatable_p(size)); #if !USE_RVARGC assert(size <= sizeof(struct RString)); #endif - return ec_str_alloc(ec, klass, size); + + RB_RVARGC_EC_NEWOBJ_OF(ec, str, struct RString, klass, + T_STRING | (RGENGC_WB_PROTECTED_STRING ? FL_WB_PROTECTED : 0), size); + + return (VALUE)str; } static inline VALUE ec_str_alloc_heap(struct rb_execution_context_struct *ec, VALUE klass) { - return ec_str_alloc(ec, klass, sizeof(struct RString)); + RB_RVARGC_EC_NEWOBJ_OF(ec, str, struct RString, klass, + T_STRING | STR_NOEMBED | (RGENGC_WB_PROTECTED_STRING ? FL_WB_PROTECTED : 0), sizeof(struct RString)); + + return (VALUE)str; } static inline VALUE @@ -1766,6 +1760,7 @@ str_duplicate_setup(VALUE klass, VALUE str, VALUE dup) if (STR_EMBED_P(str)) { long len = RSTRING_EMBED_LEN(str); + assert(STR_EMBED_P(dup)); assert(str_embed_capa(dup) >= len + 1); STR_SET_EMBED_LEN(dup, len); MEMCPY(RSTRING(dup)->as.embed.ary, RSTRING(str)->as.embed.ary, char, len + 1); @@ -1786,6 +1781,7 @@ str_duplicate_setup(VALUE klass, VALUE str, VALUE dup) else if (STR_EMBED_P(root)) { MEMCPY(RSTRING(dup)->as.embed.ary, RSTRING(root)->as.embed.ary, char, RSTRING_EMBED_LEN_MAX + 1); + FL_UNSET(dup, STR_NOEMBED); } #endif else { @@ -1809,7 +1805,7 @@ static inline VALUE ec_str_duplicate(struct rb_execution_context_struct *ec, VALUE klass, VALUE str) { VALUE dup; - if (!USE_RVARGC || FL_TEST(str, STR_NOEMBED)) { + if (FL_TEST(str, STR_NOEMBED)) { dup = ec_str_alloc_heap(ec, klass); } else { @@ -1823,7 +1819,7 @@ static inline VALUE str_duplicate(VALUE klass, VALUE str) { VALUE dup; - if (!USE_RVARGC || FL_TEST(str, STR_NOEMBED)) { + if (FL_TEST(str, STR_NOEMBED)) { dup = str_alloc_heap(klass); } else { @@ -2311,7 +2307,6 @@ rb_str_times(VALUE str, VALUE times) str2 = str_alloc_heap(rb_cString); RSTRING(str2)->as.heap.aux.capa = len; RSTRING(str2)->as.heap.ptr = ZALLOC_N(char, (size_t)len + 1); - STR_SET_NOEMBED(str2); } STR_SET_LEN(str2, len); rb_enc_copy(str2, str); diff --git a/template/Makefile.in b/template/Makefile.in index 55a4f931040a2c..d67729cdf23781 100644 --- a/template/Makefile.in +++ b/template/Makefile.in @@ -381,7 +381,7 @@ install-cross: $(arch)-fake.rb $(RBCONFIG) rbconfig.rb $(arch_hdrdir)/ruby/confi Makefile: $(srcdir)/template/Makefile.in $(srcdir)/enc/Makefile.in -$(MKFILES): config.status $(srcdir)/version.h $(srcdir)/revision.h $(ABI_VERSION_HDR) +$(MKFILES): config.status $(srcdir)/version.h $(REVISION_H) $(ABI_VERSION_HDR) @[ -f $@ ] && mv $@ $@.old MAKE=$(MAKE) $(SHELL) ./config.status $@ @cmp $@ $@.old > /dev/null 2>&1 && echo $@ unchanged && exit 0; \ diff --git a/template/prelude.c.tmpl b/template/prelude.c.tmpl index 58453636bf15ce..428c9f4d014ebb 100644 --- a/template/prelude.c.tmpl +++ b/template/prelude.c.tmpl @@ -193,7 +193,7 @@ prelude_eval(VALUE code, VALUE name, int line) }; rb_ast_t *ast = prelude_ast(name, code, line); - rb_iseq_eval(rb_iseq_new_with_opt(&ast->body, name, name, Qnil, INT2FIX(line), + rb_iseq_eval(rb_iseq_new_with_opt(&ast->body, name, name, Qnil, line, NULL, 0, ISEQ_TYPE_TOP, &optimization)); rb_ast_dispose(ast); } diff --git a/test/erb/test_erb.rb b/test/erb/test_erb.rb index fb5e9b611ec150..1db0e55f8ae022 100644 --- a/test/erb/test_erb.rb +++ b/test/erb/test_erb.rb @@ -73,12 +73,28 @@ def test_html_escape assert_equal("", ERB::Util.html_escape("")) assert_equal("abc", ERB::Util.html_escape("abc")) assert_equal("<<", ERB::Util.html_escape("<\<")) + assert_equal("'&"><", ERB::Util.html_escape("'&\"><")) assert_equal("", ERB::Util.html_escape(nil)) assert_equal("123", ERB::Util.html_escape(123)) end + def test_html_escape_to_s + object = Object.new + def object.to_s + "object" + end + assert_equal("object", ERB::Util.html_escape(object)) + end + + def test_html_escape_extension + assert_nil(ERB::Util.method(:html_escape).source_location) + end if RUBY_ENGINE == 'ruby' + def test_concurrent_default_binding + # This test randomly fails with JRuby -- NameError: undefined local variable or method `template2' + pend if RUBY_ENGINE == 'jruby' + template1 = 'one <%= ERB.new(template2).result %>' eval 'template2 = "two"', TOPLEVEL_BINDING @@ -236,6 +252,8 @@ def test_explicit_trim_line_with_carriage_return end def test_invalid_trim_mode + pend if RUBY_ENGINE == 'truffleruby' + assert_warning(/#{__FILE__}:#{__LINE__ + 1}/) do @erb.new("", trim_mode: 'abc-def') end diff --git a/test/erb/test_erb_command.rb b/test/erb/test_erb_command.rb index 0baa59ddd59ed9..446efde227934b 100644 --- a/test/erb/test_erb_command.rb +++ b/test/erb/test_erb_command.rb @@ -4,6 +4,7 @@ class TestErbCommand < Test::Unit::TestCase def test_var + pend if RUBY_ENGINE == 'truffleruby' assert_in_out_err(["-I#{File.expand_path('../../lib', __dir__)}", "-w", File.expand_path("../../libexec/erb", __dir__), "var=hoge"], @@ -11,6 +12,7 @@ def test_var end def test_template_file_encoding + pend if RUBY_ENGINE == 'truffleruby' assert_in_out_err(["-I#{File.expand_path('../../lib', __dir__)}", "-w", File.expand_path("../../libexec/erb", __dir__)], "<%=''.encoding.to_s%>", ["UTF-8"]) @@ -18,6 +20,7 @@ def test_template_file_encoding # These interfaces will be removed at Ruby 2.7. def test_deprecated_option + pend if RUBY_ENGINE == 'truffleruby' warnings = [ "warning: -S option of erb command is deprecated. Please do not use this.", /\n.+\/libexec\/erb:\d+: warning: Passing safe_level with the 2nd argument of ERB\.new is deprecated\. Do not use it, and specify other arguments as keyword arguments\.\n/, diff --git a/test/error_highlight/test_error_highlight.rb b/test/error_highlight/test_error_highlight.rb index c4a998092b5686..d5f57a88bc8805 100644 --- a/test/error_highlight/test_error_highlight.rb +++ b/test/error_highlight/test_error_highlight.rb @@ -1257,4 +1257,36 @@ def test_spot_with_backtrace_location assert_equal(22, spot[:last_column]) assert_equal(" raise_name_error\n", spot[:snippet]) end + + def test_spot_with_node + omit unless RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location) + + begin + raise_name_error + rescue NameError => exc + end + + bl = exc.backtrace_locations.first + expected_spot = ErrorHighlight.spot(exc, backtrace_location: bl) + ast = RubyVM::AbstractSyntaxTree.parse_file(__FILE__, keep_script_lines: true) + node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(bl) + node = find_node_by_id(ast, node_id) + actual_spot = ErrorHighlight.spot(node) + + assert_equal expected_spot, actual_spot + end + + private + + def find_node_by_id(node, node_id) + return node if node.node_id == node_id + + node.children.each do |child| + next unless child.is_a?(RubyVM::AbstractSyntaxTree::Node) + found = find_node_by_id(child, node_id) + return found if found + end + + return false + end end diff --git a/test/fiber/test_address_resolve.rb b/test/fiber/test_address_resolve.rb index 457221b9b1ef2c..12223bcc06fa8b 100644 --- a/test/fiber/test_address_resolve.rb +++ b/test/fiber/test_address_resolve.rb @@ -269,7 +269,7 @@ def test_socket_getnameinfo_domain_blocking Fiber.set_scheduler scheduler Fiber.schedule do - result = Socket.getnameinfo(["AF_INET", 80, "example.com"], Socket::NI_NUMERICSERV) + result = Socket.getnameinfo(["AF_INET", 80, "example.com"], Socket::NI_NUMERICSERV | Socket::NI_NUMERICHOST) assert_equal(["1.2.3.4", "80"], result) end diff --git a/test/irb/helper.rb b/test/irb/helper.rb new file mode 100644 index 00000000000000..15d5dafc57ef9d --- /dev/null +++ b/test/irb/helper.rb @@ -0,0 +1,32 @@ +require "test/unit" + +module TestIRB + class TestCase < Test::Unit::TestCase + def save_encodings + @default_encoding = [Encoding.default_external, Encoding.default_internal] + @stdio_encodings = [STDIN, STDOUT, STDERR].map {|io| [io.external_encoding, io.internal_encoding] } + end + + def restore_encodings + EnvUtil.suppress_warning do + Encoding.default_external, Encoding.default_internal = *@default_encoding + [STDIN, STDOUT, STDERR].zip(@stdio_encodings) do |io, encs| + io.set_encoding(*encs) + end + end + end + + def without_rdoc(&block) + ::Kernel.send(:alias_method, :old_require, :require) + + ::Kernel.define_method(:require) do |name| + raise LoadError, "cannot load such file -- rdoc (test)" if name.match?("rdoc") || name.match?(/^rdoc\/.*/) + ::Kernel.send(:old_require, name) + end + + yield + ensure + EnvUtil.suppress_warning { ::Kernel.send(:alias_method, :require, :old_require) } + end + end +end diff --git a/test/irb/test_cmd.rb b/test/irb/test_cmd.rb index 7b7ab55a64cd7c..2728aa656a8f5f 100644 --- a/test/irb/test_cmd.rb +++ b/test/irb/test_cmd.rb @@ -1,10 +1,11 @@ # frozen_string_literal: false -require "test/unit" require "irb" require "irb/extend-command" +require_relative "helper" + module TestIRB - class ExtendCommand < Test::Unit::TestCase + class ExtendCommand < TestCase class TestInputMethod < ::IRB::InputMethod attr_reader :list, :line_no @@ -44,8 +45,7 @@ def setup @home_backup = ENV["HOME"] ENV["HOME"] = @tmpdir @xdg_config_home_backup = ENV.delete("XDG_CONFIG_HOME") - @default_encoding = [Encoding.default_external, Encoding.default_internal] - @stdio_encodings = [STDIN, STDOUT, STDERR].map {|io| [io.external_encoding, io.internal_encoding] } + save_encodings IRB.instance_variable_get(:@CONF).clear @is_win = (RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/) end @@ -55,12 +55,7 @@ def teardown ENV["HOME"] = @home_backup Dir.chdir(@pwd) FileUtils.rm_rf(@tmpdir) - EnvUtil.suppress_warning { - Encoding.default_external, Encoding.default_internal = *@default_encoding - [STDIN, STDOUT, STDERR].zip(@stdio_encodings) do |io, encs| - io.set_encoding(*encs) - end - } + restore_encodings end def test_irb_info_multiline @@ -111,7 +106,17 @@ def test_irb_info_singleline East\sAsian\sAmbiguous\sWidth:\s\d\n #{@is_win ? 'Code\spage:\s\d+\n' : ''} }x - assert_match expected, irb.context.main.irb_info.to_s + info = irb.context.main.irb_info + capture_output do + # Reline::Core#ambiguous_width may access STDOUT, not $stdout + stdout = STDOUT.dup + STDOUT.reopen(IO::NULL, "w") + info = info.to_s + ensure + STDOUT.reopen(stdout) + stdout.close + end + assert_match expected, info ensure ENV["LANG"] = lang_backup ENV["LC_ALL"] = lc_all_backup @@ -406,6 +411,50 @@ def test_irb_source ], out) end + def test_help + IRB.init_config(nil) + input = TestInputMethod.new([ + "help 'String#gsub'\n", + "\n", + ]) + IRB.conf[:PROMPT_MODE] = :SIMPLE + IRB.conf[:VERBOSE] = false + irb = IRB::Irb.new(IRB::WorkSpace.new(self), input) + out, _ = capture_output do + irb.eval_input + end + + # the former is what we'd get without document content installed, like on CI + # the latter is what we may get locally + possible_rdoc_output = [/Nothing known about String#gsub/, /Returns a copy of self with all occurrences of the given pattern/] + assert(possible_rdoc_output.any? { |output| output.match?(out) }, "Expect the help command to match one of the possible outputs") + ensure + # this is the only way to reset the redefined method without coupling the test with its implementation + EnvUtil.suppress_warning { load "irb/cmd/help.rb" } + end + + def test_help_without_rdoc + IRB.init_config(nil) + input = TestInputMethod.new([ + "help 'String#gsub'\n", + "\n", + ]) + IRB.conf[:PROMPT_MODE] = :SIMPLE + IRB.conf[:VERBOSE] = false + irb = IRB::Irb.new(IRB::WorkSpace.new(self), input) + out, _ = capture_output do + without_rdoc do + irb.eval_input + end + end + + # if it fails to require rdoc, it only returns the command object + assert_match(/=> IRB::ExtendCommand::Help\n/, out) + ensure + # this is the only way to reset the redefined method without coupling the test with its implementation + EnvUtil.suppress_warning { load "irb/cmd/help.rb" } + end + def test_irb_load IRB.init_config(nil) File.write("#{@tmpdir}/a.rb", "a = 'hi'\n") @@ -514,6 +563,24 @@ def test_show_source assert_match(%r[/irb\.rb], out) end + def test_show_source_alias + input = TestInputMethod.new([ + "$ 'IRB.conf'\n", + ]) + IRB.init_config(nil) + IRB.conf[:COMMAND_ALIASES] = { :'$' => :show_source } + workspace = IRB::WorkSpace.new(Object.new) + IRB.conf[:VERBOSE] = false + irb = IRB::Irb.new(workspace, input) + IRB.conf[:MAIN_CONTEXT] = irb.context + irb.context.return_format = "=> %s\n" + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_match(%r[/irb\.rb], out) + end + def test_show_source_end_finder pend if RUBY_ENGINE == 'truffleruby' eval(code = <<-EOS, binding, __FILE__, __LINE__ + 1) @@ -554,5 +621,47 @@ def test_whereami assert_empty err assert_match(/^From: .+ @ line \d+ :\n/, out) end + + def test_whereami_alias + input = TestInputMethod.new([ + "@\n", + ]) + IRB.init_config(nil) + IRB.conf[:COMMAND_ALIASES] = { :'@' => :whereami } + workspace = IRB::WorkSpace.new(Object.new) + IRB.conf[:VERBOSE] = false + irb = IRB::Irb.new(workspace, input) + IRB.conf[:MAIN_CONTEXT] = irb.context + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_match(/^From: .+ @ line \d+ :\n/, out) + end + + def test_vars_with_aliases + input = TestInputMethod.new([ + "@foo\n", + "$bar\n", + ]) + IRB.init_config(nil) + IRB.conf[:COMMAND_ALIASES] = { + :'@' => :whereami, + :'$' => :show_source, + } + main = Object.new + main.instance_variable_set(:@foo, "foo") + $bar = "bar" + workspace = IRB::WorkSpace.new(main) + IRB.conf[:VERBOSE] = false + irb = IRB::Irb.new(workspace, input) + IRB.conf[:MAIN_CONTEXT] = irb.context + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_match(/"foo"/, out) + assert_match(/"bar"/, out) + end end end diff --git a/test/irb/test_color.rb b/test/irb/test_color.rb index 02c79e344376f4..1b27afb898de4f 100644 --- a/test/irb/test_color.rb +++ b/test/irb/test_color.rb @@ -1,11 +1,12 @@ # frozen_string_literal: false -require 'test/unit' require 'irb/color' require 'rubygems' require 'stringio' +require_relative "helper" + module TestIRB - class TestColor < Test::Unit::TestCase + class TestColor < TestCase CLEAR = "\e[0m" BOLD = "\e[1m" UNDERLINE = "\e[4m" diff --git a/test/irb/test_color_printer.rb b/test/irb/test_color_printer.rb index 93ec7001465564..79ab050550481b 100644 --- a/test/irb/test_color_printer.rb +++ b/test/irb/test_color_printer.rb @@ -1,11 +1,12 @@ # frozen_string_literal: false -require 'test/unit' require 'irb/color_printer' require 'rubygems' require 'stringio' +require_relative "helper" + module TestIRB - class TestColorPrinter < Test::Unit::TestCase + class TestColorPrinter < TestCase CLEAR = "\e[0m" BOLD = "\e[1m" RED = "\e[31m" diff --git a/test/irb/test_completion.rb b/test/irb/test_completion.rb index df5a78c69dcd44..1ab7dbbb194991 100644 --- a/test/irb/test_completion.rb +++ b/test/irb/test_completion.rb @@ -1,10 +1,11 @@ # frozen_string_literal: false -require "test/unit" require "pathname" require "irb" +require_relative "helper" + module TestIRB - class TestCompletion < Test::Unit::TestCase + class TestCompletion < TestCase def setup # make sure require completion candidates are not cached IRB::InputCompletor.class_variable_set(:@@files_from_load_path, nil) @@ -211,6 +212,96 @@ def test_complete_constants end end + class TestPerfectMatching < TestCompletion + def setup + # trigger PerfectMatchedProc to set up RDocRIDriver constant + IRB::InputCompletor::PerfectMatchedProc.("foo", bind: binding) + + @original_use_stdout = IRB::InputCompletor::RDocRIDriver.use_stdout + # force the driver to use stdout so it doesn't start a pager and interrupt tests + IRB::InputCompletor::RDocRIDriver.use_stdout = true + end + + def teardown + IRB::InputCompletor::RDocRIDriver.use_stdout = @original_use_stdout + end + + def test_perfectly_matched_namespace_triggers_document_display + omit unless has_rdoc_content? + + out, err = capture_output do + IRB::InputCompletor::PerfectMatchedProc.("String", bind: binding) + end + + assert_empty(err) + + assert_include(out, " S\bSt\btr\bri\bin\bng\bg") + end + + def test_perfectly_matched_multiple_namespaces_triggers_document_display + result = nil + out, err = capture_output do + result = IRB::InputCompletor::PerfectMatchedProc.("{}.nil?", bind: binding) + end + + assert_empty(err) + + # check if there're rdoc contents (e.g. CI doesn't generate them) + if has_rdoc_content? + # if there's rdoc content, we can verify by checking stdout + # rdoc generates control characters for formatting method names + assert_include(out, "P\bPr\bro\boc\bc.\b.n\bni\bil\bl?\b?") # Proc.nil? + assert_include(out, "H\bHa\bas\bsh\bh.\b.n\bni\bil\bl?\b?") # Hash.nil? + else + # this is a hacky way to verify the rdoc rendering code path because CI doesn't have rdoc content + # if there are multiple namespaces to be rendered, PerfectMatchedProc renders the result with a document + # which always returns the bytes rendered, even if it's 0 + assert_equal(0, result) + end + end + + def test_not_matched_namespace_triggers_nothing + result = nil + out, err = capture_output do + result = IRB::InputCompletor::PerfectMatchedProc.("Stri", bind: binding) + end + + assert_empty(err) + assert_empty(out) + assert_nil(result) + end + + def test_perfect_matching_stops_without_rdoc + result = nil + + out, err = capture_output do + without_rdoc do + result = IRB::InputCompletor::PerfectMatchedProc.("String", bind: binding) + end + end + + assert_empty(err) + assert_not_match(/from ruby core/, out) + assert_nil(result) + end + + def test_perfect_matching_handles_nil_namespace + out, err = capture_output do + # symbol literal has `nil` doc namespace so it's a good test subject + assert_nil(IRB::InputCompletor::PerfectMatchedProc.(":aiueo", bind: binding)) + end + + assert_empty(err) + assert_empty(out) + end + + private + + def has_rdoc_content? + File.exist?(RDoc::RI::Paths::BASE) + end + end + def test_complete_symbol %w"UTF-16LE UTF-7".each do |enc| "K".force_encoding(enc).to_sym @@ -233,10 +324,6 @@ def test_complete_absolute_constants_with_special_characters assert_empty(IRB::InputCompletor.retrieve_completion_data("::A)", bind: binding)) end - def test_complete_symbol_failure - assert_nil(IRB::InputCompletor::PerfectMatchedProc.(":aiueo", bind: binding)) - end - def test_complete_reserved_words candidates = IRB::InputCompletor.retrieve_completion_data("de", bind: binding) %w[def defined?].each do |word| diff --git a/test/irb/test_context.rb b/test/irb/test_context.rb index 998cdd8591d0f7..e7cc0bab4f4fd8 100644 --- a/test/irb/test_context.rb +++ b/test/irb/test_context.rb @@ -1,11 +1,12 @@ # frozen_string_literal: false -require 'test/unit' require 'tempfile' require 'irb' require 'rubygems' if defined?(Gem) +require_relative "helper" + module TestIRB - class TestContext < Test::Unit::TestCase + class TestContext < TestCase class TestInputMethod < ::IRB::InputMethod attr_reader :list, :line_no diff --git a/test/irb/test_history.rb b/test/irb/test_history.rb index fab38074470ed5..b0a07ca29d3e8a 100644 --- a/test/irb/test_history.rb +++ b/test/irb/test_history.rb @@ -1,11 +1,12 @@ # frozen_string_literal: false -require 'test/unit' require 'irb' require 'irb/ext/save-history' require 'readline' +require_relative "helper" + module TestIRB - class TestHistory < Test::Unit::TestCase + class TestHistory < TestCase def setup IRB.conf[:RC_NAME_GENERATOR] = nil end diff --git a/test/irb/test_init.rb b/test/irb/test_init.rb index 3293b98d345264..9591de15895f45 100644 --- a/test/irb/test_init.rb +++ b/test/irb/test_init.rb @@ -1,10 +1,11 @@ # frozen_string_literal: false -require "test/unit" require "irb" require "fileutils" +require_relative "helper" + module TestIRB - class TestInit < Test::Unit::TestCase + class TestInit < TestCase def setup # IRBRC is for RVM... @backup_env = %w[HOME XDG_CONFIG_HOME IRBRC].each_with_object({}) do |env, hash| @@ -36,13 +37,16 @@ def test_setup_with_minimum_argv_does_not_change_dollar0 def test_rc_file tmpdir = @tmpdir Dir.chdir(tmpdir) do + ENV["XDG_CONFIG_HOME"] = "#{tmpdir}/xdg" IRB.conf[:RC_NAME_GENERATOR] = nil assert_equal(tmpdir+"/.irb#{IRB::IRBRC_EXT}", IRB.rc_file) assert_equal(tmpdir+"/.irb_history", IRB.rc_file("_history")) + assert_file.not_exist?(tmpdir+"/xdg") IRB.conf[:RC_NAME_GENERATOR] = nil FileUtils.touch(tmpdir+"/.irb#{IRB::IRBRC_EXT}") assert_equal(tmpdir+"/.irb#{IRB::IRBRC_EXT}", IRB.rc_file) assert_equal(tmpdir+"/.irb_history", IRB.rc_file("_history")) + assert_file.not_exist?(tmpdir+"/xdg") end end @@ -80,6 +84,10 @@ def test_no_color_environment_variable IRB.setup(__FILE__) refute IRB.conf[:USE_COLORIZE] + ENV['NO_COLOR'] = '' + IRB.setup(__FILE__) + assert IRB.conf[:USE_COLORIZE] + ENV['NO_COLOR'] = nil IRB.setup(__FILE__) assert IRB.conf[:USE_COLORIZE] diff --git a/test/irb/test_input_method.rb b/test/irb/test_input_method.rb index b9832d39ed887f..fdfb5663905a66 100644 --- a/test/irb/test_input_method.rb +++ b/test/irb/test_input_method.rb @@ -1,29 +1,20 @@ # frozen_string_literal: false -require "test/unit" require "irb" +require_relative "helper" + module TestIRB - class TestRelineInputMethod < Test::Unit::TestCase + class TestRelineInputMethod < TestCase def setup @conf_backup = IRB.conf.dup IRB.conf[:LC_MESSAGES] = IRB::Locale.new - - # RelineInputMethod#initialize calls IRB.set_encoding, which mutates standard input/output's encoding - # so we need to make sure we set them back - @original_encodings = { - STDIN => [STDIN.external_encoding, STDIN.internal_encoding], - STDOUT => [STDOUT.external_encoding, STDOUT.internal_encoding], - STDERR => [STDERR.external_encoding, STDERR.internal_encoding], - } + save_encodings end def teardown IRB.conf.replace(@conf_backup) - - @original_encodings.each do |io, (external_encoding, internal_encoding)| - io.set_encoding(external_encoding, internal_encoding) - end + restore_encodings end def test_initialization @@ -83,19 +74,6 @@ def test_initialization_with_use_autocomplete_but_without_rdoc ensure Reline.add_dialog_proc(:show_doc, original_show_doc_proc, Reline::DEFAULT_DIALOG_CONTEXT) end - - def without_rdoc(&block) - ::Kernel.send(:alias_method, :old_require, :require) - - ::Kernel.define_method(:require) do |name| - raise LoadError, "cannot load such file -- rdoc (test)" if name == "rdoc" - original_require(name) - end - - yield - ensure - ::Kernel.send(:alias_method, :require, :old_require) - end end end diff --git a/test/irb/test_option.rb b/test/irb/test_option.rb index aa634c02a22bd1..e334cee6dd39a6 100644 --- a/test/irb/test_option.rb +++ b/test/irb/test_option.rb @@ -1,8 +1,8 @@ # frozen_string_literal: false -require 'test/unit' +require_relative "helper" module TestIRB - class TestOption < Test::Unit::TestCase + class TestOption < TestCase def test_end_of_option bug4117 = '[ruby-core:33574]' bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] diff --git a/test/irb/test_raise_no_backtrace_exception.rb b/test/irb/test_raise_no_backtrace_exception.rb index 530adc0cbf1cc3..ba8e89c59fb63b 100644 --- a/test/irb/test_raise_no_backtrace_exception.rb +++ b/test/irb/test_raise_no_backtrace_exception.rb @@ -1,10 +1,10 @@ # frozen_string_literal: false -require 'test/unit' + +require_relative "helper" module TestIRB - class TestRaiseNoBacktraceException < Test::Unit::TestCase + class TestRaiseNoBacktraceException < TestCase def test_raise_exception - pend if RUBY_ENGINE == 'truffleruby' bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] assert_in_out_err(bundle_exec + %w[-rirb -W0 -e IRB.start(__FILE__) -- -f --], <<-IRB, /Exception: foo/, []) e = Exception.new("foo") @@ -23,7 +23,6 @@ def test_raise_exception_with_invalid_byte_sequence end def test_raise_exception_with_different_encoding_containing_invalid_byte_sequence - pend if RUBY_ENGINE == 'truffleruby' backup_home = ENV["HOME"] Dir.mktmpdir("test_irb_raise_no_backtrace_exception_#{$$}") do |tmpdir| ENV["HOME"] = tmpdir @@ -40,6 +39,8 @@ def raise_euc_with_invalid_byte_sequence end env = {} %w(LC_MESSAGES LC_ALL LC_CTYPE LANG).each {|n| env[n] = "ja_JP.UTF-8" } + # TruffleRuby warns when the locale does not exist + env['TRUFFLERUBYOPT'] = "#{ENV['TRUFFLERUBYOPT']} --log.level=SEVERE" if RUBY_ENGINE == 'truffleruby' args = [env] + bundle_exec + %W[-rirb -C #{tmpdir} -W0 -e IRB.start(__FILE__) -- -f --] error = /`raise_euc_with_invalid_byte_sequence': あ\\xFF \(RuntimeError\)/ assert_in_out_err(args, <<~IRB, error, [], encoding: "UTF-8") diff --git a/test/irb/test_ruby_lex.rb b/test/irb/test_ruby_lex.rb index 1388d0896204c8..2b16e2e9a10b6c 100644 --- a/test/irb/test_ruby_lex.rb +++ b/test/irb/test_ruby_lex.rb @@ -1,10 +1,12 @@ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) -require 'irb/ruby-lex' -require 'test/unit' +require 'irb' +require 'rubygems' require 'ostruct' +require_relative "helper" + module TestIRB - class TestRubyLex < Test::Unit::TestCase + class TestRubyLex < TestCase Row = Struct.new(:content, :current_line_spaces, :new_line_spaces, :nesting_level) class MockIO_AutoIndent @@ -19,18 +21,27 @@ def auto_indent(&block) end end + def setup + save_encodings + end + + def teardown + restore_encodings + end + def assert_indenting(lines, correct_space_count, add_new_line) lines = lines + [""] if add_new_line last_line_index = lines.length - 1 byte_pointer = lines.last.length + context = build_context + context.auto_indent_mode = true ruby_lex = RubyLex.new() io = MockIO_AutoIndent.new([lines, last_line_index, byte_pointer, add_new_line]) do |auto_indent| error_message = "Calculated the wrong number of spaces for:\n #{lines.join("\n")}" assert_equal(correct_space_count, auto_indent, error_message) end - ruby_lex.set_input(io) - context = OpenStruct.new(auto_indent_mode: true) + ruby_lex.set_input(io, context: context) ruby_lex.set_auto_indent(context) end @@ -48,11 +59,10 @@ def assert_code_block_open(lines, expected, local_variables: []) def ruby_lex_for_lines(lines, local_variables: []) ruby_lex = RubyLex.new() + + context = build_context(local_variables) io = proc{ lines.join("\n") } - ruby_lex.set_input(io, io) - unless local_variables.empty? - context = OpenStruct.new(local_variables: local_variables) - end + ruby_lex.set_input(io, io, context: context) ruby_lex.lex(context) ruby_lex end @@ -620,7 +630,8 @@ def assert_dynamic_prompt(lines, expected_prompt_list) ruby_lex.set_prompt do |ltype, indent, continue, line_no| '%03d:%01d:%1s:%s ' % [line_no, indent, ltype, continue ? '*' : '>'] end - ruby_lex.set_input(io) + context = build_context + ruby_lex.set_input(io, context: context) end def test_dyanmic_prompt @@ -697,5 +708,21 @@ def test_unterminated_heredoc_string_literal assert_equal('<@tmpdir}, ".options", &block) + end + + def setup_options_xdg_config_home(&block) + setup_options({'XDG_CONFIG_HOME'=>@tmpdir+"/xdg"}, "xdg", ".options", &block) + end + + def setup_options_home_config(&block) + setup_options({'HOME'=>@tmpdir}, ".config", ".options", &block) + end + + def setup_options_xdg_config_dirs(&block) + setup_options({'XDG_CONFIG_DIRS'=>@tmpdir+"/xdgconf"}, "xdgconf", ".options", &block) + end + + def setup_options_home_config_settings(&block) + setup_options({'HOME'=>@tmpdir}, "config/settings", ".options", &block) + end + + def test_load_home_options + result, = setup_options_home + assert_load(result) + + setup_options_xdg_config_home do + assert_load(result) + end + + setup_options_home_config do + assert_load(result) + end + + setup_options_xdg_config_dirs do + assert_load(result) + end + + setup_options_home_config_settings do + assert_load(result) + end + end + + def test_load_xdg_config_home + result, = setup_options_xdg_config_home + assert_load(result) + + setup_options_home_config do + assert_load(result) + end + + setup_options_xdg_config_dirs do + assert_load(result) + end + + setup_options_home_config_settings do + assert_load(result) + end + end + + def test_load_home_config + result, = setup_options_home_config + assert_load(result) + + setup_options_xdg_config_dirs do + assert_load(result) + end + + setup_options_home_config_settings do + assert_load(result) + end + end + + def test_load_xdg_config_dirs + result, = setup_options_xdg_config_dirs + assert_load(result) + + setup_options_home_config_settings do + assert_load(result) + end + end + + def test_load_home_config_settings + result, = setup_options_home_config_settings + assert_load(result) + end + + def test_load_nothing + assert !new_parser.load + assert_nil @result + end +end diff --git a/test/ruby/test_ast.rb b/test/ruby/test_ast.rb index aaf626e801f6b4..0932e93d5a909e 100644 --- a/test/ruby/test_ast.rb +++ b/test/ruby/test_ast.rb @@ -186,6 +186,19 @@ def test_parse_file_raises_syntax_error end end + def test_node_id_for_location + exception = begin + raise + rescue => e + e + end + loc = exception.backtrace_locations.first + node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(loc) + node = RubyVM::AbstractSyntaxTree.of(loc, keep_script_lines: true) + + assert_equal node.node_id, node_id + end + def test_of_proc_and_method proc = Proc.new { 1 + 2 } method = self.method(__method__) diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb index 6cafb21698e688..1a564889af6963 100644 --- a/test/ruby/test_yjit.rb +++ b/test/ruby/test_yjit.rb @@ -513,9 +513,8 @@ def foo &blk RUBY end - def test_getblockparamproxy_with_no_block - # Currently side exits on the send - assert_compiles(<<~'RUBY', insns: [:getblockparamproxy], exits: { send: 2 }) + def test_send_blockarg + assert_compiles(<<~'RUBY', insns: [:getblockparamproxy, :send], exits: {}) def bar end @@ -526,6 +525,9 @@ def foo &blk foo foo + + foo { } + foo { } RUBY end @@ -825,12 +827,121 @@ def foo RUBY end + def test_code_gc + assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok) + return :not_paged unless add_pages(100) # prepare freeable pages + RubyVM::YJIT.code_gc # first code GC + return :not_compiled1 unless compiles { nil } # should be JITable again + + RubyVM::YJIT.code_gc # second code GC + return :not_compiled2 unless compiles { nil } # should be JITable again + + code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count] + return :"code_gc_#{code_gc_count}" if code_gc_count != 2 + + :ok + RUBY + end + + def test_on_stack_code_gc_call + assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok) + fiber = Fiber.new { + # Loop to call the same basic block again after Fiber.yield + while true + Fiber.yield(nil.to_i) + end + } + + return :not_paged1 unless add_pages(400) # go to a page without initial ocb code + return :broken_resume1 if fiber.resume != 0 # JIT the fiber + RubyVM::YJIT.code_gc # first code GC, which should not free the fiber page + return :broken_resume2 if fiber.resume != 0 # The code should be still callable + + code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count] + return :"code_gc_#{code_gc_count}" if code_gc_count != 1 + + :ok + RUBY + end + + def test_on_stack_code_gc_twice + assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok) + fiber = Fiber.new { + # Loop to call the same basic block again after Fiber.yield + while Fiber.yield(nil.to_i); end + } + + return :not_paged1 unless add_pages(400) # go to a page without initial ocb code + return :broken_resume1 if fiber.resume(true) != 0 # JIT the fiber + RubyVM::YJIT.code_gc # first code GC, which should not free the fiber page + + return :not_paged2 unless add_pages(300) # add some stuff to be freed + # Not calling fiber.resume here to test the case that the YJIT payload loses some + # information at the previous code GC. The payload should still be there, and + # thus we could know the fiber ISEQ is still on stack on this second code GC. + RubyVM::YJIT.code_gc # second code GC, which should still not free the fiber page + + return :not_paged3 unless add_pages(200) # attempt to overwrite the fiber page (it shouldn't) + return :broken_resume2 if fiber.resume(true) != 0 # The fiber code should be still fine + + return :broken_resume3 if fiber.resume(false) != nil # terminate the fiber + RubyVM::YJIT.code_gc # third code GC, freeing a page that used to be on stack + + return :not_paged4 unless add_pages(100) # check everything still works + + code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count] + return :"code_gc_#{code_gc_count}" if code_gc_count != 3 + + :ok + RUBY + end + + def test_code_gc_with_many_iseqs + assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok, mem_size: 1) + fiber = Fiber.new { + # Loop to call the same basic block again after Fiber.yield + while true + Fiber.yield(nil.to_i) + end + } + + return :not_paged1 unless add_pages(500) # use some pages + return :broken_resume1 if fiber.resume != 0 # leave an on-stack code as well + + add_pages(2000) # use a whole lot of pages to run out of 1MiB + return :broken_resume2 if fiber.resume != 0 # on-stack code should be callable + + code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count] + return :"code_gc_#{code_gc_count}" if code_gc_count == 0 + + :ok + RUBY + end + + private + + def code_gc_helpers + <<~'RUBY' + def compiles(&block) + failures = RubyVM::YJIT.runtime_stats[:compilation_failure] + block.call + failures == RubyVM::YJIT.runtime_stats[:compilation_failure] + end + + def add_pages(num_jits) + pages = RubyVM::YJIT.runtime_stats[:compiled_page_count] + num_jits.times { return false unless eval('compiles { nil.to_i }') } + pages.nil? || pages < RubyVM::YJIT.runtime_stats[:compiled_page_count] + end + RUBY + end + def assert_no_exits(script) assert_compiles(script) end ANY = Object.new - def assert_compiles(test_script, insns: [], call_threshold: 1, stdout: nil, exits: {}, result: ANY, frozen_string_literal: nil) + def assert_compiles(test_script, insns: [], call_threshold: 1, stdout: nil, exits: {}, result: ANY, frozen_string_literal: nil, mem_size: nil) reset_stats = <<~RUBY RubyVM::YJIT.runtime_stats RubyVM::YJIT.reset_stats! @@ -864,7 +975,7 @@ def collect_insns(iseq) #{write_results} RUBY - status, out, err, stats = eval_with_jit(script, call_threshold: call_threshold) + status, out, err, stats = eval_with_jit(script, call_threshold:, mem_size:) assert status.success?, "exited with status #{status.to_i}, stderr:\n#{err}" @@ -918,12 +1029,13 @@ def script_shell_encode(s) s.chars.map { |c| c.ascii_only? ? c : "\\u%x" % c.codepoints[0] }.join end - def eval_with_jit(script, call_threshold: 1, timeout: 1000) + def eval_with_jit(script, call_threshold: 1, timeout: 1000, mem_size: nil) args = [ "--disable-gems", "--yjit-call-threshold=#{call_threshold}", "--yjit-stats" ] + args << "--yjit-exec-mem-size=#{mem_size}" if mem_size args << "-e" << script_shell_encode(script) stats_r, stats_w = IO.pipe out, err, status = EnvUtil.invoke_ruby(args, diff --git a/test/rubygems/test_gem_ext_cargo_builder/custom_name/Cargo.lock b/test/rubygems/test_gem_ext_cargo_builder/custom_name/Cargo.lock index cf11ab247b86d0..aa975b1cd02d05 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/custom_name/Cargo.lock +++ b/test/rubygems/test_gem_ext_cargo_builder/custom_name/Cargo.lock @@ -160,21 +160,21 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.31" +version = "0.9.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfc41b26ea88da6100f538d31467941e41ab0c002999d687315e67d3b371b796" +checksum = "2d2bde30824a18f2e68cd1c8004cec16656764c6efc385bc1c7fb4c904b276a5" dependencies = [ - "bindgen", - "linkify", "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.31" +version = "0.9.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79be4233eabd2bf9e19eb8116391aeaf4b89b87a7ab38e0ded44de9158006e46" +checksum = "5ff5d3ba92624df9c66bf0d1f0251d96284f08ac9773b7723d370e3f225c1d38" dependencies = [ + "bindgen", + "linkify", "regex", "shell-words", ] diff --git a/test/rubygems/test_gem_ext_cargo_builder/custom_name/Cargo.toml b/test/rubygems/test_gem_ext_cargo_builder/custom_name/Cargo.toml index 8dfbaf5799f097..6673f784645b9a 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/custom_name/Cargo.toml +++ b/test/rubygems/test_gem_ext_cargo_builder/custom_name/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -rb-sys = { version = "0.9.31", features = ["gem"] } +rb-sys = { version = "0.9.35", features = ["gem"] } diff --git a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock index 93e561357465da..e7e91de57674cc 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock +++ b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock @@ -153,18 +153,18 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.34" +version = "0.9.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ff8b3a4d418f3604ac3781aee54a094f3f9d732fb9a2458f73a3937a9ea918" +checksum = "2d2bde30824a18f2e68cd1c8004cec16656764c6efc385bc1c7fb4c904b276a5" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.34" +version = "0.9.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2921dcd727615d4af5a6d39cc3c1c8c9dc8e37bf2b42d44b3e101a6da2bce17" +checksum = "5ff5d3ba92624df9c66bf0d1f0251d96284f08ac9773b7723d370e3f225c1d38" dependencies = [ "bindgen", "linkify", diff --git a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml index ecd8ea142ab34d..814afe10f72f2d 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml +++ b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -rb-sys = { version = "0.9.34", features = ["gem"] } +rb-sys = { version = "0.9.35", features = ["gem"] } diff --git a/test/test_tmpdir.rb b/test/test_tmpdir.rb index 0be2176bd9576b..eada416f6fceb5 100644 --- a/test/test_tmpdir.rb +++ b/test/test_tmpdir.rb @@ -23,14 +23,14 @@ def test_world_writable ENV[e] = tmpdirx assert_not_equal(tmpdirx, assert_warn('') {Dir.tmpdir}) File.write(tmpdirx, "") - assert_not_equal(tmpdirx, assert_warn(/not a directory/) {Dir.tmpdir}) + assert_not_equal(tmpdirx, assert_warn(/\A#{e} is not a directory/) {Dir.tmpdir}) File.unlink(tmpdirx) ENV[e] = tmpdir assert_equal(tmpdir, Dir.tmpdir) File.chmod(0555, tmpdir) - assert_not_equal(tmpdir, assert_warn(/not writable/) {Dir.tmpdir}) + assert_not_equal(tmpdir, assert_warn(/\A#{e} is not writable/) {Dir.tmpdir}) File.chmod(0777, tmpdir) - assert_not_equal(tmpdir, assert_warn(/world-writable/) {Dir.tmpdir}) + assert_not_equal(tmpdir, assert_warn(/\A#{e} is world-writable/) {Dir.tmpdir}) newdir = Dir.mktmpdir("d", tmpdir) do |dir| assert_file.directory? dir assert_equal(tmpdir, File.dirname(dir)) @@ -47,6 +47,18 @@ def test_world_writable end end + def test_tmpdir_not_empty_parent + Dir.mktmpdir do |tmpdir| + envs = %w[TMPDIR TMP TEMP] + oldenv = envs.each_with_object({}) {|v, h| h[v] = ENV.delete(v)} + ENV[envs[0]] = "" + ENV[envs[1]] = tmpdir + assert_equal(tmpdir, Dir.tmpdir) + ensure + ENV.update(oldenv) + end + end + def test_no_homedir bug7547 = '[ruby-core:50793]' home, ENV["HOME"] = ENV["HOME"], nil diff --git a/thread_none.c b/thread_none.c index cf4658e5712751..a82ced684e6897 100644 --- a/thread_none.c +++ b/thread_none.c @@ -15,6 +15,10 @@ #include +#if defined(__wasm__) && !defined(__EMSCRIPTEN__) +# include "wasm/machine.h" +#endif + #define TIME_QUANTUM_MSEC (100) #define TIME_QUANTUM_USEC (TIME_QUANTUM_MSEC * 1000) #define TIME_QUANTUM_NSEC (TIME_QUANTUM_USEC * 1000) @@ -143,6 +147,9 @@ ruby_init_stack(volatile VALUE *addr) static int native_thread_init_stack(rb_thread_t *th) { +#if defined(__wasm__) && !defined(__EMSCRIPTEN__) + th->ec->machine.stack_start = (VALUE *)rb_wasm_stack_get_base(); +#endif return 0; // success } diff --git a/tool/file2lastrev.rb b/tool/file2lastrev.rb index 94c2b2a9d6c480..1dfb01ab9e5e96 100755 --- a/tool/file2lastrev.rb +++ b/tool/file2lastrev.rb @@ -8,42 +8,46 @@ # this file run with BASERUBY, which may be older than 1.9, so no # require_relative require File.expand_path('../lib/vcs', __FILE__) +require File.expand_path('../lib/output', __FILE__) Program = $0 -@output = nil -def self.output=(output) - if @output and @output != output +@format = nil +def self.format=(format) + if @format and @format != format raise "you can specify only one of --changed, --revision.h and --doxygen" end - @output = output + @format = format end @suppress_not_found = false @limit = 20 +@output = Output.new -format = '%Y-%m-%dT%H:%M:%S%z' +time_format = '%Y-%m-%dT%H:%M:%S%z' vcs = nil OptionParser.new {|opts| opts.banner << " paths..." vcs_options = VCS.define_options(opts) + opts.new {@output.def_options(opts)} srcdir = nil opts.new opts.on("--srcdir=PATH", "use PATH as source directory") do |path| abort "#{File.basename(Program)}: srcdir is already set" if srcdir srcdir = path + @output.vpath.add(srcdir) end opts.on("--changed", "changed rev") do - self.output = :changed + self.format = :changed end opts.on("--revision.h", "RUBY_REVISION macro") do - self.output = :revision_h + self.format = :revision_h end opts.on("--doxygen", "Doxygen format") do - self.output = :doxygen + self.format = :doxygen end opts.on("--modified[=FORMAT]", "modified time") do |fmt| - self.output = :modified - format = fmt if fmt + self.format = :modified + time_format = fmt if fmt end opts.on("--limit=NUM", "limit branch name length (#@limit)", Integer) do |n| @limit = n @@ -61,15 +65,15 @@ def self.output=(output) end } -output = - case @output +formatter = + case @format when :changed, nil Proc.new {|last, changed| - changed + changed || "" } when :revision_h Proc.new {|last, changed, modified, branch, title| - vcs.revision_header(last, modified, modified, branch, title, limit: @limit) + vcs.revision_header(last, modified, modified, branch, title, limit: @limit).join("\n") } when :doxygen Proc.new {|last, changed| @@ -77,16 +81,18 @@ def self.output=(output) } when :modified Proc.new {|last, changed, modified| - modified.strftime(format) + modified.strftime(time_format) } else - raise "unknown output format `#{@output}'" + raise "unknown output format `#{@format}'" end ok = true (ARGV.empty? ? [nil] : ARGV).each do |arg| begin - puts output[*vcs.get_revisions(arg)] + data = formatter[*vcs.get_revisions(arg)] + data.sub!(/(? e warn "#{File.basename(Program)}: #{e.message}" ok = false diff --git a/tool/generic_erb.rb b/tool/generic_erb.rb index 6af995fc139e4b..47bffd830c81dd 100644 --- a/tool/generic_erb.rb +++ b/tool/generic_erb.rb @@ -5,31 +5,23 @@ require 'erb' require 'optparse' -require_relative 'lib/vpath' -require_relative 'lib/colorize' +require_relative 'lib/output' -vpath = VPath.new -timestamp = nil -output = nil -ifchange = nil +out = Output.new source = false -color = nil templates = [] ARGV.options do |o| - o.on('-t', '--timestamp[=PATH]') {|v| timestamp = v || true} o.on('-i', '--input=PATH') {|v| template << v} - o.on('-o', '--output=PATH') {|v| output = v} - o.on('-c', '--[no-]if-change') {|v| ifchange = v} o.on('-x', '--source') {source = true} - o.on('--color') {color = true} - vpath.def_options(o) + out.def_options(o) o.order!(ARGV) templates << (ARGV.shift or abort o.to_s) if templates.empty? end -color = Colorize.new(color) -unchanged = color.pass("unchanged") -updated = color.fail("updated") + +# Used in prelude.c.tmpl and unicode_norm_gen.tmpl +output = out.path +vpath = out.vpath result = templates.map do |template| if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+ @@ -41,21 +33,4 @@ source ? erb.src : proc{erb.result(binding)}.call end result = result.size == 1 ? result[0] : result.join("") -if output - if ifchange and (vpath.open(output, "rb") {|f| f.read} rescue nil) == result - puts "#{output} #{unchanged}" - else - open(output, "wb") {|f| f.print result} - puts "#{output} #{updated}" - end - if timestamp - if timestamp == true - dir, base = File.split(output) - timestamp = File.join(dir, ".time." + base) - end - File.open(timestamp, 'a') {} - File.utime(nil, nil, timestamp) - end -else - print result -end +out.write(result) diff --git a/tool/lib/colorize.rb b/tool/lib/colorize.rb index 11b878d3182c0e..f11137cfdf22c3 100644 --- a/tool/lib/colorize.rb +++ b/tool/lib/colorize.rb @@ -7,7 +7,7 @@ class Colorize def initialize(color = nil, opts = ((_, color = color, nil)[0] if Hash === color)) @colors = @reset = nil @color = (opts[:color] if opts) - if color or (color == nil && STDOUT.tty?) + if color or (color == nil && STDOUT.tty? && (ENV["NO_COLOR"] || "").empty?) if (%w[smso so].any? {|attr| /\A\e\[.*m\z/ =~ IO.popen("tput #{attr}", "r", :err => IO::NULL, &:read)} rescue nil) @beg = "\e[" colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {} diff --git a/tool/lib/leakchecker.rb b/tool/lib/leakchecker.rb index 26d75b92fa87a0..19661455883e1a 100644 --- a/tool/lib/leakchecker.rb +++ b/tool/lib/leakchecker.rb @@ -234,6 +234,9 @@ def check_env(test_name) new_env = find_env return false if old_env == new_env (old_env.keys | new_env.keys).sort.each {|k| + # Don't report changed environment variables caused by Bundler's backups + next if k.start_with?(Bundler::EnvironmentPreserver::BUNDLER_PREFIX) + if old_env.has_key?(k) if new_env.has_key?(k) if old_env[k] != new_env[k] diff --git a/tool/lib/output.rb b/tool/lib/output.rb new file mode 100644 index 00000000000000..cbaa4b5a3e8c3b --- /dev/null +++ b/tool/lib/output.rb @@ -0,0 +1,51 @@ +require_relative 'vpath' +require_relative 'colorize' + +class Output + attr_reader :path, :vpath + + def initialize + @path = @timestamp = @ifchange = @color = nil + @vpath = VPath.new + end + + def def_options(opt) + opt.separator(" Output common options:") + opt.on('-o', '--output=PATH') {|v| @path = v} + opt.on('-t', '--timestamp[=PATH]') {|v| @timestamp = v || true} + opt.on('-c', '--[no-]if-change') {|v| @ifchange = v} + opt.on('--color') {@color = true} + @vpath.def_options(opt) + end + + def write(data, overwrite: false) + unless @path + $stdout.print data + return true + end + color = Colorize.new(@color) + unchanged = color.pass("unchanged") + updated = color.fail("updated") + outpath = nil + + if @ifchange and (@vpath.open(@path, "rb") {|f| outpath = f.path; f.read == data} rescue false) + puts "#{outpath} #{unchanged}" + written = false + else + unless overwrite and outpath and (File.binwrite(outpath, data) rescue nil) + File.binwrite(outpath = @path, data) + end + puts "#{outpath} #{updated}" + written = true + end + if timestamp = @timestamp + if timestamp == true + dir, base = File.split(@path) + timestamp = File.join(dir, ".time." + base) + end + File.binwrite(timestamp, '') + File.utime(nil, nil, timestamp) + end + written + end +end diff --git a/tool/lib/test/unit.rb b/tool/lib/test/unit.rb index b2190843c8bc4e..59409f016c85be 100644 --- a/tool/lib/test/unit.rb +++ b/tool/lib/test/unit.rb @@ -787,7 +787,7 @@ def _run_parallel suites, type, result unless rep.empty? rep.each do |r| if r[:error] - puke(*r[:error], Timeout::Error) + puke(*r[:error], Timeout::Error.new) next end r[:report]&.each do |f| @@ -1742,6 +1742,9 @@ def puke klass, meth, e when Test::Unit::AssertionFailedError then @failures += 1 "Failure:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" + when Timeout::Error + @errors += 1 + "Timeout:\n#{klass}##{meth}\n" else @errors += 1 bt = Test::filter_backtrace(e.backtrace).join "\n " diff --git a/tool/lib/vcs.rb b/tool/lib/vcs.rb index 3b60385e59eb04..857b7928a458a0 100644 --- a/tool/lib/vcs.rb +++ b/tool/lib/vcs.rb @@ -1,6 +1,8 @@ # vcs require 'fileutils' require 'optparse' +require 'pp' +require 'tempfile' # This library is used by several other tools/ scripts to detect the current # VCS in use (e.g. SVN, Git) or to interact with that VCS. @@ -9,6 +11,22 @@ class VCS DEBUG_OUT = STDERR.dup + + def self.dump(obj, pre = nil) + out = DEBUG_OUT + @pp ||= PP.new(out) + @pp.guard_inspect_key do + if pre + @pp.group(pre.size, pre) { + obj.pretty_print(@pp) + } + else + obj.pretty_print(@pp) + end + @pp.flush + out << "\n" + end + end end unless File.respond_to? :realpath @@ -19,14 +37,14 @@ def File.realpath(arg) end def IO.pread(*args) - VCS::DEBUG_OUT.puts(args.inspect) if $DEBUG + VCS.dump(args, "args: ") if $DEBUG popen(*args) {|f|f.read} end module DebugPOpen refine IO.singleton_class do def popen(*args) - VCS::DEBUG_OUT.puts args.inspect if $DEBUG + VCS.dump(args, "args: ") if $DEBUG super end end @@ -34,7 +52,7 @@ def popen(*args) using DebugPOpen module DebugSystem def system(*args) - VCS::DEBUG_OUT.puts args.inspect if $DEBUG + VCS.dump(args, "args: ") if $DEBUG exception = false opts = Hash.try_convert(args[-1]) if RUBY_VERSION >= "2.6" @@ -394,7 +412,7 @@ def export_changelog(url = '.', from = nil, to = nil, _path = nil, path: _path) def commit args = %W"#{COMMAND} commit" if dryrun? - VCS::DEBUG_OUT.puts(args.inspect) + VCS.dump(args, "commit: ") return true end system(*args) @@ -402,8 +420,21 @@ def commit end class GIT < self - register(".git") { |path, dir| File.exist?(File.join(path, dir)) } - COMMAND = ENV["GIT"] || 'git' + register(".git") do |path, dir| + SAFE_DIRECTORIES ||= + begin + command = ENV["GIT"] || 'git' + dirs = IO.popen(%W"#{command} config --global --get-all safe.directory", &:read).split("\n") + rescue + command = nil + dirs = [] + ensure + VCS.dump(dirs, "safe.directory: ") if $DEBUG + COMMAND = command + end + + COMMAND and File.exist?(File.join(path, dir)) + end def cmd_args(cmds, srcdir = nil) (opts = cmds.last).kind_of?(Hash) or cmds << (opts = {}) @@ -411,7 +442,7 @@ def cmd_args(cmds, srcdir = nil) if srcdir opts[:chdir] ||= srcdir end - VCS::DEBUG_OUT.puts cmds.inspect if debug? + VCS.dump(cmds, "cmds: ") if debug? and !$DEBUG cmds end @@ -421,7 +452,7 @@ def cmd_pipe_at(srcdir, cmds, &block) def cmd_read_at(srcdir, cmds) result = without_gitconfig { IO.pread(*cmd_args(cmds, srcdir)) } - VCS::DEBUG_OUT.puts result.inspect if debug? + VCS.dump(result, "result: ") if debug? result end @@ -442,7 +473,14 @@ def svn_revision(log) def _get_revisions(path, srcdir = nil) ref = Branch === path ? path.to_str : 'HEAD' gitcmd = [COMMAND] - last = cmd_read_at(srcdir, [[*gitcmd, 'rev-parse', ref]]).rstrip + last = nil + IO.pipe do |r, w| + last = cmd_read_at(srcdir, [[*gitcmd, 'rev-parse', ref, err: w]]).rstrip + w.close + unless r.eof? + raise "#{COMMAND} rev-parse failed\n#{r.read.gsub(/^(?=\s*\S)/, ' ')}" + end + end log = cmd_read_at(srcdir, [[*gitcmd, 'log', '-n1', '--date=iso', '--pretty=fuller', *path]]) changed = log[/\Acommit (\h+)/, 1] modified = log[/^CommitDate:\s+(.*)/, 1] @@ -504,19 +542,35 @@ def revision_handler(rev) end def without_gitconfig - envs = %w'HOME XDG_CONFIG_HOME GIT_SYSTEM_CONFIG GIT_CONFIG_SYSTEM'.each_with_object({}) do |v, h| + envs = (%w'HOME XDG_CONFIG_HOME' + ENV.keys.grep(/\AGIT_/)).each_with_object({}) do |v, h| h[v] = ENV.delete(v) - ENV[v] = NullDevice if v.start_with?('GIT_') end + ENV['GIT_CONFIG_SYSTEM'] = NullDevice + ENV['GIT_CONFIG_GLOBAL'] = global_config yield ensure ENV.update(envs) end + def global_config + return NullDevice if SAFE_DIRECTORIES.empty? + unless @gitconfig + @gitconfig = Tempfile.new(%w"vcs_ .gitconfig") + @gitconfig.close + ENV['GIT_CONFIG_GLOBAL'] = @gitconfig.path + SAFE_DIRECTORIES.each do |dir| + system(*%W[#{COMMAND} config --global --add safe.directory #{dir}]) + end + VCS.dump(`#{COMMAND} config --global --get-all safe.directory`, "safe.directory: ") if debug? + end + @gitconfig.path + end + def initialize(*) super @srcdir = File.realpath(@srcdir) - VCS::DEBUG_OUT.puts @srcdir.inspect if debug? + @gitconfig = nil + VCS.dump(@srcdir, "srcdir: ") if debug? self end @@ -721,13 +775,13 @@ def upstream def commit(opts = {}) args = [COMMAND, "push"] - args << "-n" if dryrun + args << "-n" if dryrun? remote, branch = upstream args << remote branches = %W[refs/notes/commits:refs/notes/commits HEAD:#{branch}] if dryrun? branches.each do |b| - VCS::DEBUG_OUT.puts((args + [b]).inspect) + VCS.dump(args + [b], "commit: ") end return true end @@ -757,7 +811,7 @@ def commit(opts = {}) commits.each_with_index do |l, i| r, a, c = l.split(' ') dcommit = [COMMAND, "svn", "dcommit"] - dcommit.insert(-2, "-n") if dryrun + dcommit.insert(-2, "-n") if dryrun? dcommit << "--add-author-from" unless a == c dcommit << r system(*dcommit) or return false diff --git a/tool/lib/vpath.rb b/tool/lib/vpath.rb index 48ab148405d5b0..fa819f32423ffe 100644 --- a/tool/lib/vpath.rb +++ b/tool/lib/vpath.rb @@ -53,10 +53,11 @@ def foreach(file, *args, &block) end def def_options(opt) + opt.separator(" VPath common options:") opt.on("-I", "--srcdir=DIR", "add a directory to search path") {|dir| @additional << dir } - opt.on("-L", "--vpath=PATH LIST", "add directories to search path") {|dirs| + opt.on("-L", "--vpath=PATH-LIST", "add directories to search path") {|dirs| @additional << [dirs] } opt.on("--path-separator=SEP", /\A(?:\W\z|\.(\W).+)/, "separator for vpath") {|sep, vsep| @@ -80,6 +81,10 @@ def list @list end + def add(path) + @additional << path + end + def strip(path) prefix = list.map {|dir| Regexp.quote(dir)} path.sub(/\A#{prefix.join('|')}(?:\/|\z)/, '') diff --git a/tool/mkconfig.rb b/tool/mkconfig.rb index 120b90850dc4a2..fb3552240170af 100755 --- a/tool/mkconfig.rb +++ b/tool/mkconfig.rb @@ -124,6 +124,14 @@ if universal platform = val.sub(/universal/, %q[#{arch && universal[/(?:\A|\s)#{Regexp.quote(arch)}=(\S+)/, 1] || RUBY_PLATFORM[/\A[^-]*/]}]) end + when /^target_cpu$/ + if universal + val = 'arch' + end + when /^target$/ + val = '"$(target_cpu)-$(target_vendor)-$(target_os)"' + when /^host(?:_(?:os|vendor|cpu|alias))?$/ + val = %["$(#{name.sub(/^host/, 'target')})"] when /^includedir$/ val = '"$(SDKROOT)"'+val if /darwin/ =~ arch end diff --git a/tool/sync_default_gems.rb b/tool/sync_default_gems.rb index 5aa7eb06eb440c..d6c7f771d71073 100755 --- a/tool/sync_default_gems.rb +++ b/tool/sync_default_gems.rb @@ -418,11 +418,11 @@ def sync_default_gems(gem) |rakelib\/.* )\z/mx -def message_filter(repo, sha) - log = STDIN.read +def message_filter(repo, sha, input: ARGF) + log = input.read log.delete!("\r") url = "https://github.com/#{repo}" - subject, log = log.split("\n\n", 2) + subject, log = log.split(/\n(?:[\s\t]*(?:\n|\z))/, 2) conv = proc do |s| mod = true if s.gsub!(/\b(?:(?i:fix(?:e[sd])?) +)\K#(?=\d+\b)|\bGH-#?(?=\d+\b)|\(\K#(?=\d+\))/) { "#{url}/pull/" @@ -439,11 +439,15 @@ def message_filter(repo, sha) subject.gsub!(/\G.{,67}[^\s.,][.,]*\K\s+/, "\n") end end - if log + url = "#{url}/commit/#{sha[0,10]}\n" + if log and !log.empty? + log.sub!(/(?<=\n)\n+\z/, '') # drop empty lines at the last conv[log] - log.sub!(/\s*(?=(?i:\nCo-authored-by:.*)*\Z)/) { - "\n\n" "#{url}/commit/#{sha[0,10]}\n" + log.sub!(/(?:(\A\s*)|\s*\n)(?=(?i:Co-authored-by:.*)*\Z)/) { + $~.begin(1) ? "#{url}\n" : "\n\n#{url}" } + else + log = url end print subject, "\n\n", log end @@ -520,7 +524,7 @@ def sync_default_gems_with_commits(gem, ranges, edit: nil) skipped = true elsif /^CONFLICT/ =~ result result = pipe_readlines(%W"git status --porcelain -z") - result.map! {|line| line[/\A.U (.*)/, 1]} + result.map! {|line| line[/\A(?:.U|AA) (.*)/, 1]} result.compact! ignore, conflict = result.partition {|name| IGNORE_FILE_PATTERN =~ name} unless ignore.empty? @@ -656,8 +660,10 @@ def update_default_gems(gem, release: false) end when "--message-filter" ARGV.shift - abort unless ARGV.size == 2 - message_filter(*ARGV) + if ARGV.size < 2 + abort "usage: #{$0} --message-filter repository commit-hash [input...]" + end + message_filter(*ARGV.shift(2)) exit when "rdoc-ref" ARGV.shift diff --git a/variable.c b/variable.c index 4c5ae2c112b7ff..fd0c30b9b53315 100644 --- a/variable.c +++ b/variable.c @@ -111,18 +111,16 @@ rb_namespace_p(VALUE obj) static VALUE classname(VALUE klass, int *permanent) { - st_table *ivtbl; - st_data_t n; - *permanent = 0; if (!RCLASS_EXT(klass)) return Qnil; - if (!(ivtbl = RCLASS_IV_TBL(klass))) return Qnil; - if (st_lookup(ivtbl, (st_data_t)classpath, &n)) { + + VALUE classpathv = rb_ivar_lookup(klass, classpath, Qnil); + if (RTEST(classpathv)) { *permanent = 1; - return (VALUE)n; + return classpathv; } - if (st_lookup(ivtbl, (st_data_t)tmp_classpath, &n)) return (VALUE)n; - return Qnil; + + return rb_ivar_lookup(klass, tmp_classpath, Qnil);; } /* @@ -946,6 +944,8 @@ gen_ivtbl_get_unlocked(VALUE obj, ID id, struct gen_ivtbl **ivtbl) MJIT_FUNC_EXPORTED int rb_gen_ivtbl_get(VALUE obj, ID id, struct gen_ivtbl **ivtbl) { + RUBY_ASSERT(!RB_TYPE_P(obj, T_ICLASS)); + st_data_t data; int r = 0; @@ -1116,54 +1116,6 @@ gen_ivtbl_count(const struct gen_ivtbl *ivtbl) return n; } -static int -lock_st_lookup(st_table *tab, st_data_t key, st_data_t *value) -{ - int r; - RB_VM_LOCK_ENTER(); - { - r = st_lookup(tab, key, value); - } - RB_VM_LOCK_LEAVE(); - return r; -} - -static int -lock_st_delete(st_table *tab, st_data_t *key, st_data_t *value) -{ - int r; - RB_VM_LOCK_ENTER(); - { - r = st_delete(tab, key, value); - } - RB_VM_LOCK_LEAVE(); - return r; -} - -static int -lock_st_is_member(st_table *tab, st_data_t key) -{ - int r; - RB_VM_LOCK_ENTER(); - { - r = st_is_member(tab, key); - } - RB_VM_LOCK_LEAVE(); - return r; -} - -static int -lock_st_insert(st_table *tab, st_data_t key, st_data_t value) -{ - int r; - RB_VM_LOCK_ENTER(); - { - r = st_insert(tab, key, value); - } - RB_VM_LOCK_LEAVE(); - return r; -} - VALUE rb_ivar_lookup(VALUE obj, ID id, VALUE undef) { @@ -1181,21 +1133,39 @@ rb_ivar_lookup(VALUE obj, ID id, VALUE undef) case T_CLASS: case T_MODULE: { - st_data_t val; + bool found; + VALUE val; - if (RCLASS_IV_TBL(obj) && - lock_st_lookup(RCLASS_IV_TBL(obj), (st_data_t)id, &val)) { - if (rb_is_instance_id(id) && - UNLIKELY(!rb_ractor_main_p()) && - !rb_ractor_shareable_p(val)) { - rb_raise(rb_eRactorIsolationError, - "can not get unshareable values from instance variables of classes/modules from non-main Ractors"); + RB_VM_LOCK_ENTER(); + { +#if !SHAPE_IN_BASIC_FLAGS + shape_id = RCLASS_SHAPE_ID(obj); +#endif + + attr_index_t index = 0; + shape = rb_shape_get_shape_by_id(shape_id); + found = rb_shape_get_iv_index(shape, id, &index); + + if (found) { + ivar_list = RCLASS_IVPTR(obj); + RUBY_ASSERT(ivar_list); + + val = ivar_list[index]; + } + else { + val = undef; } - return val; } - else { - return undef; + RB_VM_LOCK_LEAVE(); + + if (found && + rb_is_instance_id(id) && + UNLIKELY(!rb_ractor_main_p()) && + !rb_ractor_shareable_p(val)) { + rb_raise(rb_eRactorIsolationError, + "can not get unshareable values from instance variables of classes/modules from non-main Ractors"); } + return val; } case T_OBJECT: { @@ -1247,19 +1217,25 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) { rb_check_frozen(obj); - VALUE val = Qnil; + VALUE val = undef; attr_index_t index; switch (BUILTIN_TYPE(obj)) { case T_CLASS: case T_MODULE: IVAR_ACCESSOR_SHOULD_BE_MAIN_RACTOR(id); - if (RCLASS_IV_TBL(obj)) { - st_data_t id_data = (st_data_t)id, val; - if (lock_st_delete(RCLASS_IV_TBL(obj), &id_data, &val)) { - return (VALUE)val; + + RB_VM_LOCK_ENTER(); + { + rb_shape_t * shape = rb_shape_get_shape(obj); + if (rb_shape_get_iv_index(shape, id, &index)) { + rb_shape_transition_shape_remove_ivar(obj, id, shape); + val = RCLASS_IVPTR(obj)[index]; + RCLASS_IVPTR(obj)[index] = Qundef; } } + RB_VM_LOCK_LEAVE(); + break; case T_OBJECT: { rb_shape_t * shape = rb_shape_get_shape(obj); @@ -1267,7 +1243,6 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) rb_shape_transition_shape_remove_ivar(obj, id, shape); val = ROBJECT_IVPTR(obj)[index]; ROBJECT_IVPTR(obj)[index] = Qundef; - return val; } break; @@ -1281,14 +1256,13 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) rb_gen_ivtbl_get(obj, id, &ivtbl); val = ivtbl->ivptr[index]; ivtbl->ivptr[index] = Qundef; - return val; } break; } } - return undef; + return val; } VALUE @@ -1553,10 +1527,9 @@ ivar_set(VALUE obj, ID id, VALUE val) } case T_CLASS: case T_MODULE: - // TODO: Transition shapes on classes - //rb_shape_transition_shape(obj, id, rb_shape_get_shape_by_id(RCLASS_SHAPE_ID(obj))); IVAR_ACCESSOR_SHOULD_BE_MAIN_RACTOR(id); rb_class_ivar_set(obj, id, val); + break; default: generic_ivar_set(obj, id, val); @@ -1587,34 +1560,47 @@ rb_ivar_defined(VALUE obj, ID id) attr_index_t index; if (SPECIAL_CONST_P(obj)) return Qfalse; - switch (BUILTIN_TYPE(obj)) { - case T_CLASS: - case T_MODULE: - return RBOOL(RCLASS_IV_TBL(obj) && lock_st_is_member(RCLASS_IV_TBL(obj), (st_data_t)id)); - default: - return RBOOL(rb_shape_get_iv_index(rb_shape_get_shape(obj), id, &index)); - } + return RBOOL(rb_shape_get_iv_index(rb_shape_get_shape(obj), id, &index)); } typedef int rb_ivar_foreach_callback_func(ID key, VALUE val, st_data_t arg); st_data_t rb_st_nth_key(st_table *tab, st_index_t index); +struct iv_itr_data { + VALUE obj; + struct gen_ivtbl * ivtbl; + st_data_t arg; +}; + static void -iterate_over_shapes_with_callback(rb_shape_t *shape, VALUE* iv_list, rb_ivar_foreach_callback_func *callback, st_data_t arg) +iterate_over_shapes_with_callback(rb_shape_t *shape, rb_ivar_foreach_callback_func *callback, struct iv_itr_data * itr_data) { switch ((enum shape_type)shape->type) { case SHAPE_ROOT: return; case SHAPE_IVAR: - iterate_over_shapes_with_callback(rb_shape_get_shape_by_id(shape->parent_id), iv_list, callback, arg); + iterate_over_shapes_with_callback(rb_shape_get_shape_by_id(shape->parent_id), callback, itr_data); + VALUE * iv_list; + switch (BUILTIN_TYPE(itr_data->obj)) { + case T_OBJECT: + iv_list = ROBJECT_IVPTR(itr_data->obj); + break; + case T_CLASS: + case T_MODULE: + iv_list = RCLASS_IVPTR(itr_data->obj); + break; + default: + iv_list = itr_data->ivtbl->ivptr; + break; + } VALUE val = iv_list[shape->next_iv_index - 1]; if (val != Qundef) { - callback(shape->edge_name, val, arg); + callback(shape->edge_name, val, itr_data->arg); } return; case SHAPE_IVAR_UNDEF: case SHAPE_FROZEN: - iterate_over_shapes_with_callback(rb_shape_get_shape_by_id(shape->parent_id), iv_list, callback, arg); + iterate_over_shapes_with_callback(rb_shape_get_shape_by_id(shape->parent_id), callback, itr_data); return; } } @@ -1623,7 +1609,10 @@ static void obj_ivar_each(VALUE obj, rb_ivar_foreach_callback_func *func, st_data_t arg) { rb_shape_t* shape = rb_shape_get_shape(obj); - iterate_over_shapes_with_callback(shape, ROBJECT_IVPTR(obj), func, arg); + struct iv_itr_data itr_data; + itr_data.obj = obj; + itr_data.arg = arg; + iterate_over_shapes_with_callback(shape, func, &itr_data); } static void @@ -1633,7 +1622,23 @@ gen_ivar_each(VALUE obj, rb_ivar_foreach_callback_func *func, st_data_t arg) struct gen_ivtbl *ivtbl; if (!rb_gen_ivtbl_get(obj, 0, &ivtbl)) return; - iterate_over_shapes_with_callback(shape, ivtbl->ivptr, func, arg); + struct iv_itr_data itr_data; + itr_data.obj = obj; + itr_data.ivtbl = ivtbl; + itr_data.arg = arg; + iterate_over_shapes_with_callback(shape, func, &itr_data); +} + +static void +class_ivar_each(VALUE obj, rb_ivar_foreach_callback_func *func, st_data_t arg) +{ + RUBY_ASSERT(RB_TYPE_P(obj, T_CLASS) || RB_TYPE_P(obj, T_MODULE)); + + rb_shape_t* shape = rb_shape_get_shape(obj); + struct iv_itr_data itr_data; + itr_data.obj = obj; + itr_data.arg = arg; + iterate_over_shapes_with_callback(shape, func, &itr_data); } void @@ -1720,13 +1725,11 @@ rb_ivar_foreach(VALUE obj, rb_ivar_foreach_callback_func *func, st_data_t arg) case T_CLASS: case T_MODULE: IVAR_ACCESSOR_SHOULD_BE_MAIN_RACTOR(0); - if (RCLASS_IV_TBL(obj)) { - RB_VM_LOCK_ENTER(); - { - st_foreach_safe(RCLASS_IV_TBL(obj), func, arg); - } - RB_VM_LOCK_LEAVE(); + RB_VM_LOCK_ENTER(); + { + class_ivar_each(obj, func, arg); } + RB_VM_LOCK_LEAVE(); break; default: if (FL_TEST(obj, FL_EXIVAR)) { @@ -1739,8 +1742,6 @@ rb_ivar_foreach(VALUE obj, rb_ivar_foreach_callback_func *func, st_data_t arg) st_index_t rb_ivar_count(VALUE obj) { - st_table *tbl; - if (SPECIAL_CONST_P(obj)) return 0; switch (BUILTIN_TYPE(obj)) { @@ -1758,8 +1759,22 @@ rb_ivar_count(VALUE obj) break; case T_CLASS: case T_MODULE: - if ((tbl = RCLASS_IV_TBL(obj)) != 0) { - return tbl->num_entries; + if (rb_shape_get_shape(obj)->next_iv_index > 0) { + st_index_t count = 0; + + RB_VM_LOCK_ENTER(); + { + st_index_t i, num = rb_shape_get_shape(obj)->next_iv_index; + const VALUE *const ivptr = RCLASS_IVPTR(obj); + for (i = count = 0; i < num; ++i) { + if (ivptr[i] != Qundef) { + count++; + } + } + } + RB_VM_LOCK_LEAVE(); + + return count; } break; default: @@ -1879,11 +1894,12 @@ rb_obj_remove_instance_variable(VALUE obj, VALUE name) case T_CLASS: case T_MODULE: IVAR_ACCESSOR_SHOULD_BE_MAIN_RACTOR(id); - if (RCLASS_IV_TBL(obj)) { - st_data_t id_data = (st_data_t)id, val; - if (lock_st_delete(RCLASS_IV_TBL(obj), &id_data, &val)) { - return (VALUE)val; - } + rb_shape_t * shape = rb_shape_get_shape(obj); + if (rb_shape_get_iv_index(shape, id, &index)) { + rb_shape_transition_shape_remove_ivar(obj, id, shape); + val = RCLASS_IVPTR(obj)[index]; + RCLASS_IVPTR(obj)[index] = Qundef; + return val; } break; case T_OBJECT: { @@ -2030,8 +2046,22 @@ autoload_data(VALUE mod, ID id) struct st_table *tbl; st_data_t val; + // If we are called with a non-origin ICLASS, fetch the autoload data from + // the original module. + if (RB_TYPE_P(mod, T_ICLASS)) { + if (FL_TEST_RAW(mod, RICLASS_IS_ORIGIN)) { + return 0; + } else { + mod = RBASIC(mod)->klass; + } + } + + RUBY_ASSERT(RB_TYPE_P(mod, T_CLASS) || RB_TYPE_P(mod, T_MODULE)); + // Look up the instance variable table for `autoload`, then index into that table with the given constant name `id`. - if (!st_lookup(RCLASS_IV_TBL(mod), autoload, &val) || !(tbl = check_autoload_table((VALUE)val)) || !st_lookup(tbl, (st_data_t)id, &val)) { + + VALUE tbl_value = rb_ivar_lookup(mod, autoload, 0); + if (!tbl_value || !(tbl = check_autoload_table(tbl_value)) || !st_lookup(tbl, (st_data_t)id, &val)) { return 0; } @@ -2229,23 +2259,14 @@ autoload_feature_lookup_or_create(VALUE feature, struct autoload_data **autoload static struct st_table * autoload_table_lookup_or_create(VALUE module) { - // Get or create an autoload table in the class instance variables: - struct st_table *table = RCLASS_IV_TBL(module); - VALUE autoload_table_value; - - if (table && st_lookup(table, (st_data_t)autoload, &autoload_table_value)) { - return check_autoload_table((VALUE)autoload_table_value); + VALUE autoload_table_value = rb_ivar_lookup(module, autoload, 0); + if (autoload_table_value) { + return check_autoload_table(autoload_table_value); + } else { + autoload_table_value = TypedData_Wrap_Struct(0, &autoload_table_type, 0); + rb_class_ivar_set(module, autoload, autoload_table_value); + return (DATA_PTR(autoload_table_value) = st_init_numtable()); } - - if (!table) { - table = RCLASS_IV_TBL(module) = st_init_numtable(); - } - - autoload_table_value = TypedData_Wrap_Struct(0, &autoload_table_type, 0); - st_add_direct(table, (st_data_t)autoload, (st_data_t)autoload_table_value); - - RB_OBJ_WRITTEN(module, Qnil, autoload_table_value); - return (DATA_PTR(autoload_table_value) = st_init_numtable()); } static VALUE @@ -2314,10 +2335,13 @@ autoload_delete(VALUE module, ID name) { RUBY_ASSERT_CRITICAL_SECTION_ENTER(); - st_data_t value, load = 0, key = name; + st_data_t load = 0, key = name; + + RUBY_ASSERT(RB_TYPE_P(module, T_CLASS) || RB_TYPE_P(module, T_MODULE)); - if (st_lookup(RCLASS_IV_TBL(module), (st_data_t)autoload, &value)) { - struct st_table *table = check_autoload_table((VALUE)value); + VALUE table_value = rb_ivar_lookup(module, autoload, 0); + if (table_value) { + struct st_table *table = check_autoload_table(table_value); st_delete(table, &key, &load); @@ -2342,8 +2366,7 @@ autoload_delete(VALUE module, ID name) // If the autoload table is empty, we can delete it. if (table->num_entries == 0) { - name = autoload; - st_delete(RCLASS_IV_TBL(module), &name, &value); + rb_attr_delete(module, autoload); } } } @@ -3129,10 +3152,7 @@ set_namespace_path_i(ID id, VALUE v, void *payload) return ID_TABLE_CONTINUE; } set_namespace_path(value, build_const_path(parental_path, id)); - if (RCLASS_IV_TBL(value)) { - st_data_t tmp = tmp_classpath; - st_delete(RCLASS_IV_TBL(value), &tmp, 0); - } + rb_attr_delete(value, tmp_classpath); return ID_TABLE_CONTINUE; } @@ -3469,8 +3489,20 @@ original_module(VALUE c) static int cvar_lookup_at(VALUE klass, ID id, st_data_t *v) { - if (!RCLASS_IV_TBL(klass)) return 0; - return st_lookup(RCLASS_IV_TBL(klass), (st_data_t)id, v); + if (RB_TYPE_P(klass, T_ICLASS)) { + if (FL_TEST_RAW(klass, RICLASS_IS_ORIGIN)) { + return 0; + } else { + // check the original module + klass = RBASIC(klass)->klass; + } + } + + VALUE n = rb_ivar_lookup(klass, id, Qundef); + if (n == Qundef) return 0; + + if (v) *v = n; + return 1; } static VALUE @@ -3489,8 +3521,6 @@ static void cvar_overtaken(VALUE front, VALUE target, ID id) { if (front && target != front) { - st_data_t did = (st_data_t)id; - if (original_module(front) != original_module(target)) { rb_raise(rb_eRuntimeError, "class variable % "PRIsVALUE" of %"PRIsVALUE" is overtaken by %"PRIsVALUE"", @@ -3498,7 +3528,7 @@ cvar_overtaken(VALUE front, VALUE target, ID id) rb_class_name(original_module(target))); } if (BUILTIN_TYPE(front) == T_CLASS) { - st_delete(RCLASS_IV_TBL(front), &did, 0); + rb_ivar_delete(front, id, Qundef); } } } @@ -3543,9 +3573,8 @@ find_cvar(VALUE klass, VALUE * front, VALUE * target, ID id) static void check_for_cvar_table(VALUE subclass, VALUE key) { - st_table *tbl = RCLASS_IV_TBL(subclass); - - if (tbl && st_lookup(tbl, key, NULL)) { + // Must not check ivar on ICLASS + if (!RB_TYPE_P(subclass, T_ICLASS) && RTEST(rb_ivar_defined(subclass, key))) { RB_DEBUG_COUNTER_INC(cvar_class_invalidate); ruby_vm_global_cvar_state++; return; @@ -3688,9 +3717,9 @@ mod_cvar_at(VALUE mod, void *data) if (!tbl) { tbl = st_init_numtable(); } - if (RCLASS_IV_TBL(mod)) { - st_foreach_safe(RCLASS_IV_TBL(mod), cv_i, (st_data_t)tbl); - } + mod = original_module(mod); + + rb_ivar_foreach(mod, cv_i, (st_data_t)tbl); return tbl; } @@ -3793,13 +3822,14 @@ VALUE rb_mod_remove_cvar(VALUE mod, VALUE name) { const ID id = id_for_var_message(mod, name, class, "wrong class variable name %1$s"); - st_data_t val, n = id; + st_data_t val; if (!id) { goto not_defined; } rb_check_frozen(mod); - if (RCLASS_IV_TBL(mod) && st_delete(RCLASS_IV_TBL(mod), &n, &val)) { + val = rb_ivar_delete(mod, id, Qundef); + if (val != Qundef) { return (VALUE)val; } if (rb_cvar_defined(mod, id)) { @@ -3834,30 +3864,63 @@ rb_iv_set(VALUE obj, const char *name, VALUE val) int rb_class_ivar_set(VALUE obj, ID key, VALUE value) { - if (!RCLASS_IV_TBL(obj)) { - RCLASS_IV_TBL(obj) = st_init_numtable(); + RUBY_ASSERT(RB_TYPE_P(obj, T_CLASS) || RB_TYPE_P(obj, T_MODULE)); + int found; + + RB_VM_LOCK_ENTER(); + { + rb_shape_t * shape = rb_shape_get_shape(obj); + attr_index_t idx; + found = rb_shape_get_iv_index(shape, key, &idx); + + if (found) { + // Changing an existing instance variable + RUBY_ASSERT(RCLASS_IVPTR(obj)); + + RCLASS_IVPTR(obj)[idx] = value; + RB_OBJ_WRITTEN(obj, Qundef, value); + } else { + // Creating and setting a new instance variable + + // Move to a shape which fits the new ivar + idx = shape->next_iv_index; + shape = rb_shape_get_next(shape, obj, key); + + // We always allocate a power of two sized IV array. This way we + // only need to realloc when we expand into a new power of two size + if ((idx & (idx - 1)) == 0) { + size_t newsize = idx ? idx * 2 : 1; + REALLOC_N(RCLASS_IVPTR(obj), VALUE, newsize); + } + + RUBY_ASSERT(RCLASS_IVPTR(obj)); + + RB_OBJ_WRITE(obj, &RCLASS_IVPTR(obj)[idx], value); + rb_shape_set_shape(obj, shape); + } } + RB_VM_LOCK_LEAVE(); - st_table *tbl = RCLASS_IV_TBL(obj); - int result = lock_st_insert(tbl, (st_data_t)key, (st_data_t)value); - RB_OBJ_WRITTEN(obj, Qundef, value); - return result; + return found; } static int -tbl_copy_i(st_data_t key, st_data_t value, st_data_t data) -{ - RB_OBJ_WRITTEN((VALUE)data, Qundef, (VALUE)value); +tbl_copy_i(st_data_t key, st_data_t val, st_data_t dest) { + rb_class_ivar_set(dest, key, val); + return ST_CONTINUE; } void rb_iv_tbl_copy(VALUE dst, VALUE src) { - st_table *orig_tbl = RCLASS_IV_TBL(src); - st_table *new_tbl = st_copy(orig_tbl); - st_foreach(new_tbl, tbl_copy_i, (st_data_t)dst); - RCLASS_IV_TBL(dst) = new_tbl; + RUBY_ASSERT(rb_type(dst) == rb_type(src)); + RUBY_ASSERT(RB_TYPE_P(dst, T_CLASS) || RB_TYPE_P(dst, T_MODULE)); + + RUBY_ASSERT(RCLASS_SHAPE_ID(dst) == ROOT_SHAPE_ID); + RUBY_ASSERT(!RCLASS_IVPTR(dst)); + + rb_ivar_foreach(src, tbl_copy_i, dst); } MJIT_FUNC_EXPORTED rb_const_entry_t * diff --git a/vm.c b/vm.c index 2559790319069d..06c34f949bdf5a 100644 --- a/vm.c +++ b/vm.c @@ -4040,7 +4040,7 @@ Init_vm_objects(void) vm->loading_table = st_init_strtable(); vm->frozen_strings = st_init_table_with_size(&rb_fstring_hash_type, 10000); -#if HAVE_MMAP +#ifdef HAVE_MMAP vm->shape_list = (rb_shape_t *)mmap(NULL, rb_size_mul_or_raise(SHAPE_BITMAP_SIZE * 32, sizeof(rb_shape_t), rb_eRuntimeError), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (vm->shape_list == MAP_FAILED) { diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 2434c5c3c64738..cff4b9138a6ead 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -1169,7 +1169,23 @@ vm_getivar(VALUE obj, ID id, const rb_iseq_t *iseq, IVC ic, const struct rb_call case T_CLASS: case T_MODULE: { - goto general_path; + if (UNLIKELY(!rb_ractor_main_p())) { + // For two reasons we can only use the fast path on the main + // ractor. + // First, only the main ractor is allowed to set ivars on classes + // and modules. So we can skip locking. + // Second, other ractors need to check the shareability of the + // values returned from the class ivars. + goto general_path; + } + + ivar_list = RCLASS_IVPTR(obj); + +#if !SHAPE_IN_BASIC_FLAGS + shape_id = RCLASS_SHAPE_ID(obj); +#endif + + break; } default: if (FL_TEST_RAW(obj, FL_EXIVAR)) { @@ -1507,15 +1523,13 @@ vm_getclassvariable(const rb_iseq_t *iseq, const rb_control_frame_t *reg_cfp, ID { const rb_cref_t *cref; - if (ic->entry && ic->entry->global_cvar_state == GET_GLOBAL_CVAR_STATE()) { - VALUE v = Qundef; + if (ic->entry && ic->entry->global_cvar_state == GET_GLOBAL_CVAR_STATE() && LIKELY(rb_ractor_main_p())) { RB_DEBUG_COUNTER_INC(cvar_read_inline_hit); - if (st_lookup(RCLASS_IV_TBL(ic->entry->class_value), (st_data_t)id, &v) && - LIKELY(rb_ractor_main_p())) { + VALUE v = rb_ivar_lookup(ic->entry->class_value, id, Qundef); + RUBY_ASSERT(v != Qundef); - return v; - } + return v; } cref = vm_get_cref(GET_EP()); diff --git a/wasm/machine.c b/wasm/machine.c index 238041f93e3c2b..2ca84625029a05 100644 --- a/wasm/machine.c +++ b/wasm/machine.c @@ -49,10 +49,10 @@ rb_wasm_record_stack_base(void) return 0; } -void -_rb_wasm_scan_stack(rb_wasm_scan_func scan, void *current) +void * +rb_wasm_stack_get_base(void) { - scan(current, rb_wasm_stack_base); + return rb_wasm_stack_base; } void * diff --git a/wasm/machine.h b/wasm/machine.h index 4cf7228684cc2f..1a60e51d11efe0 100644 --- a/wasm/machine.h +++ b/wasm/machine.h @@ -8,9 +8,8 @@ typedef void (*rb_wasm_scan_func)(void*, void*); // Used by conservative GC void rb_wasm_scan_locals(rb_wasm_scan_func scan); -// Scan userland C-stack memory space in WebAssembly. Used by conservative GC -#define rb_wasm_scan_stack(scan) _rb_wasm_scan_stack((scan), rb_wasm_get_stack_pointer()) -void _rb_wasm_scan_stack(rb_wasm_scan_func scan, void *current); +// Get base address of userland C-stack memory space in WebAssembly. Used by conservative GC +void *rb_wasm_stack_get_base(void); // Get the current stack pointer diff --git a/win32/dir.h b/win32/dir.h index de12f79158d2b0..0292c27c9c6c2b 100644 --- a/win32/dir.h +++ b/win32/dir.h @@ -1,7 +1,7 @@ #ifndef RUBY_WIN32_DIR_H #define RUBY_WIN32_DIR_H #include /* for uint8_t */ -#include /* for WCHAR */ +#include /* for WCHAR */ #include "ruby/encoding.h" /* for rb_encoding */ #define DT_UNKNOWN 0 diff --git a/yjit.c b/yjit.c index f6e64aad65552f..b943277d61167b 100644 --- a/yjit.c +++ b/yjit.c @@ -27,6 +27,7 @@ #include "probes_helper.h" #include "iseq.h" #include "ruby/debug.h" +#include "internal/cont.h" // For mmapp(), sysconf() #ifndef _WIN32 @@ -65,10 +66,7 @@ STATIC_ASSERT(pointer_tagging_scheme, USE_FLONUM); bool rb_yjit_mark_writable(void *mem_block, uint32_t mem_size) { - if (mprotect(mem_block, mem_size, PROT_READ | PROT_WRITE)) { - return false; - } - return true; + return mprotect(mem_block, mem_size, PROT_READ | PROT_WRITE) == 0; } void @@ -85,6 +83,20 @@ rb_yjit_mark_executable(void *mem_block, uint32_t mem_size) } } +// Free the specified memory block. +bool +rb_yjit_mark_unused(void *mem_block, uint32_t mem_size) +{ + // On Linux, you need to use madvise MADV_DONTNEED to free memory. + // We might not need to call this on macOS, but it's not really documented. + // We generally prefer to do the same thing on both to ease testing too. + madvise(mem_block, mem_size, MADV_DONTNEED); + + // On macOS, mprotect PROT_NONE seems to reduce RSS. + // We also call this on Linux to avoid executing unused pages. + return mprotect(mem_block, mem_size, PROT_NONE) == 0; +} + // `start` is inclusive and `end` is exclusive. void rb_yjit_icache_invalidate(void *start, void *end) @@ -295,6 +307,10 @@ rb_yjit_reserve_addr_space(uint32_t mem_size) // Check that the memory mapping was successful if (mem_block == MAP_FAILED) { perror("ruby: yjit: mmap:"); + if(errno == ENOMEM) { + // No crash report if it's only insufficient memory + exit(EXIT_FAILURE); + } rb_bug("mmap failed"); } @@ -387,6 +403,9 @@ rb_iseq_reset_jit_func(const rb_iseq_t *iseq) { RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq)); iseq->body->jit_func = NULL; + // Enable re-compiling this ISEQ. Event when it's invalidated for TracePoint, + // we'd like to re-compile ISEQs that haven't been converted to trace_* insns. + iseq->body->total_calls = 0; } // Get the PC for a given index in an iseq @@ -577,6 +596,12 @@ rb_get_iseq_body_local_iseq(const rb_iseq_t *iseq) return iseq->body->local_iseq; } +const rb_iseq_t * +rb_get_iseq_body_parent_iseq(const rb_iseq_t *iseq) +{ + return iseq->body->parent_iseq; +} + unsigned int rb_get_iseq_body_local_table_size(const rb_iseq_t *iseq) { @@ -601,6 +626,12 @@ rb_get_iseq_body_stack_max(const rb_iseq_t *iseq) return iseq->body->stack_max; } +bool +rb_get_iseq_flags_has_lead(const rb_iseq_t *iseq) +{ + return iseq->body->param.flags.has_lead; +} + bool rb_get_iseq_flags_has_opt(const rb_iseq_t *iseq) { @@ -644,7 +675,13 @@ rb_get_iseq_flags_has_block(const rb_iseq_t *iseq) } bool -rb_get_iseq_flags_has_accepts_no_kwarg(const rb_iseq_t *iseq) +rb_get_iseq_flags_ambiguous_param0(const rb_iseq_t *iseq) +{ + return iseq->body->param.flags.ambiguous_param0; +} + +bool +rb_get_iseq_flags_accepts_no_kwarg(const rb_iseq_t *iseq) { return iseq->body->param.flags.accepts_no_kwarg; } @@ -1032,6 +1069,7 @@ VALUE rb_yjit_get_stats(rb_execution_context_t *ec, VALUE self); VALUE rb_yjit_reset_stats_bang(rb_execution_context_t *ec, VALUE self); VALUE rb_yjit_disasm_iseq(rb_execution_context_t *ec, VALUE self, VALUE iseq); VALUE rb_yjit_insns_compiled(rb_execution_context_t *ec, VALUE self, VALUE iseq); +VALUE rb_yjit_code_gc(rb_execution_context_t *ec, VALUE self); VALUE rb_yjit_simulate_oom_bang(rb_execution_context_t *ec, VALUE self); VALUE rb_yjit_get_exit_locations(rb_execution_context_t *ec, VALUE self); diff --git a/yjit.rb b/yjit.rb index b80861dbfbb6fb..71cbaf7edb96fb 100644 --- a/yjit.rb +++ b/yjit.rb @@ -162,6 +162,11 @@ def self.insns_compiled(iseq) end end + # Free and recompile all existing JIT code + def self.code_gc + Primitive.rb_yjit_code_gc + end + def self.simulate_oom! Primitive.rb_yjit_simulate_oom_bang end @@ -182,6 +187,7 @@ def _print_stats $stderr.puts("***YJIT: Printing YJIT statistics on exit***") print_counters(stats, prefix: 'send_', prompt: 'method call exit reasons: ') + print_counters(stats, prefix: 'invokeblock_', prompt: 'invokeblock exit reasons: ') print_counters(stats, prefix: 'invokesuper_', prompt: 'invokesuper exit reasons: ') print_counters(stats, prefix: 'leave_', prompt: 'leave exit reasons: ') print_counters(stats, prefix: 'gbpp_', prompt: 'getblockparamproxy exit reasons: ') @@ -212,15 +218,20 @@ def _print_stats $stderr.puts "bindings_allocations: " + ("%10d" % stats[:binding_allocations]) $stderr.puts "bindings_set: " + ("%10d" % stats[:binding_set]) $stderr.puts "compilation_failure: " + ("%10d" % compilation_failure) if compilation_failure != 0 - $stderr.puts "compiled_iseq_count: " + ("%10d" % stats[:compiled_iseq_count]) $stderr.puts "compiled_block_count: " + ("%10d" % stats[:compiled_block_count]) + $stderr.puts "compiled_iseq_count: " + ("%10d" % stats[:compiled_iseq_count]) $stderr.puts "freed_iseq_count: " + ("%10d" % stats[:freed_iseq_count]) $stderr.puts "invalidation_count: " + ("%10d" % stats[:invalidation_count]) $stderr.puts "constant_state_bumps: " + ("%10d" % stats[:constant_state_bumps]) $stderr.puts "inline_code_size: " + ("%10d" % stats[:inline_code_size]) $stderr.puts "outlined_code_size: " + ("%10d" % stats[:outlined_code_size]) + $stderr.puts "freed_code_size: " + ("%10d" % stats[:freed_code_size]) + $stderr.puts "live_page_count: " + ("%10d" % stats[:live_page_count]) + $stderr.puts "freed_page_count: " + ("%10d" % stats[:freed_page_count]) + $stderr.puts "code_gc_count: " + ("%10d" % stats[:code_gc_count]) $stderr.puts "num_gc_obj_refs: " + ("%10d" % stats[:num_gc_obj_refs]) + $stderr.puts "side_exit_count: " + ("%10d" % side_exits) $stderr.puts "total_exit_count: " + ("%10d" % total_exits) $stderr.puts "total_insns_count: " + ("%10d" % total_insns_count) $stderr.puts "vm_insns_count: " + ("%10d" % stats[:vm_insns_count]) diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index f7ebb885777118..167ab2a74f20d1 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -250,6 +250,7 @@ fn main() { .blocklist_type("rb_control_frame_struct") .opaque_type("rb_control_frame_struct") .allowlist_function("rb_vm_bh_to_procval") + .allowlist_function("rb_vm_ep_local_ep") .allowlist_type("vm_special_object_type") .allowlist_var("VM_ENV_DATA_INDEX_SPECVAL") .allowlist_var("VM_ENV_DATA_INDEX_FLAGS") @@ -263,6 +264,7 @@ fn main() { .allowlist_function("rb_yjit_reserve_addr_space") .allowlist_function("rb_yjit_mark_writable") .allowlist_function("rb_yjit_mark_executable") + .allowlist_function("rb_yjit_mark_unused") .allowlist_function("rb_yjit_get_page_size") .allowlist_function("rb_leaf_invokebuiltin_iseq_p") .allowlist_function("rb_leaf_builtin_function") @@ -297,6 +299,9 @@ fn main() { // From internal/compile.h .allowlist_function("rb_vm_insn_decode") + // from internal/cont.h + .allowlist_function("rb_jit_cont_each_iseq") + // From iseq.h .allowlist_function("rb_vm_insn_addr2opcode") .allowlist_function("rb_iseqw_to_iseq") @@ -346,15 +351,18 @@ fn main() { .allowlist_function("rb_get_def_bmethod_proc") .allowlist_function("rb_iseq_encoded_size") .allowlist_function("rb_get_iseq_body_local_iseq") + .allowlist_function("rb_get_iseq_body_parent_iseq") .allowlist_function("rb_get_iseq_body_iseq_encoded") .allowlist_function("rb_get_iseq_body_stack_max") + .allowlist_function("rb_get_iseq_flags_has_lead") .allowlist_function("rb_get_iseq_flags_has_opt") .allowlist_function("rb_get_iseq_flags_has_kw") .allowlist_function("rb_get_iseq_flags_has_rest") .allowlist_function("rb_get_iseq_flags_has_post") .allowlist_function("rb_get_iseq_flags_has_kwrest") .allowlist_function("rb_get_iseq_flags_has_block") - .allowlist_function("rb_get_iseq_flags_has_accepts_no_kwarg") + .allowlist_function("rb_get_iseq_flags_ambiguous_param0") + .allowlist_function("rb_get_iseq_flags_accepts_no_kwarg") .allowlist_function("rb_get_iseq_flags_ruby2_keywords") .allowlist_function("rb_get_iseq_body_local_table_size") .allowlist_function("rb_get_iseq_body_param_keyword") diff --git a/yjit/src/asm/mod.rs b/yjit/src/asm/mod.rs index ab7c4d6abab13e..3d7de7cd79bed8 100644 --- a/yjit/src/asm/mod.rs +++ b/yjit/src/asm/mod.rs @@ -6,6 +6,10 @@ use std::rc::Rc; use crate::backend::x86_64::JMP_PTR_BYTES; #[cfg(target_arch = "aarch64")] use crate::backend::arm64::JMP_PTR_BYTES; +use crate::core::IseqPayload; +use crate::core::for_each_off_stack_iseq_payload; +use crate::core::for_each_on_stack_iseq_payload; +use crate::invariants::rb_yjit_tracing_invalidate_all; use crate::virtualmem::WriteError; #[cfg(feature = "disasm")] @@ -115,17 +119,23 @@ impl CodeBlock { pub fn next_page(&mut self, base_ptr: CodePtr, jmp_ptr: F) -> bool { let old_write_ptr = self.get_write_ptr(); self.set_write_ptr(base_ptr); - self.without_page_end_reserve(|cb| assert!(cb.has_capacity(JMP_PTR_BYTES))); + + // Use the freed_pages list if code GC has been used. Otherwise use the next page. + let next_page_idx = if let Some(freed_pages) = CodegenGlobals::get_freed_pages() { + let current_page = self.write_pos / self.page_size; + freed_pages.iter().find(|&&page| current_page < page).map(|&page| page) + } else { + Some(self.write_pos / self.page_size + 1) + }; // Move self to the next page - let next_page_idx = self.write_pos / self.page_size + 1; - if !self.set_page(next_page_idx, &jmp_ptr) { + if next_page_idx.is_none() || !self.set_page(next_page_idx.unwrap(), &jmp_ptr) { self.set_write_ptr(old_write_ptr); // rollback if there are no more pages return false; } // Move the other CodeBlock to the same page if it'S on the furthest page - self.other_cb().unwrap().set_page(next_page_idx, &jmp_ptr); + self.other_cb().unwrap().set_page(next_page_idx.unwrap(), &jmp_ptr); return !self.dropped_bytes; } @@ -151,7 +161,7 @@ impl CodeBlock { // We could remember the last write_pos in page2 and let set_page use that position, // but you need to waste some space for keeping write_pos for every single page. // It doesn't seem necessary for performance either. So we're currently not doing it. - let dst_pos = self.page_size * page_idx + self.page_start(); + let dst_pos = self.get_page_pos(page_idx); if self.page_size * page_idx < self.mem_size && self.write_pos < dst_pos { // Reset dropped_bytes self.dropped_bytes = false; @@ -161,6 +171,7 @@ impl CodeBlock { self.write_pos = dst_pos; let dst_ptr = self.get_write_ptr(); self.write_pos = src_pos; + self.without_page_end_reserve(|cb| assert!(cb.has_capacity(JMP_PTR_BYTES))); // Generate jmp_ptr from src_pos to dst_pos self.without_page_end_reserve(|cb| { @@ -175,6 +186,61 @@ impl CodeBlock { !self.dropped_bytes } + /// Free the memory pages of given code page indexes + fn free_pages(&mut self, page_idxs: &Vec) { + let mut page_idxs = page_idxs.clone(); + page_idxs.reverse(); // to loop with pop() + + // Group adjacent page indexes and free them in batches to reduce the # of syscalls. + while let Some(page_idx) = page_idxs.pop() { + // Group first adjacent page indexes + let mut batch_idxs = vec![page_idx]; + while page_idxs.last() == Some(&(batch_idxs.last().unwrap() + 1)) { + batch_idxs.push(page_idxs.pop().unwrap()); + } + + // Free the grouped pages at once + let start_ptr = self.mem_block.borrow().start_ptr().add_bytes(page_idx * self.page_size); + let batch_size = self.page_size * batch_idxs.len(); + self.mem_block.borrow_mut().free_bytes(start_ptr, batch_size as u32); + } + } + + pub fn page_size(&self) -> usize { + self.page_size + } + + /// Return the number of code pages that have been mapped by the VirtualMemory. + pub fn num_mapped_pages(&self) -> usize { + let mapped_region_size = self.mem_block.borrow().mapped_region_size(); + // CodeBlock's page size != VirtualMem's page size on Linux, + // so mapped_region_size % self.page_size may not be 0 + ((mapped_region_size - 1) / self.page_size) + 1 + } + + /// Return the number of code pages that have been reserved by the VirtualMemory. + pub fn num_virtual_pages(&self) -> usize { + let virtual_region_size = self.mem_block.borrow().virtual_region_size(); + // CodeBlock's page size != VirtualMem's page size on Linux, + // so mapped_region_size % self.page_size may not be 0 + ((virtual_region_size - 1) / self.page_size) + 1 + } + + /// Return the number of code pages that have been freed and not used yet. + pub fn num_freed_pages(&self) -> usize { + (0..self.num_mapped_pages()).filter(|&page_idx| self.has_freed_page(page_idx)).count() + } + + pub fn has_freed_page(&self, page_idx: usize) -> bool { + CodegenGlobals::get_freed_pages().as_ref().map_or(false, |pages| pages.contains(&page_idx)) && // code GCed + self.write_pos < page_idx * self.page_size // and not written yet + } + + /// Convert a page index to the write_pos for the page start. + fn get_page_pos(&self, page_idx: usize) -> usize { + self.page_size * page_idx + self.page_start() + } + /// write_pos of the current page start pub fn page_start_pos(&self) -> usize { self.get_write_pos() / self.page_size * self.page_size + self.page_start() @@ -216,21 +282,48 @@ impl CodeBlock { /// Return the address ranges of a given address range that this CodeBlock can write. #[cfg(any(feature = "disasm", target_arch = "aarch64"))] pub fn writable_addrs(&self, start_ptr: CodePtr, end_ptr: CodePtr) -> Vec<(usize, usize)> { - let mut addrs = vec![]; - let mut start = start_ptr.into_usize(); + // CodegenGlobals is not initialized when we write initial ocb code + let freed_pages = if CodegenGlobals::has_instance() { + CodegenGlobals::get_freed_pages().as_ref() + } else { + None + }; + let region_start = self.get_ptr(0).into_usize(); let region_end = self.get_ptr(self.get_mem_size()).into_usize(); + let mut start = start_ptr.into_usize(); let end = std::cmp::min(end_ptr.into_usize(), region_end); + + let mut addrs = vec![]; while start < end { - let current_page = region_start + - (start.saturating_sub(region_start) / self.page_size * self.page_size); + let page_idx = start.saturating_sub(region_start) / self.page_size; + let current_page = region_start + (page_idx * self.page_size); let page_end = std::cmp::min(end, current_page + self.page_end()); - addrs.push((start, page_end)); + // If code GC has been used, skip pages that are used by past on-stack code + if freed_pages.map_or(true, |pages| pages.contains(&page_idx)) { + addrs.push((start, page_end)); + } start = current_page + self.page_size + self.page_start(); } addrs } + /// Return the code size that has been used by this CodeBlock. + pub fn code_size(&self) -> usize { + let mut size = 0; + let current_page_idx = self.write_pos / self.page_size; + for page_idx in 0..self.num_mapped_pages() { + if page_idx == current_page_idx { + // Count only actually used bytes for the current page. + size += (self.write_pos % self.page_size).saturating_sub(self.page_start()); + } else if !self.has_freed_page(page_idx) { + // Count an entire range for any non-freed pages that have been used. + size += self.page_end() - self.page_start() + self.page_end_reserve; + } + } + size + } + /// Check if this code block has sufficient remaining capacity pub fn has_capacity(&self, num_bytes: usize) -> bool { let page_offset = self.write_pos % self.page_size; @@ -261,6 +354,11 @@ impl CodeBlock { self.asm_comments.get(&pos) } + pub fn clear_comments(&mut self) { + #[cfg(feature = "disasm")] + self.asm_comments.clear(); + } + pub fn get_mem_size(&self) -> usize { self.mem_size } @@ -293,6 +391,24 @@ impl CodeBlock { self.mem_block.borrow().start_ptr().add_bytes(offset) } + /// Convert an address range to memory page indexes against a num_pages()-sized array. + pub fn addrs_to_pages(&self, start_addr: CodePtr, end_addr: CodePtr) -> Vec { + let mem_start = self.mem_block.borrow().start_ptr().into_usize(); + let mem_end = self.mem_block.borrow().end_ptr().into_usize(); + assert!(mem_start <= start_addr.into_usize()); + assert!(start_addr.into_usize() <= end_addr.into_usize()); + assert!(end_addr.into_usize() <= mem_end); + + // Ignore empty code ranges + if start_addr == end_addr { + return vec![]; + } + + let start_page = (start_addr.into_usize() - mem_start) / self.page_size; + let end_page = (end_addr.into_usize() - mem_start - 1) / self.page_size; + (start_page..=end_page).collect() // TODO: consider returning an iterator + } + /// Get a (possibly dangling) direct pointer to the current write position pub fn get_write_ptr(&self) -> CodePtr { self.get_ptr(self.write_pos) @@ -301,12 +417,11 @@ impl CodeBlock { /// Write a single byte at the current position. pub fn write_byte(&mut self, byte: u8) { let write_ptr = self.get_write_ptr(); - if !self.has_capacity(1) || self.mem_block.borrow_mut().write_byte(write_ptr, byte).is_err() { + if self.has_capacity(1) && self.mem_block.borrow_mut().write_byte(write_ptr, byte).is_ok() { + self.write_pos += 1; + } else { self.dropped_bytes = true; } - - // Always advance write_pos since arm64 PadEntryExit needs this to stop the loop. - self.write_pos += 1; } /// Write multiple bytes starting from the current position. @@ -372,10 +487,11 @@ impl CodeBlock { self.label_refs.push(LabelRef { pos: self.write_pos, label_idx, num_bytes, encode }); // Move past however many bytes the instruction takes up - if !self.has_capacity(num_bytes) { + if self.has_capacity(num_bytes) { + self.write_pos += num_bytes; + } else { self.dropped_bytes = true; // retry emitting the Insn after next_page } - self.write_pos += num_bytes; } // Link internal label references @@ -431,6 +547,65 @@ impl CodeBlock { self.mem_block.borrow_mut().mark_all_executable(); } + /// Code GC. Free code pages that are not on stack and reuse them. + pub fn code_gc(&mut self) { + // The previous code GC failed to free any pages. Give up. + if CodegenGlobals::get_freed_pages() == &Some(vec![]) { + return; + } + + // Check which pages are still in use + let mut pages_in_use = vec![false; self.num_mapped_pages()]; + // For each ISEQ, we currently assume that only code pages used by inline code + // are used by outlined code, so we mark only code pages used by inlined code. + for_each_on_stack_iseq_payload(|iseq_payload| { + for page in &iseq_payload.pages { + pages_in_use[*page] = true; + } + }); + // Outlined code generated by CodegenGlobals::init() should also be kept. + for page in CodegenGlobals::get_ocb_pages() { + pages_in_use[*page] = true; + } + + // Let VirtuamMem free the pages + let mut freed_pages: Vec = pages_in_use.iter().enumerate() + .filter(|&(_, &in_use)| !in_use).map(|(page, _)| page).collect(); + self.free_pages(&freed_pages); + // Avoid accumulating freed pages for future code GC + for_each_off_stack_iseq_payload(|iseq_payload: &mut IseqPayload| { + iseq_payload.pages.clear(); + }); + + // Append virtual pages in case RubyVM::YJIT.code_gc is manually triggered. + let mut virtual_pages: Vec = (self.num_mapped_pages()..self.num_virtual_pages()).collect(); + freed_pages.append(&mut virtual_pages); + + // Invalidate everything to have more compact code after code GC. + // This currently patches every ISEQ, which works, but in the future, + // we could limit that to patch only on-stack ISEQs for optimizing code GC. + rb_yjit_tracing_invalidate_all(); + // When code GC runs next time, we could have reused pages in between + // invalidated pages. To invalidate them, we skip freezing them here. + // We free or not reuse the bytes frozen by any past invalidation, so this + // can be safely reset to pass the frozen bytes check on invalidation. + CodegenGlobals::set_inline_frozen_bytes(0); + + if let Some(&first_page) = freed_pages.first() { + let mut cb = CodegenGlobals::get_inline_cb(); + cb.write_pos = cb.get_page_pos(first_page); + cb.dropped_bytes = false; + cb.clear_comments(); + + let mut ocb = CodegenGlobals::get_outlined_cb().unwrap(); + ocb.write_pos = ocb.get_page_pos(first_page); + ocb.dropped_bytes = false; + ocb.clear_comments(); + } + + CodegenGlobals::set_freed_pages(freed_pages); + } + pub fn inline(&self) -> bool { !self.outlined } diff --git a/yjit/src/backend/arm64/mod.rs b/yjit/src/backend/arm64/mod.rs index 7aeb1435d2518e..c899f6871f9d1e 100644 --- a/yjit/src/backend/arm64/mod.rs +++ b/yjit/src/backend/arm64/mod.rs @@ -165,8 +165,8 @@ impl Assembler Opnd::Reg(_) | Opnd::InsnOut { .. } => opnd, Opnd::Mem(_) => split_load_operand(asm, opnd), Opnd::Imm(imm) => { - if imm <= 0 { - asm.load(opnd) + if imm == 0 { + Opnd::Reg(XZR_REG) } else if (dest_num_bits == 64 && BitmaskImmediate::try_from(imm as u64).is_ok()) || (dest_num_bits == 32 && @@ -708,6 +708,23 @@ impl Assembler } } + /// Call emit_jmp_ptr and immediately invalidate the written range. + /// This is needed when next_page also moves other_cb that is not invalidated + /// by compile_with_regs. Doing it here allows you to avoid invalidating a lot + /// more than necessary when other_cb jumps from a position early in the page. + /// This invalidates a small range of cb twice, but we accept the small cost. + fn emit_jmp_ptr_with_invalidation(cb: &mut CodeBlock, dst_ptr: CodePtr) { + #[cfg(not(test))] + let start = cb.get_write_ptr(); + emit_jmp_ptr(cb, dst_ptr); + #[cfg(not(test))] + { + let end = cb.get_write_ptr(); + use crate::cruby::rb_yjit_icache_invalidate; + unsafe { rb_yjit_icache_invalidate(start.raw_ptr() as _, end.raw_ptr() as _) }; + } + } + // dbg!(&self.insns); // List of GC offsets @@ -1011,14 +1028,14 @@ impl Assembler } Insn::LiveReg { .. } => (), // just a reg alloc signal, no code Insn::PadInvalPatch => { - while (cb.get_write_pos().saturating_sub(std::cmp::max(start_write_pos, cb.page_start_pos()))) < JMP_PTR_BYTES { + while (cb.get_write_pos().saturating_sub(std::cmp::max(start_write_pos, cb.page_start_pos()))) < JMP_PTR_BYTES && !cb.has_dropped_bytes() { nop(cb); } } }; // On failure, jump to the next page and retry the current insn - if !had_dropped_bytes && cb.has_dropped_bytes() && cb.next_page(src_ptr, emit_jmp_ptr) { + if !had_dropped_bytes && cb.has_dropped_bytes() && cb.next_page(src_ptr, emit_jmp_ptr_with_invalidation) { // Reset cb states before retrying the current Insn cb.set_label_state(old_label_state); } else { @@ -1335,8 +1352,8 @@ mod tests { asm.test(Opnd::Reg(X0_REG), Opnd::Imm(-7)); asm.compile_with_num_regs(&mut cb, 1); - // Assert that a load and a test instruction were written. - assert_eq!(8, cb.get_write_pos()); + // Assert that a test instruction is written. + assert_eq!(4, cb.get_write_pos()); } #[test] diff --git a/yjit/src/backend/ir.rs b/yjit/src/backend/ir.rs index 64e5805d9e0762..0aa087630d5d42 100644 --- a/yjit/src/backend/ir.rs +++ b/yjit/src/backend/ir.rs @@ -932,15 +932,14 @@ impl Assembler // Mutate the pool bitmap to indicate that the register at that index // has been allocated and is live. - fn alloc_reg(pool: &mut u32, regs: &Vec) -> Reg { + fn alloc_reg(pool: &mut u32, regs: &Vec) -> Option { for (index, reg) in regs.iter().enumerate() { if (*pool & (1 << index)) == 0 { *pool |= 1 << index; - return *reg; + return Some(*reg); } } - - unreachable!("Register spill not supported"); + None } // Allocate a specific register @@ -966,6 +965,29 @@ impl Assembler } } + // Dump live registers for register spill debugging. + fn dump_live_regs(insns: Vec, live_ranges: Vec, num_regs: usize, spill_index: usize) { + // Convert live_ranges to live_regs: the number of live registers at each index + let mut live_regs: Vec = vec![]; + let mut end_idxs: Vec = vec![]; + for (cur_idx, &end_idx) in live_ranges.iter().enumerate() { + end_idxs.push(end_idx); + while let Some(end_idx) = end_idxs.iter().position(|&end_idx| cur_idx == end_idx) { + end_idxs.remove(end_idx); + } + live_regs.push(end_idxs.len()); + } + + // Dump insns along with live registers + for (insn_idx, insn) in insns.iter().enumerate() { + print!("{:3} ", if spill_index == insn_idx { "==>" } else { "" }); + for reg in 0..=num_regs { + print!("{:1}", if reg < live_regs[insn_idx] { "|" } else { "" }); + } + println!(" [{:3}] {:?}", insn_idx, insn); + } + } + let live_ranges: Vec = take(&mut self.live_ranges); let mut asm = Assembler::new_with_label_names(take(&mut self.label_names)); let mut iterator = self.into_draining_iter(); @@ -1050,8 +1072,17 @@ impl Assembler let reg = opnd.unwrap_reg(); Some(take_reg(&mut pool, ®s, ®)) }, - _ => { - Some(alloc_reg(&mut pool, ®s)) + _ => match alloc_reg(&mut pool, ®s) { + Some(reg) => Some(reg), + None => { + let mut insns = asm.insns; + insns.push(insn); + for insn in iterator.insns { + insns.push(insn); + } + dump_live_regs(insns, live_ranges, regs.len(), index); + unreachable!("Register spill not supported"); + } } }; } diff --git a/yjit/src/backend/x86_64/mod.rs b/yjit/src/backend/x86_64/mod.rs index ac5ac0fff4aabd..dc5f21221da048 100644 --- a/yjit/src/backend/x86_64/mod.rs +++ b/yjit/src/backend/x86_64/mod.rs @@ -93,6 +93,7 @@ impl Assembler vec![ RAX_REG, RCX_REG, + RDX_REG, ] } diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index e5e6e4ec845f0b..46f5ed64d3dd3e 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -643,6 +643,11 @@ pub fn gen_entry_prologue(cb: &mut CodeBlock, iseq: IseqPtr, insn_idx: u32) -> O if cb.has_dropped_bytes() { None } else { + // Mark code pages for code GC + let iseq_payload = get_or_create_iseq_payload(iseq); + for page in cb.addrs_to_pages(code_ptr, cb.get_write_ptr()) { + iseq_payload.pages.insert(page); + } Some(code_ptr) } } @@ -1537,6 +1542,22 @@ fn gen_get_ep(asm: &mut Assembler, level: u32) -> Opnd { ep_opnd } +// Gets the EP of the ISeq of the containing method, or "local level". +// Equivalent of GET_LEP() macro. +fn gen_get_lep(jit: &mut JITState, asm: &mut Assembler) -> Opnd { + // Equivalent of get_lvar_level() in compile.c + fn get_lvar_level(iseq: IseqPtr) -> u32 { + if iseq == unsafe { rb_get_iseq_body_local_iseq(iseq) } { + 0 + } else { + 1 + get_lvar_level(unsafe { rb_get_iseq_body_parent_iseq(iseq) }) + } + } + + let level = get_lvar_level(jit.get_iseq()); + gen_get_ep(asm, level) +} + fn gen_getlocal_generic( ctx: &mut Context, asm: &mut Assembler, @@ -2605,9 +2626,6 @@ fn gen_opt_aref( return EndBlock; } - // Remember the context on entry for adding guard chains - let starting_context = *ctx; - // Specialize base on compile time values let comptime_idx = jit_peek_at_stack(jit, ctx, 0); let comptime_recv = jit_peek_at_stack(jit, ctx, 1); @@ -2620,29 +2638,21 @@ fn gen_opt_aref( return CantCompile; } - // Pop the stack operands - let idx_opnd = ctx.stack_pop(1); - let recv_opnd = ctx.stack_pop(1); - let recv_reg = asm.load(recv_opnd); - - // if (SPECIAL_CONST_P(recv)) { - // Bail if receiver is not a heap object - asm.test(recv_reg, (RUBY_IMMEDIATE_MASK as u64).into()); - asm.jnz(side_exit.into()); - asm.cmp(recv_reg, Qfalse.into()); - asm.je(side_exit.into()); - asm.cmp(recv_reg, Qnil.into()); - asm.je(side_exit.into()); + // Get the stack operands + let idx_opnd = ctx.stack_opnd(0); + let recv_opnd = ctx.stack_opnd(1); - // Bail if recv has a class other than ::Array. + // Guard that the receiver is an ::Array // BOP_AREF check above is only good for ::Array. - asm.cmp(unsafe { rb_cArray }.into(), Opnd::mem(64, recv_reg, RUBY_OFFSET_RBASIC_KLASS)); - jit_chain_guard( - JCC_JNE, + jit_guard_known_klass( jit, - &starting_context, + ctx, asm, ocb, + unsafe { rb_cArray }, + recv_opnd, + StackOpnd(1), + comptime_recv, OPT_AREF_MAX_CHAIN_DEPTH, side_exit, ); @@ -2658,6 +2668,9 @@ fn gen_opt_aref( let idx_reg = asm.rshift(idx_reg, Opnd::UImm(1)); // Convert fixnum to int let val = asm.ccall(rb_ary_entry_internal as *const u8, vec![recv_opnd, idx_reg]); + // Pop the argument and the receiver + ctx.stack_pop(2); + // Push the return value onto the stack let stack_ret = ctx.stack_push(Type::Unknown); asm.mov(stack_ret, val); @@ -3990,9 +4003,15 @@ unsafe extern "C" fn build_kwhash(ci: *const rb_callinfo, sp: *const VALUE) -> V hash } -enum BlockHandler { +// SpecVal is a single value in an iseq invocation's environment on the stack, +// at sp[-2]. Depending on the frame type, it can serve different purposes, +// which are covered here by enum variants. +enum SpecVal { None, - CurrentFrame, + BlockISeq(IseqPtr), + BlockParamProxy, + PrevEP(*const VALUE), + PrevEPOpnd(Opnd), } struct ControlFrame { @@ -4001,8 +4020,7 @@ struct ControlFrame { iseq: Option, pc: Option, frame_type: u32, - block_handler: BlockHandler, - prev_ep: Option<*const VALUE>, + specval: SpecVal, cme: *const rb_callable_method_entry_t, local_size: i32 } @@ -4023,7 +4041,7 @@ struct ControlFrame { // * Stack overflow is not checked (should be done by the caller) // * Interrupts are not checked (should be done by the caller) fn gen_push_frame( - _jit: &mut JITState, + jit: &mut JITState, _ctx: &mut Context, asm: &mut Assembler, set_sp_cfp: bool, // if true CFP and SP will be switched to the callee @@ -4033,17 +4051,6 @@ fn gen_push_frame( let sp = frame.sp; - let num_locals = frame.local_size; - if num_locals > 0 { - asm.comment("initialize locals"); - - // Initialize local variables to Qnil - for i in 0..num_locals { - let offs = (SIZEOF_VALUE as i32) * (i - num_locals - 3); - asm.store(Opnd::mem(64, sp, offs), Qnil.into()); - } - } - asm.comment("push cme, specval, frame type"); // Write method entry at sp[-3] @@ -4055,22 +4062,52 @@ fn gen_push_frame( // Write special value at sp[-2]. It's either a block handler or a pointer to // the outer environment depending on the frame type. // sp[-2] = specval; - let specval: Opnd = match (frame.prev_ep, frame.block_handler) { - (None, BlockHandler::None) => { + let specval: Opnd = match frame.specval { + SpecVal::None => { VM_BLOCK_HANDLER_NONE.into() } - (None, BlockHandler::CurrentFrame) => { + SpecVal::BlockISeq(block_iseq) => { + // Change cfp->block_code in the current frame. See vm_caller_setup_arg_block(). + // VM_CFP_TO_CAPTURED_BLOCK does &cfp->self, rb_captured_block->code.iseq aliases + // with cfp->block_code. + asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_BLOCK_CODE), VALUE::from(block_iseq).into()); + let cfp_self = asm.lea(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SELF)); asm.or(cfp_self, Opnd::Imm(1)) } - (Some(prev_ep), BlockHandler::None) => { + SpecVal::BlockParamProxy => { + let ep_opnd = gen_get_lep(jit, asm); + let block_handler = asm.load( + Opnd::mem(64, ep_opnd, (SIZEOF_VALUE as i32) * (VM_ENV_DATA_INDEX_SPECVAL as i32)) + ); + + asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_BLOCK_CODE), block_handler); + + block_handler + } + SpecVal::PrevEP(prev_ep) => { let tagged_prev_ep = (prev_ep as usize) | 1; VALUE(tagged_prev_ep).into() } - (_, _) => panic!("specval can only be one of prev_ep or block_handler") + SpecVal::PrevEPOpnd(ep_opnd) => { + asm.or(ep_opnd, 1.into()) + }, }; asm.store(Opnd::mem(64, sp, SIZEOF_VALUE_I32 * -2), specval); + // Arm requires another register to load the immediate value of Qnil before storing it. + // So doing this after releasing the register for specval to avoid register spill. + let num_locals = frame.local_size; + if num_locals > 0 { + asm.comment("initialize locals"); + + // Initialize local variables to Qnil + for i in 0..num_locals { + let offs = (SIZEOF_VALUE as i32) * (i - num_locals - 3); + asm.store(Opnd::mem(64, sp, offs), Qnil.into()); + } + } + // Write env flags at sp[-1] // sp[-1] = frame_type; asm.store(Opnd::mem(64, sp, SIZEOF_VALUE_I32 * -1), frame.frame_type.into()); @@ -4213,6 +4250,43 @@ fn gen_send_cfunc( return CantCompile; } + let block_arg = flags & VM_CALL_ARGS_BLOCKARG != 0; + let block_arg_type = if block_arg { + Some(ctx.get_opnd_type(StackOpnd(0))) + } else { + None + }; + + match block_arg_type { + Some(Type::Nil | Type::BlockParamProxy) => { + // We'll handle this later + } + None => { + // Nothing to do + } + _ => { + gen_counter_incr!(asm, send_block_arg); + return CantCompile; + } + } + + match block_arg_type { + Some(Type::Nil) => { + // We have a nil block arg, so let's pop it off the args + ctx.stack_pop(1); + } + Some(Type::BlockParamProxy) => { + // We don't need the actual stack value + ctx.stack_pop(1); + } + None => { + // Nothing to do + } + _ => { + assert!(false); + } + } + // This is a .send call and we need to adjust the stack if flags & VM_CALL_OPT_SEND != 0 { handle_opt_send_shift_stack(asm, argc as i32, ctx); @@ -4224,21 +4298,16 @@ fn gen_send_cfunc( // Store incremented PC into current control frame in case callee raises. jit_save_pc(jit, asm); - if let Some(block_iseq) = block { - // Change cfp->block_code in the current frame. See vm_caller_setup_arg_block(). - // VM_CFP_TO_CAPTURED_BLOCK does &cfp->self, rb_captured_block->code.iseq aliases - // with cfp->block_code. - asm.mov(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_BLOCK_CODE), VALUE::from(block_iseq).into()); - } - // Increment the stack pointer by 3 (in the callee) // sp += 3 let sp = asm.lea(ctx.sp_opnd((SIZEOF_VALUE as isize) * 3)); - let frame_block_handler = if let Some(_block_iseq) = block { - BlockHandler::CurrentFrame + let specval = if block_arg_type == Some(Type::BlockParamProxy) { + SpecVal::BlockParamProxy + } else if let Some(block_iseq) = block { + SpecVal::BlockISeq(block_iseq) } else { - BlockHandler::None + SpecVal::None }; let mut frame_type = VM_FRAME_MAGIC_CFUNC | VM_FRAME_FLAG_CFRAME | VM_ENV_FLAG_LOCAL; @@ -4248,11 +4317,10 @@ fn gen_send_cfunc( gen_push_frame(jit, ctx, asm, false, ControlFrame { frame_type, - block_handler: frame_block_handler, + specval, cme, recv, sp, - prev_ep: None, pc: Some(0), iseq: None, local_size: 0, @@ -4460,7 +4528,7 @@ fn gen_send_bmethod( } let frame_type = VM_FRAME_MAGIC_BLOCK | VM_FRAME_FLAG_BMETHOD | VM_FRAME_FLAG_LAMBDA; - gen_send_iseq(jit, ctx, asm, ocb, iseq, ci, frame_type, Some(capture.ep), cme, block, flags, argc) + gen_send_iseq(jit, ctx, asm, ocb, iseq, ci, frame_type, Some(capture.ep), cme, block, flags, argc, None) } fn gen_send_iseq( @@ -4476,10 +4544,10 @@ fn gen_send_iseq( block: Option, flags: u32, argc: i32, + captured_opnd: Option, ) -> CodegenStatus { let mut argc = argc; - // Create a side-exit to fall back to the interpreter let side_exit = get_side_exit(jit, ocb, ctx); @@ -4530,7 +4598,7 @@ fn gen_send_iseq( // If we have a method accepting no kwargs (**nil), exit if we have passed // it any kwargs. - if supplying_kws && unsafe { get_iseq_flags_has_accepts_no_kwarg(iseq) } { + if supplying_kws && unsafe { get_iseq_flags_accepts_no_kwarg(iseq) } { gen_counter_incr!(asm, send_iseq_complex_callee); return CantCompile; } @@ -4601,6 +4669,26 @@ fn gen_send_iseq( return CantCompile; } + let block_arg = flags & VM_CALL_ARGS_BLOCKARG != 0; + let block_arg_type = if block_arg { + Some(ctx.get_opnd_type(StackOpnd(0))) + } else { + None + }; + + match block_arg_type { + Some(Type::Nil | Type::BlockParamProxy) => { + // We'll handle this later + } + None => { + // Nothing to do + } + _ => { + gen_counter_incr!(asm, send_block_arg); + return CantCompile; + } + } + // If we have unfilled optional arguments and keyword arguments then we // would need to move adjust the arguments location to account for that. // For now we aren't handling this case. @@ -4695,6 +4783,23 @@ fn gen_send_iseq( // Check for interrupts gen_check_ints(asm, side_exit); + match block_arg_type { + Some(Type::Nil) => { + // We have a nil block arg, so let's pop it off the args + ctx.stack_pop(1); + } + Some(Type::BlockParamProxy) => { + // We don't need the actual stack value + ctx.stack_pop(1); + } + None => { + // Nothing to do + } + _ => { + assert!(false); + } + } + let leaf_builtin_raw = unsafe { rb_leaf_builtin_function(iseq) }; let leaf_builtin: Option<*const rb_builtin_function> = if leaf_builtin_raw.is_null() { None @@ -4898,43 +5003,49 @@ fn gen_send_iseq( asm.mov(ctx.stack_opnd(-1), unspec_opnd.into()); } - // Points to the receiver operand on the stack - let recv = ctx.stack_opnd(argc); + // Points to the receiver operand on the stack unless a captured environment is used + let recv = match captured_opnd { + Some(captured_opnd) => asm.load(Opnd::mem(64, captured_opnd, 0)), // captured->self + _ => ctx.stack_opnd(argc), + }; + let captured_self = captured_opnd.is_some(); + let sp_offset = (argc as isize) + if captured_self { 0 } else { 1 }; // Store the updated SP on the current frame (pop arguments and receiver) asm.comment("store caller sp"); - let caller_sp = asm.lea(ctx.sp_opnd((SIZEOF_VALUE as isize) * -((argc as isize) + 1))); + let caller_sp = asm.lea(ctx.sp_opnd((SIZEOF_VALUE as isize) * -sp_offset)); asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SP), caller_sp); // Store the next PC in the current frame jit_save_pc(jit, asm); - if let Some(block_val) = block { - // Change cfp->block_code in the current frame. See vm_caller_setup_arg_block(). - // VM_CFP_TO_CAPTURED_BLCOK does &cfp->self, rb_captured_block->code.iseq aliases - // with cfp->block_code. - asm.store(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_BLOCK_CODE), VALUE(block_val as usize).into()); - } - // Adjust the callee's stack pointer let offs = (SIZEOF_VALUE as isize) * (3 + (num_locals as isize) + if doing_kw_call { 1 } else { 0 }); let callee_sp = asm.lea(ctx.sp_opnd(offs)); - let frame_block_handler = if let Some(_) = block { - BlockHandler::CurrentFrame + let specval = if let Some(prev_ep) = prev_ep { + // We've already side-exited if the callee expects a block, so we + // ignore any supplied block here + SpecVal::PrevEP(prev_ep) + } else if let Some(captured_opnd) = captured_opnd { + let ep_opnd = asm.load(Opnd::mem(64, captured_opnd, SIZEOF_VALUE_I32)); // captured->ep + SpecVal::PrevEPOpnd(ep_opnd) + } else if block_arg_type == Some(Type::BlockParamProxy) { + SpecVal::BlockParamProxy + } else if let Some(block_val) = block { + SpecVal::BlockISeq(block_val) } else { - BlockHandler::None + SpecVal::None }; // Setup the new frame gen_push_frame(jit, ctx, asm, true, ControlFrame { frame_type, - block_handler: frame_block_handler, + specval, cme, recv, sp: callee_sp, - prev_ep, iseq: Some(iseq), pc: None, // We are calling into jitted code, which will set the PC as necessary local_size: num_locals @@ -4952,7 +5063,7 @@ fn gen_send_iseq( }; // Create a context for the callee - let mut callee_ctx = Context::new(); // Was DEFAULT_CTX + let mut callee_ctx = Context::default(); // Set the argument types in the callee's context for arg_idx in 0..argc { @@ -4961,7 +5072,11 @@ fn gen_send_iseq( callee_ctx.set_local_type(arg_idx.try_into().unwrap(), arg_type); } - let recv_type = ctx.get_opnd_type(StackOpnd(argc.try_into().unwrap())); + let recv_type = if captured_self { + Type::Unknown // we don't track the type information of captured->self for now + } else { + ctx.get_opnd_type(StackOpnd(argc.try_into().unwrap())) + }; callee_ctx.upgrade_opnd_type(SelfOpnd, recv_type); // The callee might change locals through Kernel#binding and other means. @@ -4971,7 +5086,7 @@ fn gen_send_iseq( // After the return, sp_offset will be 1. The codegen for leave writes // the return value in case of JIT-to-JIT return. let mut return_ctx = *ctx; - return_ctx.stack_pop((argc + 1).try_into().unwrap()); + return_ctx.stack_pop(sp_offset.try_into().unwrap()); return_ctx.stack_push(Type::Unknown); return_ctx.set_sp_offset(1); return_ctx.reset_chain_depth(); @@ -5141,26 +5256,23 @@ fn gen_send_general( return CantCompile; } - if flags & VM_CALL_ARGS_BLOCKARG != 0 { - gen_counter_incr!(asm, send_block_arg); - return CantCompile; - } - // Defer compilation so we can specialize on class of receiver if !jit_at_current_insn(jit) { defer_compilation(jit, ctx, asm, ocb); return EndBlock; } - let comptime_recv = jit_peek_at_stack(jit, ctx, argc as isize); + let recv_idx = argc + if flags & VM_CALL_ARGS_BLOCKARG != 0 { 1 } else { 0 }; + + let comptime_recv = jit_peek_at_stack(jit, ctx, recv_idx as isize); let comptime_recv_klass = comptime_recv.class_of(); // Guard that the receiver has the same class as the one from compile time let side_exit = get_side_exit(jit, ocb, ctx); // Points to the receiver operand on the stack - let recv = ctx.stack_opnd(argc); - let recv_opnd = StackOpnd(argc.try_into().unwrap()); + let recv = ctx.stack_opnd(recv_idx); + let recv_opnd = StackOpnd(recv_idx.try_into().unwrap()); jit_guard_known_klass( jit, ctx, @@ -5223,7 +5335,7 @@ fn gen_send_general( VM_METHOD_TYPE_ISEQ => { let iseq = unsafe { get_def_iseq_ptr((*cme).def) }; let frame_type = VM_FRAME_MAGIC_METHOD | VM_ENV_FLAG_LOCAL; - return gen_send_iseq(jit, ctx, asm, ocb, iseq, ci, frame_type, None, cme, block, flags, argc); + return gen_send_iseq(jit, ctx, asm, ocb, iseq, ci, frame_type, None, cme, block, flags, argc, None); } VM_METHOD_TYPE_CFUNC => { return gen_send_cfunc( @@ -5268,6 +5380,11 @@ fn gen_send_general( let ivar_name = unsafe { get_cme_def_body_attr_id(cme) }; + if flags & VM_CALL_ARGS_BLOCKARG != 0 { + gen_counter_incr!(asm, send_block_arg); + return CantCompile; + } + return gen_get_ivar( jit, ctx, @@ -5293,6 +5410,9 @@ fn gen_send_general( // See :attr-tracing: gen_counter_incr!(asm, send_cfunc_tracing); return CantCompile; + } else if flags & VM_CALL_ARGS_BLOCKARG != 0 { + gen_counter_incr!(asm, send_block_arg); + return CantCompile; } else { let ivar_name = unsafe { get_cme_def_body_attr_id(cme) }; return gen_set_ivar(jit, ctx, asm, comptime_recv, ivar_name, flags, argc); @@ -5321,6 +5441,10 @@ fn gen_send_general( } // Send family of methods, e.g. call/apply VM_METHOD_TYPE_OPTIMIZED => { + if flags & VM_CALL_ARGS_BLOCKARG != 0 { + gen_counter_incr!(asm, send_block_arg); + return CantCompile; + } let opt_type = unsafe { get_cme_def_body_optimized_type(cme) }; match opt_type { @@ -5536,6 +5660,95 @@ fn gen_send( return gen_send_general(jit, ctx, asm, ocb, cd, block); } +fn gen_invokeblock( + jit: &mut JITState, + ctx: &mut Context, + asm: &mut Assembler, + ocb: &mut OutlinedCb, +) -> CodegenStatus { + if !jit_at_current_insn(jit) { + defer_compilation(jit, ctx, asm, ocb); + return EndBlock; + } + + // Get call info + let cd = jit_get_arg(jit, 0).as_ptr(); + let ci = unsafe { get_call_data_ci(cd) }; + let argc: i32 = unsafe { vm_ci_argc(ci) }.try_into().unwrap(); + let flags = unsafe { vm_ci_flag(ci) }; + + // Get block_handler + let cfp = unsafe { get_ec_cfp(jit.ec.unwrap()) }; + let lep = unsafe { rb_vm_ep_local_ep(get_cfp_ep(cfp)) }; + let comptime_handler = unsafe { *lep.offset(VM_ENV_DATA_INDEX_SPECVAL.try_into().unwrap()) }; + + // Handle each block_handler type + if comptime_handler.0 == VM_BLOCK_HANDLER_NONE as usize { // no block given + gen_counter_incr!(asm, invokeblock_none); + CantCompile + } else if comptime_handler.0 & 0x3 == 0x1 { // VM_BH_ISEQ_BLOCK_P + asm.comment("get local EP"); + let ep_opnd = gen_get_lep(jit, asm); + let block_handler_opnd = asm.load( + Opnd::mem(64, ep_opnd, (SIZEOF_VALUE as i32) * (VM_ENV_DATA_INDEX_SPECVAL as i32)) + ); + + asm.comment("guard block_handler type"); + let side_exit = get_side_exit(jit, ocb, ctx); + let tag_opnd = asm.and(block_handler_opnd, 0x3.into()); // block_handler is a tagged pointer + asm.cmp(tag_opnd, 0x1.into()); // VM_BH_ISEQ_BLOCK_P + asm.jne(counted_exit!(ocb, side_exit, invokeblock_iseq_tag_changed).into()); + + // Not supporting vm_callee_setup_block_arg_arg0_splat for now + let comptime_captured = unsafe { ((comptime_handler.0 & !0x3) as *const rb_captured_block).as_ref().unwrap() }; + let comptime_iseq = unsafe { *comptime_captured.code.iseq.as_ref() }; + if argc == 1 && unsafe { get_iseq_flags_has_lead(comptime_iseq) && !get_iseq_flags_ambiguous_param0(comptime_iseq) } { + gen_counter_incr!(asm, invokeblock_iseq_arg0_splat); + return CantCompile; + } + + asm.comment("guard known ISEQ"); + let captured_opnd = asm.and(block_handler_opnd, Opnd::Imm(!0x3)); + let iseq_opnd = asm.load(Opnd::mem(64, captured_opnd, SIZEOF_VALUE_I32 * 2)); + asm.cmp(iseq_opnd, (comptime_iseq as usize).into()); + let block_changed_exit = counted_exit!(ocb, side_exit, invokeblock_iseq_block_changed); + jit_chain_guard( + JCC_JNE, + jit, + ctx, + asm, + ocb, + SEND_MAX_CHAIN_DEPTH, + block_changed_exit, + ); + + gen_send_iseq( + jit, + ctx, + asm, + ocb, + comptime_iseq, + ci, + VM_FRAME_MAGIC_BLOCK, + None, + 0 as _, + None, + flags, + argc, + Some(captured_opnd), + ) + } else if comptime_handler.0 & 0x3 == 0x3 { // VM_BH_IFUNC_P + gen_counter_incr!(asm, invokeblock_ifunc); + CantCompile + } else if comptime_handler.symbol_p() { + gen_counter_incr!(asm, invokeblock_symbol); + CantCompile + } else { // Proc + gen_counter_incr!(asm, invokeblock_proc); + CantCompile + } +} + fn gen_invokesuper( jit: &mut JITState, ctx: &mut Context, @@ -5675,7 +5888,7 @@ fn gen_invokesuper( VM_METHOD_TYPE_ISEQ => { let iseq = unsafe { get_def_iseq_ptr((*cme).def) }; let frame_type = VM_FRAME_MAGIC_METHOD | VM_ENV_FLAG_LOCAL; - gen_send_iseq(jit, ctx, asm, ocb, iseq, ci, frame_type, None, cme, block, ci_flags, argc) + gen_send_iseq(jit, ctx, asm, ocb, iseq, ci, frame_type, None, cme, block, ci_flags, argc, None) } VM_METHOD_TYPE_CFUNC => { gen_send_cfunc(jit, ctx, asm, ocb, ci, cme, block, ptr::null(), ci_flags, argc) @@ -6186,7 +6399,7 @@ fn gen_getblockparamproxy( // Push rb_block_param_proxy. It's a root, so no need to use jit_mov_gc_ptr. assert!(!unsafe { rb_block_param_proxy }.special_const_p()); - let top = ctx.stack_push(Type::Unknown); + let top = ctx.stack_push(Type::BlockParamProxy); asm.mov(top, Opnd::const_ptr(unsafe { rb_block_param_proxy }.as_ptr())); } @@ -6442,6 +6655,7 @@ fn get_gen_fn(opcode: VALUE) -> Option { YARVINSN_getblockparam => Some(gen_getblockparam), YARVINSN_opt_send_without_block => Some(gen_opt_send_without_block), YARVINSN_send => Some(gen_send), + YARVINSN_invokeblock => Some(gen_invokeblock), YARVINSN_invokesuper => Some(gen_invokesuper), YARVINSN_leave => Some(gen_leave), @@ -6504,6 +6718,15 @@ pub struct CodegenGlobals { // Methods for generating code for hardcoded (usually C) methods method_codegen_table: HashMap, + + /// Page indexes for outlined code that are not associated to any ISEQ. + ocb_pages: Vec, + + /// Freed page indexes. None if code GC has not been used. + freed_pages: Option>, + + /// How many times code GC has been executed. + code_gc_count: usize, } /// For implementing global code invalidation. A position in the inline @@ -6570,6 +6793,7 @@ impl CodegenGlobals { #[cfg(test)] let mut ocb = OutlinedCb::wrap(CodeBlock::new_dummy(mem_size / 2)); + let ocb_start_addr = ocb.unwrap().get_write_ptr(); let leave_exit_code = gen_leave_exit(&mut ocb); let stub_exit_code = gen_code_for_exit_from_stub(&mut ocb); @@ -6577,6 +6801,9 @@ impl CodegenGlobals { // Generate full exit code for C func let cfunc_exit_code = gen_full_cfunc_return(&mut ocb); + let ocb_end_addr = ocb.unwrap().get_write_ptr(); + let ocb_pages = ocb.unwrap().addrs_to_pages(ocb_start_addr, ocb_end_addr); + // Mark all code memory as executable cb.mark_all_executable(); ocb.unwrap().mark_all_executable(); @@ -6590,6 +6817,9 @@ impl CodegenGlobals { global_inval_patches: Vec::new(), inline_frozen_bytes: 0, method_codegen_table: HashMap::new(), + ocb_pages, + freed_pages: None, + code_gc_count: 0, }; // Register the method codegen functions @@ -6725,6 +6955,23 @@ impl CodegenGlobals { Some(&mgf) => Some(mgf), // Deref } } + + pub fn get_ocb_pages() -> &'static Vec { + &CodegenGlobals::get_instance().ocb_pages + } + + pub fn get_freed_pages() -> &'static mut Option> { + &mut CodegenGlobals::get_instance().freed_pages + } + + pub fn set_freed_pages(freed_pages: Vec) { + CodegenGlobals::get_instance().freed_pages = Some(freed_pages); + CodegenGlobals::get_instance().code_gc_count += 1; + } + + pub fn get_code_gc_count() -> usize { + CodegenGlobals::get_instance().code_gc_count + } } #[cfg(test)] @@ -6740,7 +6987,7 @@ mod tests { return ( JITState::new(&block), - Context::new(), + Context::default(), Assembler::new(), CodeBlock::new_dummy(256 * 1024), OutlinedCb::wrap(CodeBlock::new_dummy(256 * 1024)), @@ -6783,18 +7030,19 @@ mod tests { asm.compile(&mut cb); assert_eq!(status, KeepCompiling); - assert_eq!(context.diff(&Context::new()), 0); + assert_eq!(context.diff(&Context::default()), 0); assert_eq!(cb.get_write_pos(), 0); } #[test] fn test_gen_pop() { let (mut jit, _, mut asm, _cb, mut ocb) = setup_codegen(); - let mut context = Context::new_with_stack_size(1); + let mut context = Context::default(); + context.stack_push(Type::Fixnum); let status = gen_pop(&mut jit, &mut context, &mut asm, &mut ocb); assert_eq!(status, KeepCompiling); - assert_eq!(context.diff(&Context::new()), 0); + assert_eq!(context.diff(&Context::default()), 0); } #[test] diff --git a/yjit/src/core.rs b/yjit/src/core.rs index 705a0c46ef53a5..c0e48e87b2bc20 100644 --- a/yjit/src/core.rs +++ b/yjit/src/core.rs @@ -11,6 +11,7 @@ use crate::utils::*; use crate::disasm::*; use core::ffi::c_void; use std::cell::*; +use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::mem; use std::rc::{Rc}; @@ -43,6 +44,9 @@ pub enum Type { TString, // An object with the T_STRING flag set, possibly an rb_cString CString, // An un-subclassed string of type rb_cString (can have instance vars in some cases) + + BlockParamProxy, // A special sentinel value indicating the block parameter should be read from + // the current surrounding cfp } // Default initialization @@ -78,6 +82,12 @@ impl Type { if val.class_of() == unsafe { rb_cString } { return Type::CString; } + // We likewise can't reference rb_block_param_proxy, but it's again an optimisation; + // we can just treat it as a normal Object. + #[cfg(not(test))] + if val == unsafe { rb_block_param_proxy } { + return Type::BlockParamProxy; + } match val.builtin_type() { RUBY_T_ARRAY => Type::Array, RUBY_T_HASH => Type::Hash, @@ -141,7 +151,8 @@ impl Type { Type::Hash => Some(RUBY_T_HASH), Type::ImmSymbol | Type::HeapSymbol => Some(RUBY_T_SYMBOL), Type::TString | Type::CString => Some(RUBY_T_STRING), - Type::Unknown | Type::UnknownImm | Type::UnknownHeap => None + Type::Unknown | Type::UnknownImm | Type::UnknownHeap => None, + Type::BlockParamProxy => None, } } @@ -321,7 +332,7 @@ struct Branch { // Positions where the generated code starts and ends start_addr: Option, - end_addr: Option, + end_addr: Option, // exclusive // Context right after the branch instruction #[allow(unused)] // set but not read at the moment @@ -475,7 +486,11 @@ impl Eq for BlockRef {} /// when calling into YJIT #[derive(Default)] pub struct IseqPayload { + // Basic block versions version_map: VersionMap, + + // Indexes of code pages used by this this ISEQ + pub pages: HashSet, } impl IseqPayload { @@ -498,7 +513,7 @@ pub fn get_iseq_payload(iseq: IseqPtr) -> Option<&'static mut IseqPayload> { } /// Get the payload object associated with an iseq. Create one if none exists. -fn get_or_create_iseq_payload(iseq: IseqPtr) -> &'static mut IseqPayload { +pub fn get_or_create_iseq_payload(iseq: IseqPtr) -> &'static mut IseqPayload { type VoidPtr = *mut c_void; let payload_non_null = unsafe { @@ -537,6 +552,40 @@ pub fn for_each_iseq(mut callback: F) { unsafe { rb_yjit_for_each_iseq(Some(callback_wrapper), (&mut data) as *mut _ as *mut c_void) }; } +/// Iterate over all on-stack ISEQs +pub fn for_each_on_stack_iseq(mut callback: F) { + unsafe extern "C" fn callback_wrapper(iseq: IseqPtr, data: *mut c_void) { + let callback: &mut &mut dyn FnMut(IseqPtr) -> bool = std::mem::transmute(&mut *data); + callback(iseq); + } + let mut data: &mut dyn FnMut(IseqPtr) = &mut callback; + unsafe { rb_jit_cont_each_iseq(Some(callback_wrapper), (&mut data) as *mut _ as *mut c_void) }; +} + +/// Iterate over all on-stack ISEQ payloads +pub fn for_each_on_stack_iseq_payload(mut callback: F) { + for_each_on_stack_iseq(|iseq| { + if let Some(iseq_payload) = get_iseq_payload(iseq) { + callback(iseq_payload); + } + }); +} + +/// Iterate over all NOT on-stack ISEQ payloads +pub fn for_each_off_stack_iseq_payload(mut callback: F) { + let mut on_stack_iseqs: Vec = vec![]; + for_each_on_stack_iseq(|iseq| { + on_stack_iseqs.push(iseq); + }); + for_each_iseq(|iseq| { + if !on_stack_iseqs.contains(&iseq) { + if let Some(iseq_payload) = get_iseq_payload(iseq) { + callback(iseq_payload); + } + } + }) +} + /// Free the per-iseq payload #[no_mangle] pub extern "C" fn rb_yjit_iseq_free(payload: *mut c_void) { @@ -816,7 +865,12 @@ pub fn limit_block_versions(blockid: BlockId, ctx: &Context) -> Context { generic_ctx.stack_size = ctx.stack_size; generic_ctx.sp_offset = ctx.sp_offset; - // Mutate the incoming context + debug_assert_ne!( + usize::MAX, + ctx.diff(&generic_ctx), + "should substitute a compatible context", + ); + return generic_ctx; } @@ -854,6 +908,12 @@ fn add_block_version(blockref: &BlockRef, cb: &CodeBlock) { } incr_counter!(compiled_block_count); + + // Mark code pages for code GC + let iseq_payload = get_iseq_payload(block.blockid.iseq).unwrap(); + for page in cb.addrs_to_pages(block.start_addr.unwrap(), block.end_addr.unwrap()) { + iseq_payload.pages.insert(page); + } } /// Remove a block version from the version map of its parent ISEQ @@ -969,22 +1029,6 @@ impl Block { } impl Context { - pub fn new_with_stack_size(size: i16) -> Self { - return Context { - stack_size: size as u16, - sp_offset: size, - chain_depth: 0, - local_types: [Type::Unknown; MAX_LOCAL_TYPES], - temp_types: [Type::Unknown; MAX_TEMP_TYPES], - self_type: Type::Unknown, - temp_mapping: [MapToStack; MAX_TEMP_TYPES], - }; - } - - pub fn new() -> Self { - return Self::new_with_stack_size(0); - } - pub fn get_stack_size(&self) -> u16 { self.stack_size } @@ -1526,7 +1570,11 @@ pub fn gen_entry_point(iseq: IseqPtr, ec: EcPtr) -> Option { match block { // Compilation failed - None => return None, + None => { + // Trigger code GC. This entry point will be recompiled later. + cb.code_gc(); + return None; + } // If the block contains no Ruby instructions Some(block) => { @@ -1776,6 +1824,18 @@ fn branch_stub_hit_body(branch_ptr: *const c_void, target_idx: u32, ec: EcPtr) - block_rc.borrow().start_addr.unwrap() } None => { + // Code GC needs to borrow blocks for invalidation, so their mutable + // borrows must be dropped first. + drop(block); + drop(branch); + // Trigger code GC. The whole ISEQ will be recompiled later. + // We shouldn't trigger it in the middle of compilation in branch_stub_hit + // because incomplete code could be used when cb.dropped_bytes is flipped + // by code GC. So this place, after all compilation, is the safest place + // to hook code GC on branch_stub_hit. + cb.code_gc(); + branch = branch_rc.borrow_mut(); + // Failed to service the stub by generating a new block so now we // need to exit to the interpreter at the stubbed location. We are // intentionally *not* restoring original_interp_sp. At the time of @@ -1793,7 +1853,8 @@ fn branch_stub_hit_body(branch_ptr: *const c_void, target_idx: u32, ec: EcPtr) - let new_branch_size = branch.code_size(); assert!( new_branch_size <= branch_size_on_entry, - "branch stubs should never enlarge branches" + "branch stubs should never enlarge branches: (old_size: {}, new_size: {})", + branch_size_on_entry, new_branch_size, ); // Return a pointer to the compiled block version @@ -1904,7 +1965,10 @@ pub fn gen_branch( // Get the branch targets or stubs let dst_addr0 = get_branch_target(target0, ctx0, &branchref, 0, ocb); let dst_addr1 = if let Some(ctx) = ctx1 { - get_branch_target(target1.unwrap(), ctx, &branchref, 1, ocb) + match get_branch_target(target1.unwrap(), ctx, &branchref, 1, ocb) { + Some(dst_addr) => Some(dst_addr), + None => return, // avoid unwrap() in gen_fn() + } } else { None }; diff --git a/yjit/src/cruby.rs b/yjit/src/cruby.rs index d8dbdb40193f8d..168443e6f0a8a6 100644 --- a/yjit/src/cruby.rs +++ b/yjit/src/cruby.rs @@ -162,6 +162,7 @@ pub use rb_iseq_encoded_size as get_iseq_encoded_size; pub use rb_get_iseq_body_local_iseq as get_iseq_body_local_iseq; pub use rb_get_iseq_body_iseq_encoded as get_iseq_body_iseq_encoded; pub use rb_get_iseq_body_stack_max as get_iseq_body_stack_max; +pub use rb_get_iseq_flags_has_lead as get_iseq_flags_has_lead; pub use rb_get_iseq_flags_has_opt as get_iseq_flags_has_opt; pub use rb_get_iseq_flags_has_kw as get_iseq_flags_has_kw; pub use rb_get_iseq_flags_has_rest as get_iseq_flags_has_rest; @@ -169,7 +170,8 @@ pub use rb_get_iseq_flags_ruby2_keywords as get_iseq_flags_ruby2_keywords; pub use rb_get_iseq_flags_has_post as get_iseq_flags_has_post; pub use rb_get_iseq_flags_has_kwrest as get_iseq_flags_has_kwrest; pub use rb_get_iseq_flags_has_block as get_iseq_flags_has_block; -pub use rb_get_iseq_flags_has_accepts_no_kwarg as get_iseq_flags_has_accepts_no_kwarg; +pub use rb_get_iseq_flags_ambiguous_param0 as get_iseq_flags_ambiguous_param0; +pub use rb_get_iseq_flags_accepts_no_kwarg as get_iseq_flags_accepts_no_kwarg; pub use rb_get_iseq_body_local_table_size as get_iseq_body_local_table_size; pub use rb_get_iseq_body_param_keyword as get_iseq_body_param_keyword; pub use rb_get_iseq_body_param_size as get_iseq_body_param_size; @@ -346,13 +348,27 @@ impl VALUE { (cval & mask) == flag } - /// Return true for a static (non-heap) Ruby symbol + /// Return true if the value is a Ruby symbol (RB_SYMBOL_P) + pub fn symbol_p(self) -> bool { + self.static_sym_p() || self.dynamic_sym_p() + } + + /// Return true for a static (non-heap) Ruby symbol (RB_STATIC_SYM_P) pub fn static_sym_p(self) -> bool { let VALUE(cval) = self; let flag = RUBY_SYMBOL_FLAG as usize; (cval & 0xff) == flag } + /// Return true for a dynamic Ruby symbol (RB_DYNAMIC_SYM_P) + fn dynamic_sym_p(self) -> bool { + return if self.special_const_p() { + false + } else { + self.builtin_type() == RUBY_T_SYMBOL + } + } + /// Returns true or false depending on whether the value is nil pub fn nil_p(self) -> bool { self == Qnil diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 158bdd80dcb7b1..3373c6d76e41bc 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -965,6 +965,9 @@ pub const VM_ENV_FLAG_ESCAPED: vm_frame_env_flags = 4; pub const VM_ENV_FLAG_WB_REQUIRED: vm_frame_env_flags = 8; pub const VM_ENV_FLAG_ISOLATED: vm_frame_env_flags = 16; pub type vm_frame_env_flags = u32; +extern "C" { + pub fn rb_vm_ep_local_ep(ep: *const VALUE) -> *const VALUE; +} extern "C" { pub fn rb_iseq_path(iseq: *const rb_iseq_t) -> VALUE; } @@ -1278,12 +1281,18 @@ extern "C" { lines: *mut ::std::os::raw::c_int, ) -> ::std::os::raw::c_int; } +extern "C" { + pub fn rb_jit_cont_each_iseq(callback: rb_iseq_callback, data: *mut ::std::os::raw::c_void); +} extern "C" { pub fn rb_yjit_mark_writable(mem_block: *mut ::std::os::raw::c_void, mem_size: u32) -> bool; } extern "C" { pub fn rb_yjit_mark_executable(mem_block: *mut ::std::os::raw::c_void, mem_size: u32); } +extern "C" { + pub fn rb_yjit_mark_unused(mem_block: *mut ::std::os::raw::c_void, mem_size: u32) -> bool; +} extern "C" { pub fn rb_yjit_icache_invalidate( start: *mut ::std::os::raw::c_void, @@ -1412,6 +1421,9 @@ extern "C" { extern "C" { pub fn rb_get_iseq_body_local_iseq(iseq: *const rb_iseq_t) -> *const rb_iseq_t; } +extern "C" { + pub fn rb_get_iseq_body_parent_iseq(iseq: *const rb_iseq_t) -> *const rb_iseq_t; +} extern "C" { pub fn rb_get_iseq_body_local_table_size(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; } @@ -1421,6 +1433,9 @@ extern "C" { extern "C" { pub fn rb_get_iseq_body_stack_max(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; } +extern "C" { + pub fn rb_get_iseq_flags_has_lead(iseq: *const rb_iseq_t) -> bool; +} extern "C" { pub fn rb_get_iseq_flags_has_opt(iseq: *const rb_iseq_t) -> bool; } @@ -1443,7 +1458,10 @@ extern "C" { pub fn rb_get_iseq_flags_has_block(iseq: *const rb_iseq_t) -> bool; } extern "C" { - pub fn rb_get_iseq_flags_has_accepts_no_kwarg(iseq: *const rb_iseq_t) -> bool; + pub fn rb_get_iseq_flags_ambiguous_param0(iseq: *const rb_iseq_t) -> bool; +} +extern "C" { + pub fn rb_get_iseq_flags_accepts_no_kwarg(iseq: *const rb_iseq_t) -> bool; } extern "C" { pub fn rb_get_iseq_body_param_keyword( diff --git a/yjit/src/options.rs b/yjit/src/options.rs index 303ae4980f5fbb..645af0faacc8b0 100644 --- a/yjit/src/options.rs +++ b/yjit/src/options.rs @@ -53,7 +53,7 @@ pub struct Options { // Initialize the options to default values pub static mut OPTIONS: Options = Options { - exec_mem_size: 256 * 1024 * 1024, + exec_mem_size: 128 * 1024 * 1024, code_page_size: 16 * 1024, call_threshold: 10, greedy_versioning: false, @@ -91,7 +91,7 @@ macro_rules! get_option_ref { // Unsafe is ok here because options are initialized // once before any Ruby code executes ($option_name:ident) => { - unsafe { &(OPTIONS.$option_name) } + unsafe { &($crate::options::OPTIONS.$option_name) } }; } pub(crate) use get_option_ref; diff --git a/yjit/src/stats.rs b/yjit/src/stats.rs index 0ad77fc5df1230..a60fcaf8364f84 100644 --- a/yjit/src/stats.rs +++ b/yjit/src/stats.rs @@ -217,6 +217,14 @@ make_counters! { invokesuper_me_changed, invokesuper_block, + invokeblock_none, + invokeblock_iseq_arg0_splat, + invokeblock_iseq_block_changed, + invokeblock_iseq_tag_changed, + invokeblock_ifunc, + invokeblock_proc, + invokeblock_symbol, + leave_se_interrupt, leave_interp_return, leave_start_pc_non_zero, @@ -351,23 +359,40 @@ fn rb_yjit_gen_stats_dict() -> VALUE { return Qnil; } + macro_rules! hash_aset_usize { + ($hash:ident, $counter_name:expr, $value:expr) => { + let key = rust_str_to_sym($counter_name); + let value = VALUE::fixnum_from_usize($value); + rb_hash_aset($hash, key, value); + } + } + let hash = unsafe { rb_hash_new() }; - // Inline and outlined code size + // CodeBlock stats unsafe { // Get the inline and outlined code blocks let cb = CodegenGlobals::get_inline_cb(); let ocb = CodegenGlobals::get_outlined_cb(); // Inline code size - let key = rust_str_to_sym("inline_code_size"); - let value = VALUE::fixnum_from_usize(cb.get_write_pos()); - rb_hash_aset(hash, key, value); + hash_aset_usize!(hash, "inline_code_size", cb.code_size()); // Outlined code size - let key = rust_str_to_sym("outlined_code_size"); - let value = VALUE::fixnum_from_usize(ocb.unwrap().get_write_pos()); - rb_hash_aset(hash, key, value); + hash_aset_usize!(hash, "outlined_code_size", ocb.unwrap().code_size()); + + // GCed pages + let freed_page_count = cb.num_freed_pages(); + hash_aset_usize!(hash, "freed_page_count", freed_page_count); + + // GCed code size + hash_aset_usize!(hash, "freed_code_size", freed_page_count * cb.page_size()); + + // Live pages + hash_aset_usize!(hash, "live_page_count", cb.num_mapped_pages() - freed_page_count); + + // Code GC count + hash_aset_usize!(hash, "code_gc_count", CodegenGlobals::get_code_gc_count()); } // If we're not generating stats, the hash is done diff --git a/yjit/src/virtualmem.rs b/yjit/src/virtualmem.rs index 5234963872f513..1d80983c9e9e44 100644 --- a/yjit/src/virtualmem.rs +++ b/yjit/src/virtualmem.rs @@ -51,6 +51,8 @@ pub trait Allocator { fn mark_writable(&mut self, ptr: *const u8, size: u32) -> bool; fn mark_executable(&mut self, ptr: *const u8, size: u32); + + fn mark_unused(&mut self, ptr: *const u8, size: u32) -> bool; } /// Pointer into a [VirtualMemory]. @@ -91,6 +93,15 @@ impl VirtualMemory { CodePtr(self.region_start) } + pub fn end_ptr(&self) -> CodePtr { + CodePtr(self.region_start.wrapping_add(self.mapped_region_bytes)) + } + + /// Size of the region in bytes that we have allocated physical memory for. + pub fn mapped_region_size(&self) -> usize { + self.mapped_region_bytes + } + /// Size of the region in bytes where writes could be attempted. pub fn virtual_region_size(&self) -> usize { self.region_size_bytes @@ -177,6 +188,12 @@ impl VirtualMemory { // Make mapped region executable self.allocator.mark_executable(region_start, mapped_region_bytes); } + + /// Free a range of bytes. start_ptr must be memory page-aligned. + pub fn free_bytes(&mut self, start_ptr: CodePtr, size: u32) { + assert_eq!(start_ptr.into_usize() % self.page_size_bytes, 0); + self.allocator.mark_unused(start_ptr.0, size); + } } impl CodePtr { @@ -235,6 +252,10 @@ mod sys { fn mark_executable(&mut self, ptr: *const u8, size: u32) { unsafe { rb_yjit_mark_executable(ptr as VoidPtr, size) } } + + fn mark_unused(&mut self, ptr: *const u8, size: u32) -> bool { + unsafe { rb_yjit_mark_unused(ptr as VoidPtr, size) } + } } } @@ -258,6 +279,7 @@ pub mod tests { enum AllocRequest { MarkWritable{ start_idx: usize, length: usize }, MarkExecutable{ start_idx: usize, length: usize }, + MarkUnused{ start_idx: usize, length: usize }, } use AllocRequest::*; @@ -298,6 +320,13 @@ pub mod tests { // We don't try to execute generated code in cfg(test) // so no need to actually request executable memory. } + + fn mark_unused(&mut self, ptr: *const u8, length: u32) -> bool { + let index = self.bounds_check_request(ptr, length); + self.requests.push(MarkUnused { start_idx: index, length: length.as_usize() }); + + true + } } // Fictional architecture where each page is 4 bytes long diff --git a/yjit/src/yjit.rs b/yjit/src/yjit.rs index 5cd23f066f52d8..4850dca7a84e1a 100644 --- a/yjit/src/yjit.rs +++ b/yjit/src/yjit.rs @@ -79,6 +79,18 @@ pub extern "C" fn rb_yjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr) -> *con } } +/// Free and recompile all existing JIT code +#[no_mangle] +pub extern "C" fn rb_yjit_code_gc(_ec: EcPtr, _ruby_self: VALUE) -> VALUE { + if !yjit_enabled_p() { + return Qnil; + } + + let cb = CodegenGlobals::get_inline_cb(); + cb.code_gc(); + Qnil +} + /// Simulate a situation where we are out of executable memory #[no_mangle] pub extern "C" fn rb_yjit_simulate_oom_bang(_ec: EcPtr, _ruby_self: VALUE) -> VALUE {