diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 2e504f175fc0de..00000000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,65 +0,0 @@ -environment: - global: - CCACHE_DIR: C:\ccache - CYG_MIRROR: http://cygwin.mirror.constant.com - CYG_CACHE: '%CYG_ROOT%\var\cache\setup' - CYG_BASH: '%CYG_ROOT%\bin\bash' - - matrix: - - MINGW_ARCH: "i686" - CYG_ROOT: C:\cygwin - CYG_SETUP: setup-x86.exe - JULIA_TEST_MAXRSS_MB: 500 - - - MINGW_ARCH: "x86_64" - CYG_ROOT: C:\cygwin64 - CYG_SETUP: setup-x86_64.exe - JULIA_TEST_MAXRSS_MB: 450 - -# Only build on master and PR's for now, not personal branches -# Whether or not PR's get built is determined in the webhook settings -branches: - only: - - master - - /^release-.*/ - -# Note: use `[ci skip]` or `[skip ci]` anywhere in the commit message and AppVeyor won't be -# built for that commit. You can use `[skip appveyor]` to explicitly skip AppVeyor and -# allow other CI to still run. -skip_commits: -# Add [av skip] to commit messages for docfixes, etc to reduce load on queue - message: /\[av skip\]/ -# Skip running CI for changes only to the documentation -# https://github.com/JuliaLang/julia/pull/27356#discussion_r192536676 -# files: -# - doc/ - -notifications: - - provider: Email - on_build_success: false - on_build_failure: false - on_build_status_changed: false - -cache: - - '%CYG_CACHE%' - - '%CCACHE_DIR%' - -init: - - git config --global core.autocrlf input - -install: - - '%CYG_ROOT%\%CYG_SETUP% -gnq -R "%CYG_ROOT%" -s "%CYG_MIRROR%" -l "%CYG_CACHE%" -P make,python2,libiconv,curl,time,p7zip,ccache,mingw64-%MINGW_ARCH%-gcc-g++,mingw64-%MINGW_ARCH%-gcc-fortran > NULL 2>&1' - - '%CYG_ROOT%\bin\cygcheck -dc cygwin' - -build_script: - - 'echo Building Julia' - - '%CYG_BASH% -lc "cd $APPVEYOR_BUILD_FOLDER && ./contrib/windows/appveyor_build.sh"' - -test_script: - - 'echo Testing Julia' - - usr\bin\julia -e "Base.require(Main, :InteractiveUtils).versioninfo()" - - usr\bin\julia --sysimage-native-code=no -e "true" - - cd julia-* && .\bin\julia.exe --check-bounds=yes share\julia\test\runtests.jl all && - .\bin\julia.exe --check-bounds=yes share\julia\test\runtests.jl LibGit2/online download - - cd .. - - usr\bin\julia usr\share\julia\test\embedding\embedding-test.jl test\embedding\embedding.exe diff --git a/.buildkite-external-version b/.buildkite-external-version new file mode 100644 index 00000000000000..ba2906d0666cf7 --- /dev/null +++ b/.buildkite-external-version @@ -0,0 +1 @@ +main diff --git a/.clang-format b/.clang-format index 0322d0f6749a9d..39b5767a502918 100644 --- a/.clang-format +++ b/.clang-format @@ -109,7 +109,6 @@ StatementMacros: - checked_intrinsic_ctype - cvt_iintrinsic - fpiseq_n - - fpislt_n - ter_fintrinsic - ter_intrinsic_ctype - un_fintrinsic diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000000000..35cde5cd5e8543 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,4 @@ +coverage: + status: + project: off + patch: off diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000000..d2da8839ddb39c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +CODEOWNERS @JuliaLang/github-actions +/.github/ @JuliaLang/github-actions +/.buildkite/ @JuliaLang/github-actions + +/.github/workflows/retry.yml @DilumAluthge +/.github/workflows/statuses.yml @DilumAluthge diff --git a/.github/workflows/rerun_failed.yml b/.github/workflows/rerun_failed.yml new file mode 100644 index 00000000000000..7d022920658a90 --- /dev/null +++ b/.github/workflows/rerun_failed.yml @@ -0,0 +1,92 @@ +# Please ping @DilumAluthge when making any changes to this file. + +# Here are some steps that we take in this workflow file for security reasons: +# 1. We do not checkout any code. +# 2. We only run actions that are defined in a repository in the `JuliaLang` GitHub organization. +# 3. We do not give the `GITHUB_TOKEN` any permissions. +# 4. We only give the Buildkite API token (`BUILDKITE_API_TOKEN_RETRY`) the minimum necessary +# set of permissions. + +# Important note to Buildkite maintainers: +# In order to make this work, you need to tell Buildkite that it should NOT create a brand-new +# build when someone closes and reopens a pull request. To do so: +# 1. Go to the relevant pipeline (e.g. https://buildkite.com/julialang/julia-master). +# 2. Click on the "Pipeline Settings" button. +# 3. In the left sidebar, under "Pipeline Settings", click on "GitHub". +# 4. In the "GitHub Settings", under "Build Pull Requests", make sure that the "Skip pull +# request builds for existing commits" checkbox is checked. This is the setting that tells +# Buildkite that it should NOT create a brand-new build when someone closes and reopens a +# pull request. +# 5. At the bottom of the page, click the "Save GitHub Settings" button. + +name: Rerun Failed Buildkite Jobs + +# There are two ways that a user can rerun the failed Buildkite jobs: +# 1. Close and reopen the pull request. +# In order to use this approach, the user must be in one of the following three categories: +# (i) Author of the pull request +# (ii) Commit permissions +# (iii) Triage permissions +# 2. Post a comment on the pull request with exactly the following contents: /buildkite rerun failed +# In order to use this approach, the user must be in the following category: +# - A member of the JuliaLang GitHub organization (the membership must be publicized) + +on: + # When using the `pull_request_target` event, all PRs will get access to secret environment + # variables (such as the `BUILDKITE_API_TOKEN_RETRY` secret environment variable), even if + # the PR is from a fork. Therefore, for security reasons, we do not checkout any code in + # this workflow. + pull_request_target: + types: [ reopened ] + issue_comment: + types: [ created ] + +# We do not give the `GITHUB_TOKEN` any permissions. +# Therefore, the `GITHUB_TOKEN` only has the same access as any member of the public. +permissions: + contents: none + +jobs: + rerun-failed-buildkite-jobs: + name: Rerun Failed Buildkite Jobs + runs-on: ubuntu-latest + if: (github.repository == 'JuliaLang/julia') && ((github.event_name == 'pull_request_target' && github.event.action == 'reopened') || (github.event_name == 'issue_comment' && github.event.issue.pull_request && github.event.comment.body == '/buildkite rerun failed')) + steps: + # For security reasons, we do not checkout any code in this workflow. + - name: Check organization membership + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [[ "${{ github.event_name }}" == "pull_request_target" ]]; then + if [[ "${{ github.event.action }}" == "reopened" ]]; then + echo "This is a \"reopened\" event, so we do not need to check the user's organization membership." + echo "GOOD_TO_PROCEED=yes" >> ${GITHUB_ENV:?} + echo "PULL_REQUEST_NUMBER=${{ github.event.number }}" >> ${GITHUB_ENV:?} + else + echo "ERROR: The github.event_name is \"pull_request_target\", but the github.event.action is not \"reopened\"." + exit 1 + fi + else + curl -H "Authorization: token ${GITHUB_TOKEN:?}" "https://api.github.com/users/${{ github.event.sender.login }}" + curl -H "Authorization: token ${GITHUB_TOKEN:?}" "https://api.github.com/users/${{ github.event.sender.login }}/orgs" + export USER_IS_ORGANIZATION_MEMBER=`curl -H "Authorization: token ${GITHUB_TOKEN:?}" "https://api.github.com/users/${{ github.event.sender.login }}/orgs" | jq '[.[] | .login] | index("JuliaLang") != null' | tr -s ' '` + if [[ "${USER_IS_ORGANIZATION_MEMBER:?}" == "true" ]]; then + echo "The \"${{ github.event.sender.login }}\" user is a public member of the JuliaLang organization." + echo "GOOD_TO_PROCEED=yes" >> ${GITHUB_ENV:?} + echo "PULL_REQUEST_NUMBER=${{ github.event.issue.number }}" >> ${GITHUB_ENV:?} + else + echo "ERROR: the \"${{ github.event.sender.login }}\" user is NOT a public member of the JuliaLang organization." + echo "If you are a member, please make sure that you have publicized your membership." + exit 1 + fi + fi + - run: | + echo "GOOD_TO_PROCEED: ${{ env.GOOD_TO_PROCEED }}" + echo "PULL_REQUEST_NUMBER: ${{ env.PULL_REQUEST_NUMBER }}" + - uses: JuliaLang/buildkite-rerun-failed@057f6f2d37aa29a57b7679fd2af0df1d9f9188b4 + if: env.GOOD_TO_PROCEED == 'yes' + with: + buildkite_api_token: ${{ secrets.BUILDKITE_API_TOKEN_RETRY }} + buildkite_organization_slug: 'julialang' + buildkite_pipeline_slug: 'julia-master' + pr_number: ${{ env.PULL_REQUEST_NUMBER }} diff --git a/.github/workflows/statuses.yml b/.github/workflows/statuses.yml new file mode 100644 index 00000000000000..36a694a7c6d20f --- /dev/null +++ b/.github/workflows/statuses.yml @@ -0,0 +1,65 @@ +# Please ping @DilumAluthge when making any changes to this file. + +# This is just a short-term solution until we have migrated all of CI to Buildkite. +# +# 1. TODO: delete this file once we have migrated all of CI to Buildkite. + +# Here are some steps that we take in this workflow file for security reasons: +# 1. We do not checkout any code. +# 2. We do not run any external actions. +# 3. We only give the `GITHUB_TOKEN` the minimum necessary set of permissions. + +name: Create Buildbot Statuses + +on: + push: + branches: + - 'master' + - 'release-*' + # When using the `pull_request_target` event, all PRs will get a `GITHUB_TOKEN` that has + # write permissions, even if the PR is from a fork. + # Therefore, for security reasons, we do not checkout any code in this workflow. + pull_request_target: + types: [opened, synchronize] + branches: + - 'master' + - 'release-*' + +# These are the permissions for the `GITHUB_TOKEN`. +# We should only give the token the minimum necessary set of permissions. +permissions: + statuses: write + +jobs: + create-buildbot-statuses: + name: Create Buildbot Statuses + runs-on: ubuntu-latest + if: github.repository == 'JuliaLang/julia' + steps: + # For security reasons, we do not checkout any code in this workflow. + - run: echo "SHA=${{ github.event.pull_request.head.sha }}" >> $GITHUB_ENV + if: github.event_name == 'pull_request_target' + - run: echo "SHA=${{ github.sha }}" >> $GITHUB_ENV + if: github.event_name != 'pull_request_target' + - run: echo "The SHA is ${{ env.SHA }}" + + # As we incrementally migrate individual jobs from Buildbot to Buildkite, we should + # remove them from the `context_list`. + - run: | + declare -a CONTEXT_LIST=( + "buildbot/tester_freebsd64" + "buildbot/tester_win32" + "buildbot/tester_win64" + ) + for CONTEXT in "${CONTEXT_LIST[@]}" + do + curl \ + -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + -d "{\"context\": \"$CONTEXT\", \"state\": \"$STATE\"}" \ + https://api.github.com/repos/JuliaLang/julia/statuses/${{ env.SHA }} + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + STATE: "pending" diff --git a/.gitignore b/.gitignore index 2da56ff47739e3..2780210c41a9bd 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ *.so *.dylib *.dSYM +*.h.gen *.jl.cov *.jl.*.cov *.jl.mem @@ -31,3 +32,13 @@ /perf* .DS_Store .idea/* +.vscode/* + +# Buildkite: Ignore the entire .buildkite directory +/.buildkite + +# Buildkite: Ignore the unencrypted repo_key +repo_key + +# Buildkite: Ignore any agent keys (public or private) we have stored +agent_key* diff --git a/.mailmap b/.mailmap index bcb3c842a76052..5335c88a63d7d9 100644 --- a/.mailmap +++ b/.mailmap @@ -24,6 +24,8 @@ Viral B. Shah Viral B. Shah Viral B. Shah Viral B. Shah +Viral B. Shah +Viral B. Shah George Xing George Xing @@ -257,3 +259,27 @@ Curtis Vogt Rafael Fourquet Rafael Fourquet + +Nathan Daly +Nathan Daly + +Mosè Giordano +Mosè Giordano + +Andy Ferris +Andy Ferris + +David Varela <00.varela.david@gmail.com> +David Varela + +Arch D. Robison +Arch D. Robison + +Matt Bauman +Matt Bauman + +Daniel Karrasch +Daniel Karrasch + +Roger Luo +Roger Luo diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 665cebadce6b1a..00000000000000 --- a/.travis.yml +++ /dev/null @@ -1,153 +0,0 @@ -language: cpp -sudo: required -dist: trusty -matrix: - include: - - os: linux - env: ARCH="i686" - compiler: "g++-5 -m32" - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - ccache - - libssl1.0.0 - - bar - - time - - binutils - - gcc-5 - - g++-5 - - gcc-5-multilib - - g++-5-multilib - - make:i386 - - libssl-dev:i386 - - gfortran-5 - - gfortran-5-multilib - - os: linux - env: ARCH="x86_64" - compiler: "g++-5 -m64" - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - ccache - - libssl1.0.0 - - bar - - time - - g++-5 - - gfortran-5 - - os: osx - env: ARCH="x86_64" - osx_image: xcode8.3 -cache: ccache -branches: - only: - - master - - /^release-.*/ - - /^v\d+\.\d+\.\d+$/ -notifications: - email: false -before_install: - - make check-whitespace - - if [ `uname` = "Linux" ]; then - contrib/travis_fastfail.sh || exit 1; - mkdir -p $HOME/bin; - ln -s /usr/bin/gcc-5 $HOME/bin/gcc; - ln -s /usr/bin/g++-5 $HOME/bin/g++; - ln -s /usr/bin/gfortran-5 $HOME/bin/gfortran; - ln -s /usr/bin/gcc-5 $HOME/bin/x86_64-linux-gnu-gcc; - ln -s /usr/bin/g++-5 $HOME/bin/x86_64-linux-gnu-g++; - gcc --version; - BAR="bar -i 30"; - BUILDOPTS="-j5 VERBOSE=1 FORCE_ASSERTIONS=1 LLVM_ASSERTIONS=1 USECCACHE=1 USE_BINARYBUILDER_LIBUV=0 USE_BINARYBUILDER_LIBUNWIND=0 USE_BINARYBUILDER_LLVM=0"; - echo "override ARCH=$ARCH" >> Make.user; - sudo sh -c "echo 0 > /proc/sys/net/ipv6/conf/lo/disable_ipv6"; - export JULIA_CPU_THREADS=4; - export JULIA_TEST_MAXRSS_MB=1200; - TESTSTORUN="all"; - elif [ `uname` = "Darwin" ]; then - brew update; - brew install -v jq pv ccache; - export PATH="$(brew --prefix ccache)/libexec:$PATH"; - BAR="pv -i 30"; - contrib/travis_fastfail.sh || exit 1; - brew rm --force gcc gmp mpfr pcre2; - brew install -v gcc gmp mpfr pcre2; - BUILDOPTS="-j3 USECLANG=1 USECCACHE=1 VERBOSE=1 FORCE_ASSERTIONS=1"; - for proj in LLVM LLVM_ASSERTS OPENBLAS SUITESPARSE OPENLIBM; do - BUILDOPTS="$BUILDOPTS USE_BINARYBUILDER_${proj}=1"; - done; - for lib in GMP MPFR LIBUNWIND; do - BUILDOPTS="$BUILDOPTS USE_SYSTEM_$lib=1"; - done; - spawn_DYLD_FALLBACK_LIBRARY_PATH="/usr/local/lib:/lib:/usr/lib"; - export JULIA_MACOS_SPAWN="DYLD_FALLBACK_LIBRARY_PATH=\"$spawn_DYLD_FALLBACK_LIBRARY_PATH\" \$1"; - export BUILDOPTS="$BUILDOPTS spawn=\$(JULIA_MACOS_SPAWN)"; - export JULIA_CPU_THREADS=2; - export JULIA_TEST_MAXRSS_MB=600; - TESTSTORUN="all --skip linalg/triangular subarray"; fi # TODO: re enable these if possible without timing out - - echo "override JULIA_CPU_TARGET=generic;native" >> Make.user - - wget http://http.debian.net/debian/pool/main/m/moreutils/moreutils_0.62.orig.tar.xz - - tar -xJvf moreutils_0.62.orig.tar.xz && mv moreutils-0.62 moreutils -script: - - echo BUILDOPTS=$BUILDOPTS - - export BUILDOPTS - # compile / install dependencies - - contrib/download_cmake.sh - - make -C moreutils mispipe - - make $BUILDOPTS -C base version_git.jl.phony - # capture the log, but only print it if `make deps` fails - # try to show the end of the log first, because this log might be very long (> 4MB) - # and thus be truncated by travis - - moreutils/mispipe "make \$BUILDOPTS NO_GIT=1 -C deps 2> deps-err.log" "$BAR" > deps.log || - { echo "-- deps build log stderr tail 100 --------------------------------------"; - tail -n 100 deps-err.log; - echo "-- deps build log stdout tail 100 --------------------------------------"; - tail -n 100 deps.log; - echo "-- deps build log stderr all -------------------------------------------"; - cat deps-err.log; - echo "-- deps build log stdout all -------------------------------------------"; - cat deps.log; - echo "-- end of deps build log -----------------------------------------------"; - false; } - # compile / install Julia - - make $BUILDOPTS NO_GIT=1 prefix=/tmp/julia release | moreutils/ts -s "%.s" - - make $BUILDOPTS NO_GIT=1 prefix=/tmp/julia install | moreutils/ts -s "%.s" - - make $BUILDOPTS NO_GIT=1 build-stats - - du -sk /tmp/julia/* - - ls -l /tmp/julia/lib - - ls -l /tmp/julia/lib/julia - - FILES_CHANGED=$(git diff --name-only $TRAVIS_COMMIT_RANGE -- || git ls-files) - - cd .. && mv julia julia2 - # run tests - - /tmp/julia/bin/julia --sysimage-native-code=no -e 'true' - # - /tmp/julia/bin/julia-debug --sysimage-native-code=no -e 'true' - - /tmp/julia/bin/julia -e 'Base.require(Main, :InteractiveUtils).versioninfo()' - - pushd /tmp/julia/share/julia/test - # skip tests if only files within the "doc" dir have changed - - if [ $(echo "$FILES_CHANGED" | grep -cv '^doc/') -gt 0 ]; then - /tmp/julia/bin/julia --check-bounds=yes runtests.jl $TESTSTORUN && - /tmp/julia/bin/julia --check-bounds=yes runtests.jl LibGit2/online download; fi - - popd - # test that the embedding code works on our installation - - mkdir /tmp/embedding-test && - make check -C /tmp/julia/share/julia/test/embedding - JULIA="/tmp/julia/bin/julia" - BIN=/tmp/embedding-test - "$(cd julia2 && make print-CC)" - # restore initial state and prepare for travis caching - - mv julia2 julia && - rm -f julia/deps/scratch/libgit2-*/CMakeFiles/CMakeOutput.log - # run the LLVM tests on Linux - - if [ `uname` = "Linux" ]; then - pushd julia && make -C test/llvmpasses && popd; fi - # run the doctests on Linux 64-bit - - if [ `uname` = "Linux" ] && [ $ARCH = "x86_64" ]; then - pushd julia && make -C doc doctest=true && popd; fi -# uncomment the following if failures are suspected to be due to the out-of-memory killer -# - dmesg -after_success: - - if [ `uname` = "Linux" ] && [ $ARCH = "x86_64" ]; then - cd julia && make -C doc deploy; fi diff --git a/CITATION.bib b/CITATION.bib index af8cffe7aa5248..f1361a1eea0b89 100644 --- a/CITATION.bib +++ b/CITATION.bib @@ -8,68 +8,8 @@ @article{Julia-2017 pages={65--98}, year={2017}, publisher={SIAM}, - doi={10.1137/141000671} + doi={10.1137/141000671}, + url={https://epubs.siam.org/doi/10.1137/141000671} } -% The following citations are about specific aspects of Julia. - -@article{Julia-2019-a, - author = {Bezanson, Jeff and Chen, Jiahao and Chung, Benjamin and Karpinski, Stefan and Shah, Viral B. and Vitek, Jan and Zoubritzky, Lionel}, - title = {Julia: Dynamism and Performance Reconciled by Design}, - journal = {Proc. ACM Program. Lang.}, - issue_date = {November 2018}, - volume = {2}, - number = {OOPSLA}, - month = oct, - year = {2018}, - issn = {2475-1421}, - pages = {120:1--120:23}, - articleno = {120}, - numpages = {23}, - url = {https://doi.acm.org/10.1145/3276490}, - doi = {10.1145/3276490}, - acmid = {3276490}, - publisher = {ACM}, - address = {New York, NY, USA}, - keywords = {dynamic languages, just-in-time compilation, multiple dispatch}, -} - -@article{Julia-2019-b, - author = {Zappa Nardelli, Francesco and Belyakova, Julia and Pelenitsyn, Artem and Chung, Benjamin and Bezanson, Jeff and Vitek, Jan}, - title = {Julia Subtyping: A Rational Reconstruction}, - journal = {Proc. ACM Program. Lang.}, - issue_date = {November 2018}, - volume = {2}, - number = {OOPSLA}, - month = oct, - year = {2018}, - issn = {2475-1421}, - pages = {113:1--113:27}, - articleno = {113}, - numpages = {27}, - url = {https://doi.acm.org/10.1145/3276483}, - doi = {10.1145/3276483}, - acmid = {3276483}, - publisher = {ACM}, - address = {New York, NY, USA}, - keywords = {Multiple Dispatch, Subtyping}, -} - -@inproceedings{Julia-2014, - author = {Bezanson, Jeff and Chen, Jiahao and Karpinski, Stefan and Shah, Viral and Edelman, Alan}, - title = {Array Operators Using Multiple Dispatch: A Design Methodology for Array Implementations in Dynamic Languages}, - booktitle = {Proceedings of ACM SIGPLAN International Workshop on Libraries, Languages, and Compilers for Array Programming}, - series = {ARRAY'14}, - year = {2014}, - isbn = {978-1-4503-2937-8}, - location = {Edinburgh, United Kingdom}, - pages = {56:56--56:61}, - articleno = {56}, - numpages = {6}, - url = {https://doi.acm.org/10.1145/2627373.2627383}, - doi = {10.1145/2627373.2627383}, - acmid = {2627383}, - publisher = {ACM}, - address = {New York, NY, USA}, - keywords = {Julia, array indexing, dynamic dispatch, multiple dispatch, static analysis, type inference}, -} +% For more details on research related to Julia, see https://julialang.org/research diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 00000000000000..c88727bcfa3114 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,40 @@ +cff-version: 1.2.0 +message: "Cite this paper whenever you use Julia" +authors: +- family-names: "Bezanson" + given-names: "Jeff" +- family-names: "Edelman" + given-names: "Alan" +- family-names: "Karpinski" + given-names: "Stefan" +- family-names: "Shah" + given-names: "Viral B." +title: "Julia: A fresh approach to numerical computing" +version: "v1" +license: "MIT" +doi: "10.1137/141000671" +date-released: 2017-02-07 +url: "https://julialang.org" +preferred-citation: + authors: + - family-names: "Bezanson" + given-names: "Jeff" + - family-names: "Edelman" + given-names: "Alan" + - family-names: "Karpinski" + given-names: "Stefan" + - family-names: "Shah" + given-names: "Viral B." + doi: "10.1137/141000671" + journal: "SIAM Review" + month: 9 + start: 65 + end: 98 + pages: 33 + title: "Julia: A fresh approach to numerical computing" + type: article + volume: 59 + issue: 1 + year: 2017 + publisher: + name: "SIAM" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63ea55230ddd82..2c924b2cdabb9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ If you are already familiar with Julia itself, this blog post by Katharine Hyatt ## Learning Julia -[The learning page](https://julialang.org/learning) has a great list of resources for new and experienced users alike. [This tutorial video](https://www.youtube.com/watch?v=vWkgEddb4-A) is one recommended starting point, as is the "[Invitation to Julia](https://www.youtube.com/watch?v=gQ1y5NUD_RI)" workshop video from JuliaCon 2015 ([slide materials here](https://github.com/dpsanders/invitation_to_julia)). The [Julia documentation](https://docs.julialang.org) covers the language and core library features, and is searchable. +[The learning page](https://julialang.org/learning) has a great list of resources for new and experienced users alike. ## Before filing an issue @@ -90,7 +90,7 @@ from Julia's root directory. This will rebuild the Julia system image, then inst > **Note** > -> When making changes to any of Julia's documentation it is recommended that you run `make docs` to check the your changes are valid and do not produce any errors before opening a pull request. +> When making changes to any of Julia's documentation it is recommended that you run `make docs` to check that your changes are valid and do not produce any errors before opening a pull request. Below are outlined the three most common types of documentation changes and the steps required to perform them. Please note that the following instructions do not cover the full range of features provided by Documenter.jl. Refer to [Documenter's documentation](https://juliadocs.github.io/Documenter.jl/stable) if you encounter anything that is not covered by the sections below. @@ -158,7 +158,9 @@ Examples written within docstrings can be used as testcases known as "doctests" "DOCSTRING TEST" ``` -A doctest needs to match an interactive REPL including the `julia>` prompt. To run doctests you need to run `make -C doc doctest=true` from the root directory. It is recommended to add the header `# Examples` above the doctests. +A doctest needs to match an interactive REPL including the `julia>` prompt. It is recommended to add the header `# Examples` above the doctests. + +To run doctests you need to run `make -C doc doctest=true` from the root directory. You can use `make -C doc doctest=true revise=true` if you are modifying the doctests and don't want to rebuild Julia after each change (see details below about the Revise.jl workflow). #### News-worthy changes @@ -189,7 +191,7 @@ Note: These instructions are for adding to or improving functionality in the bas Add new code to Julia's base libraries as follows (this is the "basic" approach; see a more efficient approach in the next section): - 1. Edit the appropriate file in the `base/` directory, or add new files if necessary. Create tests for your functionality and add them to files in the `test/` directory. If you're editing C or Scheme code, most likely it lives in `src/` or one of its subdirectories, although some aspects of Julia's REPL initialization live in `ui/`. + 1. Edit the appropriate file in the `base/` directory, or add new files if necessary. Create tests for your functionality and add them to files in the `test/` directory. If you're editing C or Scheme code, most likely it lives in `src/` or one of its subdirectories, although some aspects of Julia's REPL initialization live in `cli/`. 2. Add any new files to `sysimg.jl` in order to build them into the Julia system image. @@ -207,8 +209,6 @@ or with the `runtests.jl` script, e.g. to run `test/bitarray.jl` and `test/math. ./usr/bin/julia test/runtests.jl bitarray math -Make sure that [Travis](https://www.travis-ci.org) greenlights the pull request with a [`Good to merge` message](https://blog.travis-ci.com/2012-09-04-pull-requests-just-got-even-more-awesome). - #### Modifying base more efficiently with Revise.jl [Revise](https://github.com/timholy/Revise.jl) is a package that @@ -248,6 +248,51 @@ process before running the corresponding test. This can be useful as a shortcut on the command line (since tests aren't always designed to be run outside the runtest harness). +### Contributing to patch releases + +The process of creating a patch release is roughly as follows: + +1. Create a new branch (e.g. `backports-release-1.6`) against the relevant minor release + branch (e.g. `release-1.6`). Usually a corresponding pull request is created as well. + +2. Add commits, nominally from `master` (hence "backports"), to that branch. + See below for more information on this process. + +3. Run the [BaseBenchmarks.jl](https://github.com/JuliaCI/BaseBenchmarks.jl) benchmark + suite and [PkgEval.jl](https://github.com/JuliaCI/PkgEval.jl) package ecosystem + exerciser against that branch. Nominally BaseBenchmarks.jl and PkgEval.jl are + invoked via [Nanosoldier.jl](https://github.com/JuliaCI/Nanosoldier.jl) from + the pull request associated with the backports branch. Fix any issues. + +4. Once all test and benchmark reports look good, merge the backports branch into + the corresponding release branch (e.g. merge `backports-release-1.6` into + `release-1.6`). + +5. Open a pull request that bumps the version of the relevant minor release to the + next patch version, e.g. as in [this pull request](https://github.com/JuliaLang/julia/pull/37718). + +6. Ping `@JuliaLang/releases` to tag the patch release and update the website. + +7. Open a pull request that bumps the version of the relevant minor release to the + next prerelase patch version, e.g. as in [this pull request](https://github.com/JuliaLang/julia/pull/37724). + +Step 2 above, i.e. backporting commits to the `backports-release-X.Y` branch, has largely +been automated via [`Backporter`](https://github.com/KristofferC/Backporter): Backporter +searches for merged pull requests with the relevant `backport-X.Y` tag, and attempts to +cherry-pick the commits from those pull requests onto the `backports-release-X.Y` branch. +Some commits apply successfully without intervention, others not so much. The latter +commits require "manual" backporting, with which help is generally much appreciated. +Backporter generates a report identifying those commits it managed to backport automatically +and those that require manual backporting; this report is usually copied into the first +post of the pull request associated with `backports-release-X.Y` and maintained as +additional commits are automatically and/or manually backported. + +When contributing a manual backport, if you have the necessary permissions, please push the +backport directly to the `backports-release-X.Y` branch. If you lack the relevant +permissions, please open a pull request against the `backports-release-X.Y` branch with the +manual backport. Once the manual backport is live on the `backports-release-X.Y` branch, +please remove the `backport-X.Y` tag from the originating pull request for the commits. + ### Code Formatting Guidelines #### General Formatting Guidelines for Julia code contributions @@ -289,6 +334,11 @@ runtest harness). - To remove whitespace relative to the `master` branch, run `git rebase --whitespace=fix master`. +#### Git Recommendations For Pull Request Reviewers + +- When merging, we generally like `squash+merge`. Unless it is the rare case of a PR with carefully staged individual commits that you want in the history separately, in which case `merge` is acceptable, but usually prefer `squash+merge`. + + ## Resources * Julia @@ -299,7 +349,7 @@ runtest harness). - **Code coverage:** * Design of Julia - - [Julia: A Fresh Approach to Numerical Computing](https://julialang.org/research/julia-fresh-approach-BEKS.pdf) + - [Julia: A Fresh Approach to Numerical Computing](https://julialang.org/assets/research/julia-fresh-approach-BEKS.pdf) - [Julia: Dynamism and Performance Reconciled by Design](http://janvitek.org/pubs/oopsla18b.pdf) - [All Julia Publications](https://julialang.org/research) diff --git a/HISTORY.md b/HISTORY.md index 3e9142291dc91a..1fcb416d4d47f3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,1022 @@ +Julia v1.8 Release Notes +======================== + +New language features +--------------------- + +* Mutable struct fields may now be annotated as `const` to prevent changing them after construction, + providing for greater clarity and optimization ability of these objects ([#43305]). +* Type annotations can now be added to global variables to make accessing them type stable ([#43671]). +* Empty n-dimensional arrays can now be created using multiple semicolons inside square brackets, + e.g. `[;;;]` creates a 0×0×0 `Array` ([#41618]). +* `try`-blocks can now optionally have an `else`-block which is executed right after the main body only if + no errors were thrown ([#42211]). +* `@inline` and `@noinline` annotations can now be placed within a function body ([#41312]). +* `@inline` and `@noinline` annotations can now be applied to a function call site or block + to enforce the involved function calls to be (or not to be) inlined ([#41328]). +* `∀`, `∃`, and `∄` are now allowed as identifier characters ([#42314]). +* Support for Unicode 14.0.0 ([#43443]). +* `Module(:name, false, false)` can be used to create a `module` that contains no names + (it does not import `Base` or `Core` and does not contain a reference to itself) ([#40110], [#42154]). + +Language changes +---------------- + +* Newly-created Task objects (`@spawn`, `@async`, etc.) now adopt the world age for methods from their parent + Task upon creation, instead of using the global latest world at start. This is done to enable inference to + eventually optimize these calls. Places that wish for the old behavior may use `Base.invokelatest` ([#41449]). +* Unbalanced Unicode bidirectional formatting directives are now disallowed within strings and comments, + to mitigate the ["trojan source"](https://www.trojansource.codes) vulnerability ([#42918]). +* `Base.ifelse` is now defined as a generic function rather than a builtin one, allowing packages to + extend its definition ([#37343]). +* Every assignment to a global variable now first goes through a call to `convert(Any, x)` (or `convert(T, x)` + respectively if a type `T` has been declared for the global). This means great care should be taken + to ensure the invariant `convert(Any, x) === x` always holds, as this change could otherwise lead to + unexpected behavior ([#43671]). +* Builtin functions are now a bit more like generic functions, and can be enumerated with `methods` ([#43865]). + +Compiler/Runtime improvements +----------------------------- + +* Bootstrapping time has been improved by about 25% ([#41794]). +* The LLVM-based compiler has been separated from the run-time library into a new library, + `libjulia-codegen`. It is loaded by default, so normal usage should see no changes. + In deployments that do not need the compiler (e.g. system images where all needed code + is precompiled), this library (and its LLVM dependency) can simply be excluded ([#41936]). +* Conditional type constraints can now be forwarded interprocedurally (i.e. propagated from caller to callee) ([#42529]). +* Julia-level SROA (Scalar Replacement of Aggregates) has been improved: allowing elimination of + `getfield` calls with constant global fields ([#42355]), enabling elimination of mutable structs with + uninitialized fields ([#43208]), improving performance ([#43232]), and handling more nested `getfield` + calls ([#43239]). +* Abstract call sites can now be inlined or statically resolved as long as the call site has a single + matching method ([#43113]). +* Inference now tracks various effects such as side-effectful-ness and nothrow-ness on a per-specialization basis. + Code heavily dependent on constant propagation should see significant compile-time performance improvements and + certain cases (e.g. calls to uninlinable functions that are nevertheless effect free) should see runtime performance + improvements. Effects may be overwritten manually with the `@Base.assume_effects` macro ([#43852]). + +Command-line option changes +--------------------------- + +* The default behavior of observing `@inbounds` declarations is now an option via `auto` in `--check-bounds=yes|no|auto` ([#41551]). +* New option `--strip-metadata` to remove docstrings, source location information, and local + variable names when building a system image ([#42513]). +* New option `--strip-ir` to remove the compiler's IR (intermediate representation) of source + code when building a system image. The resulting image will only work if `--compile=all` is + used, or if all needed code is precompiled ([#42925]). +* When the program file is `-` the code to be executed is read from standard in ([#43191]). + +Multi-threading changes +----------------------- + +* `Threads.@threads` now defaults to a new `:dynamic` schedule option which is similar to the previous behavior except + that iterations will be scheduled dynamically to available worker threads rather than pinned to each thread. This + behavior is more composable with (possibly nested) `@spawn` and `@threads` loops ([#43919], [#44136]). + +Build system changes +-------------------- + + +New library functions +--------------------- + +* New function `eachsplit(str)` for iteratively performing `split(str)` ([#39245]). +* New function `allequal(itr)` for testing if all elements in an iterator are equal ([#43354]). +* `hardlink(src, dst)` can be used to create hard links ([#41639]). +* `setcpuaffinity(cmd, cpus)` can be used to set CPU affinity of sub-processes ([#42469]). +* `diskstat(path=pwd())` can be used to return statistics about the disk ([#42248]). +* New `@showtime` macro to show both the line being evaluated and the `@time` report ([#42431]). +* The `LazyString` and the `lazy"str"` macro were added to support delayed construction of error messages in error paths ([#33711]). + +New library features +-------------------- + +* `@time` and `@timev` now take an optional description to allow annotating the source of time reports, + e.g. `@time "Evaluating foo" foo()` ([#42431]). +* `range` accepts either `stop` or `length` as a sole keyword argument ([#39241]). +* `precision` and `setprecision` now accept a `base` keyword argument ([#42428]). +* TCP socket objects now expose `closewrite` functionality and support half-open mode usage ([#40783]). +* `extrema` now accepts an `init` keyword argument ([#36265], [#43604]). +* `Iterators.countfrom` now accepts any type that defines `+` ([#37747]). + +Standard library changes +------------------------ + +* Keys with value `nothing` are now removed from the environment in `addenv` ([#43271]). +* `Iterators.reverse` (and hence `last`) now supports `eachline` iterators ([#42225]). +* The `length` function on certain ranges of certain element types no longer checks for integer + overflow in most cases. The new function `checked_length` is now available, which will try to use checked + arithmetic to error if the result may be wrapping. Or use a package such as SaferIntegers.jl when + constructing the range ([#40382]). +* Intersect returns a result with the eltype of the type-promoted eltypes of the two inputs ([#41769]). +* Iterating an `Iterators.Reverse` now falls back on reversing the eachindex iterator, if possible ([#43110]). + +#### InteractiveUtils + +* New macro `@time_imports` for reporting any time spent importing packages and their dependencies ([#41612]). + +#### LinearAlgebra + +* The BLAS submodule now supports the level-2 BLAS subroutine `spr!` ([#42830]). +* `cholesky[!]` now supports `LinearAlgebra.PivotingStrategy` (singleton type) values + as its optional `pivot` argument: the default is `cholesky(A, NoPivot())` (vs. + `cholesky(A, RowMaximum())`); the former `Val{true/false}`-based calls are deprecated ([#41640]). +* The standard library `LinearAlgebra.jl` is now completely independent of `SparseArrays.jl`, + both in terms of the source code as well as unit testing ([#43127]). As a consequence, + sparse arrays are no longer (silently) returned by methods from `LinearAlgebra` applied + to `Base` or `LinearAlgebra` objects. Specifically, this results in the following breaking + changes: + * Concatenations involving special "sparse" matrices (`*diagonal`) now return dense matrices; + As a consequence, the `D1` and `D2` fields of `SVD` objects, constructed upon `getproperty` + calls are now dense matrices. + * 3-arg `similar(::SpecialSparseMatrix, ::Type, ::Dims)` returns a dense zero matrix. + As a consequence, products of bi-, tri- and symmetric tridiagonal matrices with each + other result in dense output. Moreover, constructing 3-arg similar matrices of special + "sparse" matrices of (nonstatic) matrices now fails for the lack of `zero(::Type{Matrix{T}})`. + +#### Printf + +* Now uses `textwidth` for formatting `%s` and `%c` widths ([#41085]). + +#### Profile + +* CPU profiling now records sample metadata including thread and task. `Profile.print()` has a new `groupby` kwarg that allows + grouping by thread, task, or nested thread/task, task/thread, and `threads` and `tasks` kwargs to allow filtering. + Further, percent utilization is now reported as a total or per-thread, based on whether the thread is idle or not at + each sample. `Profile.fetch()` includes the new metadata by default. For backwards compatibility with external + profiling data consumers, it can be excluded by passing `include_meta=false` ([#41742]). +* The new `Profile.Allocs` module allows memory allocations to be profiled. The stack trace, type, and size of each + allocation is recorded, and a `sample_rate` argument allows a tunable amount of allocations to be skipped, + reducing performance overhead ([#42768]). +* A fixed duration cpu profile can now be triggered by the user during running tasks without `Profile` being loaded + first and the report will show during execution. On MacOS & FreeBSD press `ctrl-t` or raise a `SIGINFO`. + For other platforms raise a `SIGUSR1` i.e. `% kill -USR1 $julia_pid`. Not currently available on windows ([#43179]). + +#### REPL + +* `RadioMenu` now supports optional `keybindings` to directly select options ([#41576]). +* ` ?(x, y` followed by TAB displays all methods that can be called + with arguments `x, y, ...`. (The space at the beginning prevents entering help-mode.) + `MyModule.?(x, y` limits the search to `MyModule`. TAB requires that at least one + argument have a type more specific than `Any`; use SHIFT-TAB instead of TAB + to allow any compatible methods ([#38791]). +* New `err` global variable in `Main` set when an expression throws an exception, akin to `ans`. Typing `err` reprints + the exception information ([#40642]). + +#### SparseArrays + +* The code for SparseArrays has been moved from the Julia repo to the external + repo at https://github.com/JuliaSparse/SparseArrays.jl. This is only a code + movement and does not impact any usage ([#43813]). +* New sparse concatenation functions `sparse_hcat`, `sparse_vcat`, and `sparse_hvcat` return + `SparseMatrixCSC` output independent from the types of the input arguments. They make + concatenation behavior available, in which the presence of some special "sparse" matrix + argument resulted in sparse output by multiple dispatch. This is no longer possible after + making `LinearAlgebra.jl` independent from `SparseArrays.jl` ([#43127]). + +#### Logging + +* The standard log levels `BelowMinLevel`, `Debug`, `Info`, `Warn`, `Error`, + and `AboveMaxLevel` are now exported from the Logging stdlib ([#40980]). + +#### Unicode + +* Added function `isequal_normalized` to check for Unicode equivalence without + explicitly constructing normalized strings ([#42493]). +* The `Unicode.normalize` function now accepts a `chartransform` keyword that can + be used to supply custom character mappings, and a `Unicode.julia_chartransform` + function is provided to reproduce the mapping used in identifier normalization + by the Julia parser ([#42561]). + +#### Test + +* `@test_throws "some message" triggers_error()` can now be used to check whether the displayed error text + contains "some message" regardless of the specific exception type. + Regular expressions, lists of strings, and matching functions are also supported ([#41888]). +* `@testset foo()` can now be used to create a test set from a given function. The name of the test set + is the name of the called function. The called function can contain `@test` and other `@testset` + definitions, including to other function calls, while recording all intermediate test results ([#42518]). +* `TestLogger` and `LogRecord` are now exported from the Test stdlib ([#44080]). + +Deprecated or removed +--------------------- + + +External dependencies +--------------------- + + +Tooling Improvements +--------------------- + +* `GC.enable_logging(true)` can be used to log each garbage collection, with the + time it took and the amount of memory that was collected ([#43511]). + + +[#33711]: https://github.com/JuliaLang/julia/issues/33711 +[#36265]: https://github.com/JuliaLang/julia/issues/36265 +[#37343]: https://github.com/JuliaLang/julia/issues/37343 +[#37747]: https://github.com/JuliaLang/julia/issues/37747 +[#38791]: https://github.com/JuliaLang/julia/issues/38791 +[#39241]: https://github.com/JuliaLang/julia/issues/39241 +[#39245]: https://github.com/JuliaLang/julia/issues/39245 +[#40110]: https://github.com/JuliaLang/julia/issues/40110 +[#40382]: https://github.com/JuliaLang/julia/issues/40382 +[#40642]: https://github.com/JuliaLang/julia/issues/40642 +[#40783]: https://github.com/JuliaLang/julia/issues/40783 +[#40980]: https://github.com/JuliaLang/julia/issues/40980 +[#41085]: https://github.com/JuliaLang/julia/issues/41085 +[#41312]: https://github.com/JuliaLang/julia/issues/41312 +[#41328]: https://github.com/JuliaLang/julia/issues/41328 +[#41449]: https://github.com/JuliaLang/julia/issues/41449 +[#41551]: https://github.com/JuliaLang/julia/issues/41551 +[#41576]: https://github.com/JuliaLang/julia/issues/41576 +[#41612]: https://github.com/JuliaLang/julia/issues/41612 +[#41618]: https://github.com/JuliaLang/julia/issues/41618 +[#41639]: https://github.com/JuliaLang/julia/issues/41639 +[#41640]: https://github.com/JuliaLang/julia/issues/41640 +[#41742]: https://github.com/JuliaLang/julia/issues/41742 +[#41769]: https://github.com/JuliaLang/julia/issues/41769 +[#41794]: https://github.com/JuliaLang/julia/issues/41794 +[#41888]: https://github.com/JuliaLang/julia/issues/41888 +[#41936]: https://github.com/JuliaLang/julia/issues/41936 +[#42154]: https://github.com/JuliaLang/julia/issues/42154 +[#42211]: https://github.com/JuliaLang/julia/issues/42211 +[#42225]: https://github.com/JuliaLang/julia/issues/42225 +[#42248]: https://github.com/JuliaLang/julia/issues/42248 +[#42314]: https://github.com/JuliaLang/julia/issues/42314 +[#42355]: https://github.com/JuliaLang/julia/issues/42355 +[#42428]: https://github.com/JuliaLang/julia/issues/42428 +[#42431]: https://github.com/JuliaLang/julia/issues/42431 +[#42469]: https://github.com/JuliaLang/julia/issues/42469 +[#42493]: https://github.com/JuliaLang/julia/issues/42493 +[#42513]: https://github.com/JuliaLang/julia/issues/42513 +[#42518]: https://github.com/JuliaLang/julia/issues/42518 +[#42529]: https://github.com/JuliaLang/julia/issues/42529 +[#42561]: https://github.com/JuliaLang/julia/issues/42561 +[#42768]: https://github.com/JuliaLang/julia/issues/42768 +[#42830]: https://github.com/JuliaLang/julia/issues/42830 +[#42918]: https://github.com/JuliaLang/julia/issues/42918 +[#42925]: https://github.com/JuliaLang/julia/issues/42925 +[#43110]: https://github.com/JuliaLang/julia/issues/43110 +[#43113]: https://github.com/JuliaLang/julia/issues/43113 +[#43127]: https://github.com/JuliaLang/julia/issues/43127 +[#43179]: https://github.com/JuliaLang/julia/issues/43179 +[#43191]: https://github.com/JuliaLang/julia/issues/43191 +[#43208]: https://github.com/JuliaLang/julia/issues/43208 +[#43232]: https://github.com/JuliaLang/julia/issues/43232 +[#43239]: https://github.com/JuliaLang/julia/issues/43239 +[#43271]: https://github.com/JuliaLang/julia/issues/43271 +[#43305]: https://github.com/JuliaLang/julia/issues/43305 +[#43354]: https://github.com/JuliaLang/julia/issues/43354 +[#43443]: https://github.com/JuliaLang/julia/issues/43443 +[#43511]: https://github.com/JuliaLang/julia/issues/43511 +[#43604]: https://github.com/JuliaLang/julia/issues/43604 +[#43671]: https://github.com/JuliaLang/julia/issues/43671 +[#43813]: https://github.com/JuliaLang/julia/issues/43813 +[#43852]: https://github.com/JuliaLang/julia/issues/43852 +[#43865]: https://github.com/JuliaLang/julia/issues/43865 +[#43919]: https://github.com/JuliaLang/julia/issues/43919 +[#44080]: https://github.com/JuliaLang/julia/issues/44080 +[#44136]: https://github.com/JuliaLang/julia/issues/44136 + +Julia v1.7 Release Notes +======================== + +New language features +--------------------- + +* `(; a, b) = x` can now be used to destructure properties `a` and `b` of `x`. + This syntax is equivalent to `a = getproperty(x, :a); b = getproperty(x, :b)` ([#39285]). +* Implicit multiplication by juxtaposition is now allowed for radical symbols (e.g. `x√y` and `x∛y`) ([#40173]). +* The short-circuiting operators `&&` and `||` can now be dotted to participate in broadcast fusion + as `.&&` and `.||` ([#39594]). +* `⫪` (U+2AEA, `\Top`, `\downvDash`) and `⫫` (U+2AEB, `\Bot`, `\upvDash`, `\indep`) + may now be used as binary operators with comparison precedence ([#39403]). +* Repeated semicolons can now be used inside array concatenation expressions to separate dimensions + of an array, with the number of semicolons specifying the dimension. Just as a single semicolon + in `[A; B]` has always described concatenating in the first dimension (vertically), now two + semicolons `[A;; B]` do so in the second dimension (horizontally), three semicolons `;;;` in the + third, and so on ([#33697]). +* A backslash (`\`) before a newline inside a string literal now removes the newline while also + respecting indentation. This can be used to split up long strings without newlines into multiple + lines of code ([#40753]). +* A backslash before a newline in command literals now always removes the newline, similar to standard string + literals, whereas the result was not well-defined before ([#40753]). + +Language changes +---------------- + +* `macroexpand`, `@macroexpand`, and `@macroexpand1` no longer wrap errors in a `LoadError`. + To reduce breakage, `@test_throws` has been modified so that many affected tests will still pass ([#38379]). +* The middle dot `·` (`\cdotp` U+00b7) and the Greek interpunct `·` (U+0387) are now treated as equivalent to + the dot operator `⋅` (`\cdot` U+22c5) (#25157). +* The minus sign `−` (`\minus` U+2212) is now treated as equivalent to the hyphen-minus sign `-` (U+002d) ([#40948]). +* Destructuring will no longer mutate values on the left-hand side while iterating through values on + the right-hand side. In the example of an array `x`, `x[2], x[1] = x` will now swap the first and + second elements of `x`, whereas it used to fill both entries with `x[1]` because `x[2]` was mutated during + the iteration of `x` ([#40737]). +* The default random number generator has changed, so all random numbers will be different (even with the + same seed) unless an explicit RNG object is used. + See the section on the `Random` standard library below ([#40546]). +* `Iterators.peel(itr)` now returns `nothing` when `itr` is empty instead of throwing a `BoundsError` ([#39607]). +* Multiple successive semicolons in an array expresion were previously ignored (e.g., `[1 ;; 2] == [1 ; 2]`). + This syntax is now used to separate dimensions (see **New language features**). + +Compiler/Runtime improvements +----------------------------- + + +Command-line option changes +--------------------------- + +* The Julia `--project` option and the `JULIA_PROJECT` environment variable now support selecting shared + environments like `.julia/environments/myenv` the same way the package management console does: + use `julia --project=@myenv` resp. `export JULIA_PROJECT="@myenv"` ([#40025]). + +Multi-threading changes +----------------------- + +* Intrinsics for atomic pointer operations are now defined for certain byte sizes ([#37847]). +* Support for declaring and using individual fields of a mutable struct as atomic has been + added; see the new `@atomic` macro ([#37847]). +* If the `JULIA_NUM_THREADS` environment variable is set to `auto`, then the + number of threads will be set to the number of CPU threads ([#38952]). +* Every `Task` object has a local random number generator state, providing + reproducible (schedule-independent) execution of parallel simulation code by + default. The default generator is also significantly faster in parallel than + in previous versions ([#40546]). +* Tasks can now migrate among threads when they are re-scheduled. Previously, a Task + would always run on whichever thread executed it first ([#40715]). + +Build system changes +-------------------- + + +New library functions +--------------------- + +* Two argument methods `findmax(f, domain)`, `argmax(f, domain)` and the corresponding + `min` versions ([#35316]). +* `isunordered(x)` returns true if `x` is a value that is normally unordered, such as + `NaN` or `missing` ([#35316]). +* New `keepat!(vector, inds)` function which is the inplace equivalent of `vector[inds]` + for a list `inds` of integers ([#36229]). +* Two arguments method `lock(f, lck)` now accepts a `Channel` as the second argument ([#39312]). +* New functor `Returns(value)`, which returns `value` for any arguments ([#39794]). +* New macros `@something` and `@coalesce` which are short-circuiting versions of `something` and + `coalesce`, respectively ([#40729]). +* New function `redirect_stdio` for redirecting `stdin`, `stdout` and `stderr` ([#37978]). +* New macro `Base.@invoke f(arg1::T1, arg2::T2; kwargs...)` provides an easier syntax to call + `invoke(f, Tuple{T1,T2}, arg1, arg2; kwargs...)` ([#38438]). +* New macro `Base.@invokelatest f(args...; kwargs...)` providing a convenient way to call + `Base.invokelatest(f, args...; kwargs...)` ([#37971]). + +New library features +-------------------- + +* The optional keyword argument `context` of `sprint` can now be set to a tuple of `:key => value` + pairs to specify multiple attributes ([#39381]). +* `bytes2hex` and `hex2bytes` are no longer limited to arguments of type `Union{String,AbstractVector{UInt8}}` + and now only require that they're iterable and have a length ([#39710]). +* `stat(file)` now has a more detailed and user-friendly `show` method ([#39463]). + +Standard library changes +------------------------ + +* `count` and `findall` now accept an `AbstractChar` argument to search for a character in + a string ([#38675]). +* New methods `range(start, stop)` and `range(start, stop, length)` ([#39228]). +* `range` now supports `start` as an optional keyword argument ([#38041]). +* Some operations on ranges will return a `StepRangeLen` instead of a `StepRange`, to allow + the resulting step to be zero. Previously, `λ .* (1:9)` gave an error when `λ = 0` ([#40320]). +* `islowercase` and `isuppercase` are now compliant with the Unicode lower/uppercase categories ([#38574]). +* `iseven` and `isodd` functions now support non-`Integer` numeric types ([#38976]). +* `escape_string` now accepts a collection of characters via the keyword + `keep` that are to be kept as they are ([#38597]). +* `getindex` for `NamedTuple`s now accepts a tuple of symbols in order to index multiple values ([#38878]). +* Subtypes of `AbstractRange` now correctly follow the general array indexing behavior when indexed by + `Bool`s, erroring for scalar `Bool`s and treating arrays (including ranges) of `Bool` as + logical indices ([#31829]). +* `keys(::RegexMatch)` is now defined to return the capture's keys, by name if named, or by index if not ([#37299]). +* `keys(::Generator)` is now defined to return the iterator's keys ([#34678]). +* `RegexMatch` is now iterable, giving the captured substrings ([#34355]). +* `lpad/rpad` are now defined in terms of `textwidth` ([#39044]). +* `Test.@test` now accepts `broken` and `skip` boolean keyword arguments, which + mimic `Test.@test_broken` and `Test.@test_skip` behavior, but allows skipping + tests failing only under certain conditions. For example + ```julia + if T == Float64 + @test_broken isequal(complex(one(T)) / complex(T(Inf), T(-Inf)), complex(zero(T), zero(T))) + else + @test isequal(complex(one(T)) / complex(T(Inf), T(-Inf)), complex(zero(T), zero(T))) + end + ``` + can be replaced by + ```julia + @test isequal(complex(one(T)) / complex(T(Inf), T(-Inf)), complex(zero(T), zero(T))) broken=(T == Float64) + ``` + ([#39322]). +* `@lock` is now exported from Base ([#39588]). +* The experimental function `Base.catch_stack()` has been renamed to `current_exceptions()`, exported + from Base and given a more specific return type ([#29901]). +* Some degree trigonometric functions, `sind`, `cosd`, `tand`, `asind`, `acosd`, `asecd`, `acscd`, + `acotd`, `atand` now accept a square matrix ([#39758]). +* `replace(::String)` now accepts multiple patterns, which will be applied left-to-right simultaneously, + so only one pattern will be applied to any character, and the patterns will only be applied to the input + text, not the replacements ([#40484]). +* New `replace` methods to replace elements of a `Tuple` ([#38216]). + + +#### Package Manager + +* If a package is `using` or `import`ed from the `julia>` prompt that isn't found but is available + from a registry, a `pkg> add` prompt now offers to install the package into the current environment, + precompile it, and continue to load it ([#39026]). +* A new `Manifest.toml` format is now used that captures extensible metadata fields, including the + julia version that generated the manifest. Old format manifests are still supported and will be + maintained in their original format, unless the user runs `Pkg.upgrade_manifest()` to upgrade the + format of the current environment's manifest without re-resolving ([#40765]). +* `pkg> precompile` will now precompile new versions of packages that are already loaded, rather than + postponing to the next session (the `?`-marked dependencies) ([#40345]). +* `pkg> rm`, `pin`, and `free` now accept the `--all` argument to call the action on all packages. +* Registries downloaded from the Pkg Server (not git) are no longer uncompressed into files but instead + read directly from the compressed tarball into memory. This improves performance on + filesystems which do not handle a large number of files well. To turn this feature off, set the + environment variable `JULIA_PKG_UNPACK_REGISTRY=true`. +* It is now possible to use an external `git` executable instead of the default libgit2 library + for the downloads that happen via the Git protocol by setting the environment variable + `JULIA_PKG_USE_CLI_GIT=true`. +* Registries downloaded from the Pkg Server (not git) is now assumed to be immutable. Manual changes + to their files might not be picked up by a running Pkg session. +* Adding packages by directory name in the REPL mode now requires prepending `./` to the name if the + package is in the current directory; e.g. `add ./Package` is required instead of `add Package`. + This is to avoid confusion between the package name `Package` and the local directory `Package`. +* The `mode` keyword for `PackageSpec` has been removed. + +#### LinearAlgebra + +* Use [Libblastrampoline](https://github.com/staticfloat/libblastrampoline/) to pick a BLAS + and LAPACK at runtime. By default it forwards to OpenBLAS in the Julia distribution. + The forwarding mechanism can be used by packages to replace the BLAS and LAPACK with + user preferences ([#39455]). +* On aarch64, OpenBLAS now uses an ILP64 BLAS like all other 64-bit platforms ([#39436]). +* OpenBLAS is updated to 0.3.13 ([#39216]). +* SuiteSparse is updated to 5.8.1 ([#39455]). +* The shape of an `UpperHessenberg` matrix is preserved under certain arithmetic operations, + e.g. when multiplying or dividing by an `UpperTriangular` matrix ([#40039]). +* Real quasitriangular Schur factorizations `S` can now be efficiently converted to complex + upper-triangular form with `Schur{Complex}(S)` ([#40573]). +* `cis(A)` now supports matrix arguments ([#40194]). +* `dot` now supports `UniformScaling` with `AbstractMatrix` ([#40250]). +* `qr[!]` and `lu[!]` now support `LinearAlgebra.PivotingStrategy` (singleton type) values + as their optional `pivot` argument: defaults are `qr(A, NoPivot())` (vs. `qr(A, ColumnNorm())` + for pivoting) and `lu(A, RowMaximum())` (vs. `lu(A, NoPivot())` without pivoting); the former + `Val{true/false}`-based calls are deprecated ([#40623]). +* `det(M::AbstractMatrix{BigInt})` now calls `det_bareiss(M)`, which uses the + [Bareiss](https://en.wikipedia.org/wiki/Bareiss_algorithm) algorithm to calculate precise + values ([#40868]). + +#### Markdown + + +#### Printf + + +#### Random + +* The default random number generator has been changed from Mersenne Twister to + [Xoshiro256++](https://prng.di.unimi.it/). + The new generator has smaller state, better performance, and superior statistical properties. + This generator is the one used for reproducible Task-local randomness ([#40546]). + +#### REPL + +* Long strings are now elided using the syntax `"head" ⋯ 12345 bytes ⋯ "tail"` when displayed + in the REPL ([#40736]). +* Pasting repl examples into the repl (prompt pasting) now supports all repl modes (`julia`, `pkg`, + `shell`, `help?`) and switches mode automatically ([#40604]). +* `help?>` for modules without docstrings now returns a list of exported names and prints + the contents of an associated `README.md` if found ([#39093]). + +#### SparseArrays + +* new `sizehint!(::SparseMatrixCSC, ::Integer)` method ([#30676]). +* `cholesky()` now fully preserves the user-specified permutation ([#40560]). +* `issparse` now applies consistently to all wrapper arrays, including nested, by checking + `issparse` on the wrapped parent array ([#37644]). + +#### Dates + +* The `Dates.periods` function can be used to get the `Vector` of `Period`s that comprise a + `CompoundPeriod` ([#39169]). + +#### Downloads + +* If a cookie header is set in a redirected request, the cookie will now be sent in following + requests (). +* If a `~/.netrc` file exists, it is used to get passwords for authenticated websites + (). +* [Server Name Indication](https://en.wikipedia.org/wiki/Server_Name_Indication) is now sent with + all TLS connections, even when the server's identity is not verified (see [NetworkOptions](https://github.com/JuliaLang/NetworkOptions.jl); ). +* When verifying TLS connections on Windows, if the certificate revocation server cannot be + reached, the connection is allowed; this matches what other applications do and how revocation + is performed on macOS (). +* There is now a 30-second connection timeout and a 20-second timeout if no data is sent; in + combination, this guarantees that connections must make some progress or they will timeout in + under a minute (). + +#### Statistics + + +#### Sockets + + +#### Tar + +* `Tar.extract` now ignores the exact permission mode in a tarball and normalizes modes in the + same way that `Tar.create` does, which is, in turn the same way that `git` normalizes them + (). +* Functions that consume tarballs now handle hard links: the link target must be a previously seen + file; `Tar.list` lists the entry with `:hardlink` type and `.link` field giving the path to the + target; other functions — `Tar.extract`, `Tar.rewrite`, `Tar.tree_hash` — treat a hard link as a + copy of the target file (). +* The standard format generated by `Tar.create` and `Tar.rewrite` now includes entries for non-empty + directories; this shouldn't be neccessary, but some tools that consume tarballs (including docker) + are confused by the absence of these directory entries (). +* `Tar` now accepts tarballs with leading spaces in octal integer header fields: this is technically + not a valid format according to the POSIX spec, but old Solaris `tar` commands produced tarballs like + this so this format does occur in the wild, and it seems harmless to accept it + (). +* `Tar.extract` now takes a `set_permissions` keyword argument, which defaults to `true`; if `false` is + passed instead, the permissions of extracted files are not modified on extraction + (). + +#### Distributed + + +#### UUIDs + + +#### Mmap + +* `mmap` is now exported ([#39816]). + +#### DelimitedFiles + +* `readdlm` now defaults to `use_mmap=false` on all OSes for consistent reliability in abnormal + filesystem situations ([#40415]). + +Deprecated or removed +--------------------- + + +External dependencies +--------------------- + + +Tooling Improvements +--------------------- + + + +[#29901]: https://github.com/JuliaLang/julia/issues/29901 +[#30676]: https://github.com/JuliaLang/julia/issues/30676 +[#31829]: https://github.com/JuliaLang/julia/issues/31829 +[#33697]: https://github.com/JuliaLang/julia/issues/33697 +[#34355]: https://github.com/JuliaLang/julia/issues/34355 +[#34678]: https://github.com/JuliaLang/julia/issues/34678 +[#35316]: https://github.com/JuliaLang/julia/issues/35316 +[#36229]: https://github.com/JuliaLang/julia/issues/36229 +[#37299]: https://github.com/JuliaLang/julia/issues/37299 +[#37644]: https://github.com/JuliaLang/julia/issues/37644 +[#37847]: https://github.com/JuliaLang/julia/issues/37847 +[#37971]: https://github.com/JuliaLang/julia/issues/37971 +[#37978]: https://github.com/JuliaLang/julia/issues/37978 +[#38041]: https://github.com/JuliaLang/julia/issues/38041 +[#38216]: https://github.com/JuliaLang/julia/issues/38216 +[#38379]: https://github.com/JuliaLang/julia/issues/38379 +[#38438]: https://github.com/JuliaLang/julia/issues/38438 +[#38574]: https://github.com/JuliaLang/julia/issues/38574 +[#38597]: https://github.com/JuliaLang/julia/issues/38597 +[#38675]: https://github.com/JuliaLang/julia/issues/38675 +[#38878]: https://github.com/JuliaLang/julia/issues/38878 +[#38952]: https://github.com/JuliaLang/julia/issues/38952 +[#38976]: https://github.com/JuliaLang/julia/issues/38976 +[#39026]: https://github.com/JuliaLang/julia/issues/39026 +[#39044]: https://github.com/JuliaLang/julia/issues/39044 +[#39093]: https://github.com/JuliaLang/julia/issues/39093 +[#39169]: https://github.com/JuliaLang/julia/issues/39169 +[#39216]: https://github.com/JuliaLang/julia/issues/39216 +[#39228]: https://github.com/JuliaLang/julia/issues/39228 +[#39285]: https://github.com/JuliaLang/julia/issues/39285 +[#39312]: https://github.com/JuliaLang/julia/issues/39312 +[#39322]: https://github.com/JuliaLang/julia/issues/39322 +[#39381]: https://github.com/JuliaLang/julia/issues/39381 +[#39403]: https://github.com/JuliaLang/julia/issues/39403 +[#39436]: https://github.com/JuliaLang/julia/issues/39436 +[#39455]: https://github.com/JuliaLang/julia/issues/39455 +[#39463]: https://github.com/JuliaLang/julia/issues/39463 +[#39588]: https://github.com/JuliaLang/julia/issues/39588 +[#39594]: https://github.com/JuliaLang/julia/issues/39594 +[#39607]: https://github.com/JuliaLang/julia/issues/39607 +[#39710]: https://github.com/JuliaLang/julia/issues/39710 +[#39758]: https://github.com/JuliaLang/julia/issues/39758 +[#39794]: https://github.com/JuliaLang/julia/issues/39794 +[#39816]: https://github.com/JuliaLang/julia/issues/39816 +[#40025]: https://github.com/JuliaLang/julia/issues/40025 +[#40039]: https://github.com/JuliaLang/julia/issues/40039 +[#40173]: https://github.com/JuliaLang/julia/issues/40173 +[#40194]: https://github.com/JuliaLang/julia/issues/40194 +[#40250]: https://github.com/JuliaLang/julia/issues/40250 +[#40320]: https://github.com/JuliaLang/julia/issues/40320 +[#40345]: https://github.com/JuliaLang/julia/issues/40345 +[#40415]: https://github.com/JuliaLang/julia/issues/40415 +[#40484]: https://github.com/JuliaLang/julia/issues/40484 +[#40546]: https://github.com/JuliaLang/julia/issues/40546 +[#40560]: https://github.com/JuliaLang/julia/issues/40560 +[#40573]: https://github.com/JuliaLang/julia/issues/40573 +[#40604]: https://github.com/JuliaLang/julia/issues/40604 +[#40623]: https://github.com/JuliaLang/julia/issues/40623 +[#40715]: https://github.com/JuliaLang/julia/issues/40715 +[#40729]: https://github.com/JuliaLang/julia/issues/40729 +[#40736]: https://github.com/JuliaLang/julia/issues/40736 +[#40737]: https://github.com/JuliaLang/julia/issues/40737 +[#40753]: https://github.com/JuliaLang/julia/issues/40753 +[#40765]: https://github.com/JuliaLang/julia/issues/40765 +[#40868]: https://github.com/JuliaLang/julia/issues/40868 +[#40948]: https://github.com/JuliaLang/julia/issues/40948 + + +Julia v1.6 Release Notes +======================== + +New language features +--------------------- + +* Types written with `where` syntax can now be used to define constructors, e.g. + `(Foo{T} where T)(x) = ...`. +* `<--` and `<-->` are now available as infix operators, with the same precedence + and associativity as other arrow-like operators ([#36666]). +* Compilation and type inference can now be enabled or disabled at the module level + using the experimental macro `Base.Experimental.@compiler_options` ([#37041]). +* The library name passed to `ccall` or `@ccall` can now be an expression involving + global variables and function calls. The expression will be evaluated the first + time the `ccall` executes ([#36458]). +* `ꜛ` (U+A71B), `ꜜ` (U+A71C) and `ꜝ` (U+A71D) can now also be used as operator + suffixes. They can be tab-completed from `\^uparrow`, `\^downarrow` and `\^!` in the REPL + ([#37542]). +* Standalone "dotted" operators now get parsed as `Expr(:., :op)`, which gets lowered to + `Base.BroadcastFunction(op)`. This means `.op` is functionally equivalent to + `(x...) -> (op).(x...)`, which can be useful for passing the broadcasted version of an + operator to higher-order functions, for example `map(.*, A, B)` for an elementwise + product of two arrays of arrays ([#37583]). +* The syntax `import A as B` (plus `import A: x as y`, `import A.x as y`, and `using A: x as y`) + can now be used to rename imported modules and identifiers ([#1255]). +* Unsigned literals (starting with `0x`) which are too big to fit in a `UInt128` object + are now interpreted as `BigInt` ([#23546]). +* It is now possible to use `...` on the left-hand side of assignments for taking any + number of items from the front of an iterable collection, while also collecting the rest, + for example `a, b... = [1, 2, 3]`. This syntax is implemented using `Base.rest`, + which can be overloaded to customize its behavior for different collection types + ([#37410]). + +Language changes +---------------- + +* The postfix conjugate transpose operator `'` now accepts Unicode modifiers as + suffixes, so e.g. `a'ᵀ` is parsed as `var"'ᵀ"(a)`, which can be defined by the + user. `a'ᵀ` parsed as `a' * ᵀ` before, so this is a minor breaking change ([#37247]). +* Macros that return `:quote` expressions (e.g. via `Expr(:quote, ...)`) were previously + able to work without escaping (`esc(...)`) their output when needed. This has been + corrected, and now `esc` must be used in these macros as it is in other macros ([#37540]). +* The `-->` operator now lowers to a `:call` expression, so it can be defined as + a function like other operators. The dotted version `.-->` is now parsed as well. + For backwards compatibility, `-->` still parses using its own expression head + instead of `:call`. +* The `a[begin, k]` syntax now calls `firstindex(a, 1)` rather than `first(axes(a, 1))` ([#35779]), + but the former now defaults to the latter for any `a` ([#38742]). +* `⌿` (U+233F) and `¦` (U+00A6) are now infix operators with times-like and plus-like precedence, + respectively. Previously they were parsed as identifier characters ([#37973]). + +Compiler/Runtime improvements +----------------------------- + +* All platforms can now use `@executable_path` within `jl_load_dynamic_library()`. + This allows executable-relative paths to be embedded within executables on all + platforms, not just MacOS, which the syntax is borrowed from ([#35627]). +* Constant propagation now occurs through keyword arguments ([#35976]). +* The precompilation cache is now created atomically ([#36416]). Invoking _n_ + Julia processes simultaneously may create _n_ temporary caches. + +Command-line option changes +--------------------------- + +* There is no longer a concept of "home project": starting `julia --project=dir` + is now exactly equivalent to starting `julia` and then doing `pkg> activate + $dir` and `julia --project` is exactly equivalent to doing that where + `dir = Base.current_project()`. In particular, this means that if you do + `pkg> activate` after starting `julia` with the `--project` option (or with + `JULIA_PROJECT` set) it will take you to the default active project, which is + `@v1.6` unless you have modified `LOAD_PATH` ([#36434]). + +Multi-threading changes +----------------------- + +* Locks now automatically inhibit finalizers from running, to avoid deadlock ([#38487]). +* New function `Base.Threads.foreach(f, channel::Channel)` for multithreaded `Channel` consumption ([#34543]). + +Build system changes +-------------------- + +* Windows Installer now has the option to 'Add Julia to Path'. To unselect this option + from the commandline simply remove the tasks you do not want to be installed: e.g. + `./julia-installer.exe /TASKS="desktopicon,startmenu,addtopath"`, adds a desktop + icon, a startmenu group icon, and adds Julia to system PATH. + +New library functions +--------------------- + +* New function `Base.kron!` and corresponding overloads for various matrix types for performing Kronecker + product in-place ([#31069]). +* New function `Base.readeach(io, T)` for iteratively performing `read(io, T)` ([#36150]). +* `Iterators.map` is added. It provides another syntax `Iterators.map(f, iterators...)` + for writing `(f(args...) for args in zip(iterators...))`, i.e. a lazy `map` ([#34352]). +* New function `sincospi` for simultaneously computing `sinpi(x)` and `cospi(x)` more + efficiently ([#35816]). +* New function `cispi(x)` for more accurately computing `cis(pi * x)` ([#38449]). +* New function `addenv` for adding environment mappings into a `Cmd` object, returning the new `Cmd` object. +* New function `insorted` for determining whether an element is in a sorted collection or not ([#37490]). +* New function `Base.rest` for taking the rest of a collection, starting from a specific + iteration state, in a generic way ([#37410]). + +New library features +-------------------- + +* The `redirect_*` functions now accept `devnull` to discard all output redirected to it, and as an empty + input ([#36146]). +* The `redirect_*` functions can now be called on `IOContext` objects ([#36688]). +* `findfirst`, `findnext`, `findlast`, and `findall` now support `AbstractVector{<:Union{Int8,UInt8}}` + (pattern, array) arguments ([#37283]). +* New constructor `NamedTuple(iterator)` that constructs a named tuple from a key-value pair iterator. +* A new `reinterpret(reshape, T, a::AbstractArray{S})` reinterprets `a` to have eltype `T` while potentially + inserting or consuming the first dimension depending on the ratio of `sizeof(T)` and `sizeof(S)`. +* New `append!(vector, collections...)` and `prepend!(vector, collections...)` methods accept multiple + collections to be appended or prepended ([#36227]). +* `keys(io::IO)` has been added, which returns all keys of `io` if `io` is an `IOContext` and an empty + `Base.KeySet` otherwise ([#37753]). +* `count` now accepts an optional `init` argument to control the accumulation type ([#37461]). +* New method `occursin(haystack)` that returns a function that checks whether its argument occurs in + `haystack` ([#38475]). +* New methods `∉(collection)`, `∋(item)`, and `∌(item)` returning corresponding containment-testing + functions ([#38475]). +* The `nextprod` function now accepts tuples and other array types for its first argument ([#35791]). +* The `reverse(A; dims)` function for multidimensional `A` can now reverse multiple dimensions at once + by passing a tuple for `dims`, and defaults to reversing all dimensions; there is also a multidimensional + in-place `reverse!(A; dims)` ([#37367]). +* The function `isapprox(x,y)` now accepts the `norm` keyword argument also for numeric (i.e., non-array) + arguments `x` and `y` ([#35883]). +* `ispow2(x)` now supports non-`Integer` arguments `x` ([#37635]). +* `view`, `@view`, and `@views` now work on `AbstractString`s, returning a `SubString` when appropriate ([#35879]). +* All `AbstractUnitRange{<:Integer}`s now work with `SubString`, `view`, `@view` and `@views` on strings ([#35879]). +* `sum`, `prod`, `maximum`, and `minimum` now support `init` keyword argument ([#36188], [#35839]). +* `unique(f, itr; seen=Set{T}())` now allows you to declare the container type used for + keeping track of values returned by `f` on elements of `itr` ([#36280]). +* `first` and `last` functions now accept an integer as second argument to get that many + leading or trailing elements of any iterable ([#34868]). +* `CartesianIndices` now supports step different from `1`. It can also be constructed from three + `CartesianIndex`es `I`, `S`, `J` using `I:S:J`. `step` for `CartesianIndices` now returns a + `CartesianIndex` ([#37829]). +* `RegexMatch` objects can now be probed for whether a named capture group exists within it through `haskey()` ([#36717]). +* For consistency `haskey(r::RegexMatch, i::Integer)` has also been added and returns if the capture group + for `i` exists ([#37300]). + +Standard library changes +------------------------ + +* A new standard library `TOML` has been added for parsing and printing [TOML files](https://toml.io) ([#37034]). +* A new standard library `Downloads` has been added, which replaces the old `Base.download` function with + `Downloads.download`, providing cross-platform, multi-protocol, in-process download functionality implemented + with [libcurl](https://curl.haxx.se/libcurl/) ([#37340]). +* `Libdl` has been moved to `Base.Libc.Libdl`, however it is still accessible as an stdlib ([#35628]). +* To download artifacts lazily, `LazyArtifacts` now must be explicitly listed as a dependency, to avoid needing the + support machinery to be available when it is not commonly needed ([#37844]). +* It is no longer possible to create a `LinRange`, `StepRange`, or `StepRangeLen` with a `<: Integer` eltype but + non-integer step ([#32439]). +* `intersect` on `CartesianIndices` now returns `CartesianIndices` instead of `Vector{<:CartesianIndex}` ([#36643]). +* `push!(c::Channel, v)` now returns channel `c`. Previously, it returned the pushed value `v` ([#34202]). +* The composition operator `∘` now returns a `Base.ComposedFunction` instead of an anonymous function ([#37517]). +* Logging (such as `@warn`) no longer catches exceptions in the logger itself ([#36600]). +* `@time` now reports if the time presented included any compilation time, which is shown as a percentage ([#37678]). +* `@varinfo` can now report non-exported objects within modules, look recursively into submodules, and return a sorted + results table ([#38042]). +* `@testset` now supports the option `verbose` to show the test result summary + of the children even if they all pass ([#33755]). +* In `LinearIndices(::Tuple)` and `CartesianIndices(::Tuple)`, integers (as opposed to ranges of integers) in the + argument tuple now consistently describe 1-based ranges, e.g, `CartesianIndices((3, 1:3))` is equivalent to + `CartesianIndices((1:3, 1:3))`. This is how tuples of integers have always been documented to work, but a + bug had caused erroneous behaviors with heterogeneous tuples containing both integers and ranges ([#37829], [#37928]). + +#### Package Manager + +* `pkg> precompile` is now parallelized through depth-first precompilation of dependencies. Errors will only throw for + direct dependencies listed in the `Project.toml`. +* `pkg> precompile` is now automatically triggered whenever Pkg changes the active manifest. Auto-precompilation will + remember if a package has errored within the given environment and will not retry until it changes. + Auto-precompilation can be gracefully interrupted with a `ctrl-c` and disabled by setting the environment variable + `JULIA_PKG_PRECOMPILE_AUTO=0`. +* The `Pkg.BinaryPlatforms` module has been moved into `Base` as `Base.BinaryPlatforms` and heavily reworked. + Applications that want to be compatible with the old API should continue to import `Pkg.BinaryPlatforms`, + however new users should use `Base.BinaryPlatforms` directly ([#37320]). +* The `Pkg.Artifacts` module has been imported as a separate standard library. It is still available as + `Pkg.Artifacts`, however starting from Julia v1.6+, packages may import simply `Artifacts` without importing + all of `Pkg` alongside ([#37320]). + +#### LinearAlgebra + +* New method `LinearAlgebra.issuccess(::CholeskyPivoted)` for checking whether pivoted Cholesky factorization was + successful ([#36002]). +* `UniformScaling` can now be indexed into using ranges to return dense matrices and vectors ([#24359]). +* New function `LinearAlgebra.BLAS.get_num_threads()` for getting the number of BLAS threads ([#36360]). +* `(+)(::UniformScaling)` is now defined, making `+I` a valid unary operation ([#36784]). +* Instances of `UniformScaling` are no longer `isequal` to matrices. Previous + behaviour violated the rule that `isequal(x, y)` implies `hash(x) == hash(y)`. +* Transposing `*Triangular` matrices now returns matrices of the opposite triangular type, consistently + with `adjoint!(::*Triangular)` and `transpose!(::*Triangular)`. Packages containing methods with, e.g., + `Adjoint{<:Any,<:LowerTriangular{<:Any,<:OwnMatrixType}}` should replace that by + `UpperTriangular{<:Any,<:Adjoint{<:Any,<:OwnMatrixType}}` in the method signature ([#38168]). + +#### Markdown + + +#### Printf + +* Complete overhaul of internal code to use the ryu float printing algorithms (from Julia 1.4); leads to + consistent 2-5x performance improvements. +* New `Printf.tofloat` function allowing custom float types to more easily integrate with Printf formatting + by converting their type to `Float16`, `Float32`, `Float64`, or `BigFloat`. +* New `Printf.format"..."` and `Printf.Format(...)` functions that allow creating `Printf.Format` objects + that can be passed to `Printf.format` for easier dynamic printf formatting. +* `Printf.format(f::Printf.Format, args...)` as a non-macro function that applies a printf format `f` to + provided `args`. + +#### Random + + +#### REPL + +* The `AbstractMenu` extension interface of `REPL.TerminalMenus` has been extensively + overhauled. The new interface does not rely on global configuration variables, is more + consistent in delegating printing of the navigation/selection markers, and provides + improved support for dynamic menus. These changes are compatible with the previous + (deprecated) interface, so are non-breaking. + + The new API offers several enhancements: + + + Menus are configured in their constructors via keyword arguments. + + For custom menu types, the new `Config` and `MultiSelectConfig` replace the global `CONFIG` `Dict`. + + `request(menu; cursor=1)` allows you to control the initial cursor position in the menu (defaults to first item). + + `MultiSelectMenu` allows you to pass a list of initially-selected items with the `selected` keyword argument. + + `writeLine` was deprecated to `writeline`, and `writeline` methods are not expected to print the cursor indicator. + The old `writeLine` continues to work, and any of its method extensions should print the cursor indicator as before. + + `printMenu` has been deprecated to `printmenu`, and it both accepts a state input and returns a state output + that controls the number of terminal lines erased when the menu is next refreshed. This plus related changes + makes `printmenu` work properly when the number of menu items might change depending on user choices. + + `numoptions`, returning the number of items in the menu, has been added as an alternative to implementing `options`. + + `suppress_output` (primarily a testing option) has been added as a keyword argument to `request`, + rather than a configuration option. +* Tab completion now supports runs of consecutive sub/superscript characters, + e.g. `\^(3)` tab-completes to `⁽³⁾` ([#38649]). +* Windows REPL now supports 24-bit colors, by correctly interpreting virtual terminal escapes. + +#### SparseArrays + +* Display large sparse matrices with a Unicode "spy" plot of their nonzero patterns, + and display small sparse matrices by an `Matrix`-like 2d layout of their contents ([#33821]). +* New convenient `spdiagm([m, n,] v::AbstractVector)` methods which call + `spdiagm([m, n,] 0 => v)`, consistently with their dense `diagm` counterparts ([#37684]). + +#### Dates + +* `Quarter` period is defined ([#35519]). +* `canonicalize` can now take `Period` as an input ([#37391]). +* Zero-valued `FixedPeriod`s and `OtherPeriod`s now compare equal, e.g., + `Year(0) == Day(0)`. The behavior of non-zero `Period`s is not changed ([#37486]). + +#### Statistics + + +#### Sockets + + +#### Distributed + +* Now supports invoking Windows workers via ssh (via new keyword argument `shell=:wincmd` in `addprocs`) ([#30614]). +* Other new keyword arguments in `addprocs`: `ssh` to specify the ssh client path, `env` to pass environment + variables to workers, and `cmdline_cookie` to work around an ssh problem with Windows workers that run older + (pre-ConPTY) versions of Windows, Julia or OpenSSH ([#30614]). + +#### UUIDs + +* Change `uuid1` and `uuid4` to use `Random.RandomDevice()` as default random number generator ([#35872]). +* Added `parse(::Type{UUID}, ::AbstractString)` method. + +#### Mmap + +* On Unix systems, the `Mmap.madvise!` function (along with OS-specific `Mmap.MADV_*` + constants) has been added to give advice on handling of memory-mapped arrays ([#37369]). + +Deprecated or removed +--------------------- + +* The `Base.download` function has been deprecated (silently, by default) in favor of the new `Downloads.download` + standard library function ([#37340]). +* The `Base.Grisu` code has been officially removed (float printing was switched to the ryu algorithm code in 1.4). + The code is available from [JuliaAttic](https://github.com/JuliaAttic/Grisu.jl) if needed. + +External dependencies +--------------------- + + +Tooling Improvements +--------------------- + + + +[#1255]: https://github.com/JuliaLang/julia/issues/1255 +[#23546]: https://github.com/JuliaLang/julia/issues/23546 +[#24359]: https://github.com/JuliaLang/julia/issues/24359 +[#30614]: https://github.com/JuliaLang/julia/issues/30614 +[#31069]: https://github.com/JuliaLang/julia/issues/31069 +[#32439]: https://github.com/JuliaLang/julia/issues/32439 +[#33755]: https://github.com/JuliaLang/julia/issues/33755 +[#33821]: https://github.com/JuliaLang/julia/issues/33821 +[#34202]: https://github.com/JuliaLang/julia/issues/34202 +[#34352]: https://github.com/JuliaLang/julia/issues/34352 +[#34543]: https://github.com/JuliaLang/julia/issues/34543 +[#34868]: https://github.com/JuliaLang/julia/issues/34868 +[#35519]: https://github.com/JuliaLang/julia/issues/35519 +[#35627]: https://github.com/JuliaLang/julia/issues/35627 +[#35628]: https://github.com/JuliaLang/julia/issues/35628 +[#35779]: https://github.com/JuliaLang/julia/issues/35779 +[#35791]: https://github.com/JuliaLang/julia/issues/35791 +[#35816]: https://github.com/JuliaLang/julia/issues/35816 +[#35839]: https://github.com/JuliaLang/julia/issues/35839 +[#35872]: https://github.com/JuliaLang/julia/issues/35872 +[#35879]: https://github.com/JuliaLang/julia/issues/35879 +[#35883]: https://github.com/JuliaLang/julia/issues/35883 +[#35976]: https://github.com/JuliaLang/julia/issues/35976 +[#36002]: https://github.com/JuliaLang/julia/issues/36002 +[#36146]: https://github.com/JuliaLang/julia/issues/36146 +[#36150]: https://github.com/JuliaLang/julia/issues/36150 +[#36188]: https://github.com/JuliaLang/julia/issues/36188 +[#36227]: https://github.com/JuliaLang/julia/issues/36227 +[#36280]: https://github.com/JuliaLang/julia/issues/36280 +[#36360]: https://github.com/JuliaLang/julia/issues/36360 +[#36416]: https://github.com/JuliaLang/julia/issues/36416 +[#36434]: https://github.com/JuliaLang/julia/issues/36434 +[#36458]: https://github.com/JuliaLang/julia/issues/36458 +[#36600]: https://github.com/JuliaLang/julia/issues/36600 +[#36643]: https://github.com/JuliaLang/julia/issues/36643 +[#36666]: https://github.com/JuliaLang/julia/issues/36666 +[#36688]: https://github.com/JuliaLang/julia/issues/36688 +[#36717]: https://github.com/JuliaLang/julia/issues/36717 +[#36784]: https://github.com/JuliaLang/julia/issues/36784 +[#37034]: https://github.com/JuliaLang/julia/issues/37034 +[#37041]: https://github.com/JuliaLang/julia/issues/37041 +[#37247]: https://github.com/JuliaLang/julia/issues/37247 +[#37283]: https://github.com/JuliaLang/julia/issues/37283 +[#37300]: https://github.com/JuliaLang/julia/issues/37300 +[#37320]: https://github.com/JuliaLang/julia/issues/37320 +[#37340]: https://github.com/JuliaLang/julia/issues/37340 +[#37367]: https://github.com/JuliaLang/julia/issues/37367 +[#37369]: https://github.com/JuliaLang/julia/issues/37369 +[#37391]: https://github.com/JuliaLang/julia/issues/37391 +[#37410]: https://github.com/JuliaLang/julia/issues/37410 +[#37461]: https://github.com/JuliaLang/julia/issues/37461 +[#37486]: https://github.com/JuliaLang/julia/issues/37486 +[#37490]: https://github.com/JuliaLang/julia/issues/37490 +[#37517]: https://github.com/JuliaLang/julia/issues/37517 +[#37540]: https://github.com/JuliaLang/julia/issues/37540 +[#37542]: https://github.com/JuliaLang/julia/issues/37542 +[#37583]: https://github.com/JuliaLang/julia/issues/37583 +[#37635]: https://github.com/JuliaLang/julia/issues/37635 +[#37678]: https://github.com/JuliaLang/julia/issues/37678 +[#37684]: https://github.com/JuliaLang/julia/issues/37684 +[#37753]: https://github.com/JuliaLang/julia/issues/37753 +[#37829]: https://github.com/JuliaLang/julia/issues/37829 +[#37844]: https://github.com/JuliaLang/julia/issues/37844 +[#37973]: https://github.com/JuliaLang/julia/issues/37973 +[#38042]: https://github.com/JuliaLang/julia/issues/38042 +[#38062]: https://github.com/JuliaLang/julia/issues/38062 +[#38168]: https://github.com/JuliaLang/julia/issues/38168 +[#38449]: https://github.com/JuliaLang/julia/issues/38449 +[#38475]: https://github.com/JuliaLang/julia/issues/38475 +[#38487]: https://github.com/JuliaLang/julia/issues/38487 +[#38649]: https://github.com/JuliaLang/julia/issues/38649 +[#38742]: https://github.com/JuliaLang/julia/issues/38742 + + Julia v1.5 Release Notes ======================== @@ -3083,7 +4102,7 @@ Library improvements + Using colons (`:`) to represent a collection of indices is deprecated. They now must be explicitly converted to a specialized array of integers with the `to_indices` function. -    As a result, the type of `SubArray`s that represent views over colon indices has changed. + As a result, the type of `SubArray`s that represent views over colon indices has changed. + Logical indexing is now more efficient. Logical arrays are converted by `to_indices` to a lazy, iterable collection of indices that doesn't support indexing. A deprecation @@ -4860,7 +5879,7 @@ New language features shell. For example: julia> ;ls - CONTRIBUTING.md Makefile VERSION deps/ julia@ ui/ + CONTRIBUTING.md Makefile VERSION cli/ deps/ julia@ DISTRIBUTING.md NEWS.md Windows.inc doc/ src/ usr/ LICENSE.md README.md base/ etc/ test/ Make.inc README.windows.md contrib/ examples/ tmp/ diff --git a/LICENSE.md b/LICENSE.md index d1438a5f68bfc0..fdf24e7603d730 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,82 +1,26 @@ -The Julia language is licensed under the MIT License. The "language" consists -of the compiler (the contents of src/), most of the standard library (base/), -and some utilities (most of the rest of the files in this repository). See below -for exceptions. +MIT License -> Copyright (c) 2009-2019: Jeff Bezanson, Stefan Karpinski, Viral B. Shah, -> and other contributors: -> -> https://github.com/JuliaLang/julia/contributors -> -> Permission is hereby granted, free of charge, to any person obtaining -> a copy of this software and associated documentation files (the -> "Software"), to deal in the Software without restriction, including -> without limitation the rights to use, copy, modify, merge, publish, -> distribute, sublicense, and/or sell copies of the Software, and to -> permit persons to whom the Software is furnished to do so, subject to -> the following conditions: -> -> The above copyright notice and this permission notice shall be -> included in all copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -> NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -> LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -> OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -> WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Copyright (c) 2009-2022: Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors: https://github.com/JuliaLang/julia/contributors -Julia includes code from the following projects, which have their own licenses: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -- [crc32c.c](https://stackoverflow.com/questions/17645167/implementing-sse-4-2s-crc32c-in-software) (CRC-32c checksum code by Mark Adler) [[ZLib](https://opensource.org/licenses/Zlib)]. -- [LDC](https://github.com/ldc-developers/ldc/blob/master/LICENSE) (for ccall/cfunction ABI definitions) [BSD-3]. The portion of code that Julia uses from LDC is [BSD-3] licensed. -- [LLVM](https://releases.llvm.org/3.9.0/LICENSE.TXT) (for parts of src/jitlayers.cpp and src/disasm.cpp) [BSD-3, effectively] -- [MUSL](https://git.musl-libc.org/cgit/musl/tree/COPYRIGHT) (for getopt implementation on Windows) [MIT] -- [MINGW](https://sourceforge.net/p/mingw/mingw-org-wsl/ci/legacy/tree/mingwrt/mingwex/dirname.c) (for dirname implementation on Windows) [MIT] -- [NetBSD](https://www.netbsd.org/about/redistribution.html) (for setjmp, longjmp, and strptime implementations on Windows) [BSD-3] -- [Python](https://docs.python.org/3/license.html) (for strtod and joinpath implementation on Windows) [BSD-3, effectively] -- [Google Benchmark](https://github.com/google/benchmark) (for cyclecount implementation) [Apache 2.0] +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -The following components included in Julia `Base` have their own separate licenses: +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- base/ryu/* [Boost] (see [ryu](https://github.com/ulfjack/ryu/blob/master/LICENSE-Boost)) -- base/grisu/* [BSD-3] (see [double-conversion](https://github.com/google/double-conversion/blob/master/LICENSE)) -- base/special/{exp,rem_pio2,hyperbolic}.jl [Freely distributable with preserved copyright notice] (see [FDLIBM](https://www.netlib.org/fdlibm)) +end of terms and conditions -The Julia language links to the following external libraries, which have their -own licenses: - -- [FEMTOLISP](https://github.com/JeffBezanson/femtolisp) [BSD-3] -- [LIBUNWIND](https://git.savannah.gnu.org/gitweb/?p=libunwind.git;a=blob_plain;f=LICENSE;hb=master) [MIT] -- [LIBUV](https://github.com/joyent/libuv/blob/master/LICENSE) [MIT] -- [LLVM](https://releases.llvm.org/6.0.0/LICENSE.TXT) [BSD-3, effectively] -- [UTF8PROC](https://github.com/JuliaStrings/utf8proc) [MIT] - -Julia's `stdlib` uses the following external libraries, which have their own licenses: - -- [DSFMT](http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/SFMT/LICENSE.txt) [BSD-3] -- [OPENLIBM](https://github.com/JuliaMath/openlibm/blob/master/LICENSE.md) [MIT, BSD-2, ISC] -- [GMP](https://gmplib.org/manual/Copying.html#Copying) [LGPL3+ or GPL2+] -- [LIBGIT2](https://github.com/libgit2/libgit2/blob/development/COPYING) [GPL2+ with unlimited linking exception] -- [CURL](https://curl.haxx.se/docs/copyright.html) [MIT/X derivative] -- [LIBSSH2](https://github.com/libssh2/libssh2/blob/master/COPYING) [BSD-3] -- [MBEDTLS](https://tls.mbed.org/how-to-get) [either GPLv2 or Apache 2.0] -- [MPFR](https://www.mpfr.org/mpfr-current/mpfr.html#Copying) [LGPL3+] -- [OPENBLAS](https://raw.github.com/xianyi/OpenBLAS/master/LICENSE) [BSD-3] -- [LAPACK](https://netlib.org/lapack/LICENSE.txt) [BSD-3] -- [PCRE](https://www.pcre.org/licence.txt) [BSD-3] -- [SUITESPARSE](http://suitesparse.com) [mix of LGPL2+ and GPL2+; see individual module licenses] - -Julia's build process uses the following external tools: - -- [PATCHELF](https://nixos.org/patchelf.html) -- [OBJCONV](https://www.agner.org/optimize/#objconv) - -Julia bundles the following external programs and libraries: - -- [7-Zip](https://www.7-zip.org/license.txt) -- [ZLIB](https://zlib.net/zlib_license.html) - -On some platforms, distributions of Julia contain SSL certificate authority certificates, -released under the [Mozilla Public License](https://en.wikipedia.org/wiki/Mozilla_Public_License). +Please see [THIRDPARTY.md](./THIRDPARTY.md) for license information for other software used in this project. diff --git a/Make.inc b/Make.inc index df2e522cad09f2..b70b56a8097360 100644 --- a/Make.inc +++ b/Make.inc @@ -1,4 +1,4 @@ -# -*- mode: makefile-gmake -*- +# -*- mode: makefile -*- # vi:syntax=make ## Note: @@ -14,8 +14,16 @@ # Set to zero to turn off extra precompile (e.g. for the REPL) JULIA_PRECOMPILE ?= 1 +# Set FORCE_ASSERTIONS to 1 to enable assertions in the C and C++ portions +# of the Julia code base. You may also want to set LLVM_ASSERTIONS to 1, +# which will enable assertions in LLVM. +# An "assert build" of Julia is a build that has both FORCE_ASSERTIONS=1 +# and LLVM_ASSERTIONS=1. FORCE_ASSERTIONS ?= 0 +# Set BOOTSTRAP_DEBUG_LEVEL to 1 to enable Julia-level stacktrace during bootstrapping. +BOOTSTRAP_DEBUG_LEVEL ?= 0 + # OPENBLAS build options OPENBLAS_TARGET_ARCH:= OPENBLAS_SYMBOLSUFFIX:= @@ -32,6 +40,7 @@ OPENBLAS_USE_THREAD:=1 # Flags for using libraries available on the system instead of building them. # Please read the notes around usage of SYSTEM flags in README.md # Issues resulting from use of SYSTEM versions will generally not be accepted. +USE_SYSTEM_CSL:=0 USE_SYSTEM_LLVM:=0 USE_SYSTEM_LIBUNWIND:=0 DISABLE_LIBUNWIND:=0 @@ -40,11 +49,12 @@ USE_SYSTEM_LIBM:=0 USE_SYSTEM_OPENLIBM:=0 UNTRUSTED_SYSTEM_LIBM:=0 USE_SYSTEM_DSFMT:=0 +USE_SYSTEM_LIBBLASTRAMPOLINE:=0 USE_SYSTEM_BLAS:=0 USE_SYSTEM_LAPACK:=0 USE_SYSTEM_GMP:=0 USE_SYSTEM_MPFR:=0 -USE_SYSTEM_SUITESPARSE:=0 +USE_SYSTEM_LIBSUITESPARSE:=0 USE_SYSTEM_LIBUV:=0 USE_SYSTEM_UTF8PROC:=0 USE_SYSTEM_MBEDTLS:=0 @@ -53,36 +63,20 @@ USE_SYSTEM_NGHTTP2:=0 USE_SYSTEM_CURL:=0 USE_SYSTEM_LIBGIT2:=0 USE_SYSTEM_PATCHELF:=0 +USE_SYSTEM_LIBWHICH:=0 USE_SYSTEM_ZLIB:=0 USE_SYSTEM_P7ZIP:=0 # Link to the LLVM shared library USE_LLVM_SHLIB := 1 -## Settings for various Intel tools -# Set to 1 to use MKL -USE_INTEL_MKL ?= 0 -# Set to 1 to use Intel LIBM -USE_INTEL_LIBM ?= 0 -# Set to 1 to enable profiling with Intel VTune Amplifier -USE_INTEL_JITEVENTS ?= 0 -# Set to 1 to use Intel C, C++, and FORTRAN compilers -USEICC ?= 0 -USEIFC ?= 0 - # Enable threading with one thread JULIA_THREADS := 1 -ifeq ($(USE_MKL), 1) -$(warning "The julia make variable USE_MKL has been renamed to USE_INTEL_MKL") -USE_INTEL_MKL := 1 -endif - # Set to 1 to enable profiling with OProfile USE_OPROFILE_JITEVENTS ?= 0 -# Set to 1 to enable profiling with perf -USE_PERF_JITEVENTS ?= 0 +# USE_PERF_JITEVENTS defined below since default is OS specific # assume we don't have LIBSSP support in our compiler, will enable later if likely true HAVE_SSP := 0 @@ -91,12 +85,28 @@ HAVE_SSP := 0 WITH_GC_VERIFY := 0 WITH_GC_DEBUG_ENV := 0 +# Enable DTrace support +WITH_DTRACE := 0 + # Prevent picking up $ARCH from the environment variables ARCH:= + +# Literal values that are hard to use in Makefiles otherwise: +define newline # a literal \n + + +endef +COMMA:=, +SPACE:=$(eval) $(eval) + +# force a sane / stable configuration +export LC_ALL=C +export LANG=C + # We need python for things like BB triplet recognition and relative path computation. # We don't really care about version, generally, so just find something that works: -PYTHON := "$(shell which python 2>/dev/null || which python3 2>/dev/null || which python2 2>/dev/null || echo not found)" +PYTHON := "$(shell which python 2>/dev/null || which python3 2>/dev/null || which python2 2>/dev/null || echo "{python|python3|python2} not found")" PYTHON_SYSTEM := $(shell $(PYTHON) -c 'from __future__ import print_function; import platform; print(platform.system())') # If we're running on Cygwin, but using a native-windows Python, we need to use cygpath -w @@ -319,11 +329,16 @@ endef $(foreach D,libdir private_libdir datarootdir libexecdir docdir sysconfdir includedir,$(eval $(call cache_rel_path,$(D),$(bindir)))) $(foreach D,build_libdir build_private_libdir,$(eval $(call cache_rel_path,$(D),$(build_bindir)))) +# Save a special one: reverse_private_libdir_rel: usually just `../`, but good to be general: +reverse_private_libdir_rel_eval = $(call rel_path,$(private_libdir),$(libdir)) +reverse_private_libdir_rel = $(call hit_cache,reverse_private_libdir_rel_eval) + INSTALL_F := $(JULIAHOME)/contrib/install.sh 644 INSTALL_M := $(JULIAHOME)/contrib/install.sh 755 # LLVM Options LLVMROOT := $(build_prefix) +# Set LLVM_ASSERTIONS to 1 to enable assertions in LLVM. LLVM_ASSERTIONS := 0 LLVM_DEBUG := 0 # set to 1 to get clang and compiler-rt @@ -331,6 +346,7 @@ BUILD_LLVM_CLANG := 0 # set to 1 to get lldb (often does not work, no chance with llvm3.2 and earlier) # see http://lldb.llvm.org/build.html for dependencies BUILD_LLDB := 0 +BUILD_LIBCXX := 0 # Options to enable Polly and its code-generation options USE_POLLY := 0 @@ -340,6 +356,9 @@ USE_POLLY_ACC := 0 # Enable GPU code-generation # Options to use MLIR USE_MLIR := 0 +# Options to use RegionVectorizer +USE_RV := 0 + # Cross-compile #XC_HOST := i686-w64-mingw32 #XC_HOST := x86_64-w64-mingw32 @@ -366,8 +385,10 @@ ifeq ($(XC_HOST),) CROSS_COMPILE:= # delayed expansion of $(CC), since it won't be computed until later HOSTCC = $(CC) +HOSTCXX = $(CXX) else HOSTCC ?= gcc +HOSTCXX ?= g++ OPENBLAS_DYNAMIC_ARCH := 1 override CROSS_COMPILE:=$(XC_HOST)- ifneq (,$(findstring mingw,$(XC_HOST))) @@ -416,6 +437,13 @@ fPIC := -fPIC EXE := endif +# Set to 1 to enable profiling with perf +ifeq ("$(OS)", "Linux") +USE_PERF_JITEVENTS ?= 1 +else +USE_PERF_JITEVENTS ?= 0 +endif + JULIACODEGEN := LLVM # flag for disabling assertions @@ -429,97 +457,54 @@ CXX_DISABLE_ASSERTION := -DJL_NDEBUG DISABLE_ASSERTIONS := -DNDEBUG -DJL_NDEBUG endif -ifeq ($(LLVM_ASSERTIONS),0) -CXX_DISABLE_ASSERTION += -DNDEBUG -endif - # Compiler specific stuff -ifeq ($(USEMSVC), 1) -USEGCC := 0 -USECLANG := 0 -USEICC := 0 -else -ifeq ($(USECLANG), 1) +CC_VERSION_STRING = $(shell $(CC) --version) +ifneq (,$(findstring clang,$(CC_VERSION_STRING))) +USECLANG := 1 USEGCC := 0 -USEICC := 0 else -ifeq ($(USEICC), 1) -USEGCC := 0 USECLANG := 0 -else # default to gcc USEGCC := 1 -USECLANG := 0 -USEICC := 0 -endif -endif endif -ifeq ($(USEIFC), 1) -FC := ifort -else FC := $(CROSS_COMPILE)gfortran -endif - -ifeq ($(OS), FreeBSD) -USEGCC := 0 -USECLANG := 1 -endif +# Note: Supporting only macOS Yosemite and above ifeq ($(OS), Darwin) -DARWINVER := $(shell uname -r | cut -b 1-2) -DARWINVER_GTE11 := $(shell expr $(DARWINVER) \>= 11) -DARWINVER_GTE13 := $(shell expr $(DARWINVER) \>= 13) -ifeq ($(DARWINVER_GTE11),0) # Snow Leopard specific configuration -USEGCC := 1 -USECLANG := 0 -MACOSX_VERSION_MIN := 10.6 -OPENBLAS_TARGET_ARCH:=NEHALEM -OPENBLAS_DYNAMIC_ARCH:=0 -USE_SYSTEM_LIBUNWIND:=1 +APPLE_ARCH := $(shell uname -m) +ifneq ($(APPLE_ARCH),arm64) +MACOSX_VERSION_MIN := 10.10 else -ifeq ($(DARWINVER_GTE13),0) # Lion / Mountain Lion specific configuration -USEGCC := 0 -USECLANG := 1 -MACOSX_VERSION_MIN := 10.6 -else # Newer versions -USEGCC := 0 -USECLANG := 1 -endif +MACOSX_VERSION_MIN := 11.0 endif endif ifeq ($(USEGCC),1) -ifeq ($(SANITIZE),1) -$(error Sanitizers are only supported with clang. Try setting SANITIZE=0) -endif CC := $(CROSS_COMPILE)gcc CXX := $(CROSS_COMPILE)g++ -JCFLAGS := -std=gnu99 -pipe $(fPIC) -fno-strict-aliasing -D_FILE_OFFSET_BITS=64 +JCFLAGS := -std=gnu11 -pipe $(fPIC) -fno-strict-aliasing -D_FILE_OFFSET_BITS=64 # AArch64 needs this flag to generate the .eh_frame used by libunwind JCPPFLAGS := -fasynchronous-unwind-tables -JCXXFLAGS := -pipe $(fPIC) -fno-rtti +JCXXFLAGS := -pipe $(fPIC) -fno-rtti -std=c++14 ifneq ($(OS), WINNT) # Do not enable on windows to avoid warnings from libuv. JCXXFLAGS += -pedantic endif -DEBUGFLAGS := -O0 -ggdb2 -DJL_DEBUG_BUILD -fstack-protector-all +DEBUGFLAGS := -O0 -ggdb2 -DJL_DEBUG_BUILD -fstack-protector SHIPFLAGS := -O3 -ggdb2 -falign-functions endif ifeq ($(USECLANG),1) CC := $(CROSS_COMPILE)clang CXX := $(CROSS_COMPILE)clang++ -JCFLAGS := -std=gnu99 -pipe $(fPIC) -fno-strict-aliasing -D_FILE_OFFSET_BITS=64 +JCFLAGS := -std=gnu11 -pipe $(fPIC) -fno-strict-aliasing -D_FILE_OFFSET_BITS=64 # AArch64 needs this flag to generate the .eh_frame used by libunwind JCPPFLAGS := -fasynchronous-unwind-tables -JCXXFLAGS := -pipe $(fPIC) -fno-rtti -pedantic -DEBUGFLAGS := -O0 -g -DJL_DEBUG_BUILD -fstack-protector-all +JCXXFLAGS := -pipe $(fPIC) -fno-rtti -pedantic -std=c++14 +DEBUGFLAGS := -O0 -g -DJL_DEBUG_BUILD -fstack-protector SHIPFLAGS := -O3 -g ifeq ($(OS), Darwin) -ifeq ($(MACOSX_VERSION_MIN),) -MACOSX_VERSION_MIN := 10.9 -endif CC += -mmacosx-version-min=$(MACOSX_VERSION_MIN) CXX += -mmacosx-version-min=$(MACOSX_VERSION_MIN) FC += -mmacosx-version-min=$(MACOSX_VERSION_MIN) @@ -529,18 +514,7 @@ JCPPFLAGS += -D_LARGEFILE_SOURCE -D_DARWIN_USE_64_BIT_INODE=1 endif endif -ifeq ($(USEICC),1) -ifeq ($(SANITIZE),1) -$(error Sanitizers only supported with clang. Try setting SANITIZE=0) -endif -CC := icc -CXX := icpc -JCFLAGS := -std=gnu11 -pipe $(fPIC) -fno-strict-aliasing -D_FILE_OFFSET_BITS=64 -fp-model precise -fp-model except -no-ftz -JCPPFLAGS := -JCXXFLAGS := -pipe $(fPIC) -fno-rtti -DEBUGFLAGS := -O0 -g -DJL_DEBUG_BUILD -fstack-protector-all -SHIPFLAGS := -O3 -g -falign-functions -endif +JLDFLAGS := ifeq ($(USECCACHE), 1) # Expand CC, CXX and FC here already because we want the original definition and not the ccache version. @@ -577,24 +551,15 @@ FC_ARG := $(shell echo $(FC) | cut -s -d' ' -f2-) endif JFFLAGS := -O2 $(fPIC) -ifneq ($(USEMSVC),1) CPP := $(CC) -E AR := $(CROSS_COMPILE)ar AS := $(CROSS_COMPILE)as LD := $(CROSS_COMPILE)ld -else #USEMSVC -CPP := $(CC) -EP -AR := lib -ifeq ($(ARCH),x86_64) -AS := ml64 -else -AS := ml -endif #ARCH -LD := link -endif #USEMSVC RANLIB := $(CROSS_COMPILE)ranlib OBJCOPY := $(CROSS_COMPILE)objcopy +CPP_STDOUT := $(CPP) -P + # file extensions ifeq ($(OS), WINNT) SHLIB_EXT := dll @@ -604,11 +569,66 @@ else SHLIB_EXT := so endif +ifeq ($(OS),WINNT) +define versioned_libname +$$(if $(2),$(1)-$(2).$(SHLIB_EXT),$(1).$(SHLIB_EXT)) +endef +else ifeq ($(OS),Darwin) +define versioned_libname +$$(if $(2),$(1).$(2).$(SHLIB_EXT),$(1).$(SHLIB_EXT)) +endef +else +define versioned_libname +$$(if $(2),$(1).$(SHLIB_EXT).$(2),$(1).$(SHLIB_EXT)) +endef +endif + + +ifeq ($(SHLIB_EXT), so) +define SONAME_FLAGS + -Wl,-soname=$1 +endef +else +define SONAME_FLAGS +endef +endif + +ifeq ($(OS),WINNT) +define IMPLIB_FLAGS + -Wl,--out-implib,$(build_libdir)/$(notdir $1).a +endef +else +define IMPLIB_FLAGS +endef +endif + # On Windows, we want shared library files to end up in $(build_bindir), instead of $(build_libdir) +# We also don't really have a private bindir on windows right now, due to lack of RPATH. ifeq ($(OS),WINNT) +shlibdir := $(bindir) +private_shlibdir := $(bindir) build_shlibdir := $(build_bindir) +build_private_shlibdir := $(build_bindir) else +shlibdir := $(libdir) +private_shlibdir := $(private_libdir) build_shlibdir := $(build_libdir) +build_private_shlibdir := $(build_private_libdir) +endif + +# If we're on windows, don't do versioned shared libraries. If we're on OSX, +# put the version number before the .dylib. Otherwise, put it after. +ifeq ($(OS), WINNT) +JL_MAJOR_MINOR_SHLIB_EXT := $(SHLIB_EXT) +JL_MAJOR_SHLIB_EXT := $(SHLIB_EXT) +else +ifeq ($(OS), Darwin) +JL_MAJOR_MINOR_SHLIB_EXT := $(SOMAJOR).$(SOMINOR).$(SHLIB_EXT) +JL_MAJOR_SHLIB_EXT := $(SOMAJOR).$(SHLIB_EXT) +else +JL_MAJOR_MINOR_SHLIB_EXT := $(SHLIB_EXT).$(SOMAJOR).$(SOMINOR) +JL_MAJOR_SHLIB_EXT := $(SHLIB_EXT).$(SOMAJOR) +endif endif ifeq ($(OS), FreeBSD) @@ -630,8 +650,8 @@ ifeq ($(OS),FreeBSD) ifneq (,$(findstring gfortran,$(FC))) # First let's figure out what version of GCC we're dealing with -_GCCMAJOR := $(shell $(FC) -dumpversion | cut -d'.' -f1) -_GCCMINOR := $(shell $(FC) -dumpversion | cut -d'.' -f2) +_GCCMAJOR := $(shell $(FC) -dumpversion 2>/dev/null | cut -d'.' -f1) +_GCCMINOR := $(shell $(FC) -dumpversion 2>/dev/null | cut -d'.' -f2) # The ports system uses major and minor for GCC < 5 (e.g. gcc49 for GCC 4.9), otherwise major only ifeq ($(_GCCMAJOR),4) @@ -702,6 +722,13 @@ JCXXFLAGS += -DGC_DEBUG_ENV JCFLAGS += -DGC_DEBUG_ENV endif +ifeq ($(WITH_DTRACE), 1) +JCXXFLAGS += -DUSE_DTRACE +JCFLAGS += -DUSE_DTRACE +DTRACE := dtrace +else +endif + # =========================================================================== # Select the cpu architecture to target, or automatically detects the user's compiler @@ -734,6 +761,11 @@ XC_HOST := $(ARCH)$(shell echo $(BUILD_MACHINE) | sed "s/[^-]*\(.*\)$$/\1/") endif endif +# Normalize ppc64le to powerpc64le +ifeq ($(ARCH), ppc64le) +override ARCH := powerpc64le +endif + ifeq ($(ARCH),mingw32) $(error "the mingw32 compiler you are using fails the openblas testsuite. please see the README.windows document for a replacement") else ifeq (cygwin, $(shell $(CC) -dumpmachine | cut -d\- -f3)) @@ -839,6 +871,10 @@ ifneq (,$(filter $(ARCH), powerpc64le ppc64le)) DIST_ARCH:=ppc64le endif ifeq (1,$(ISX86)) +# on x86 make sure not to use 80 bit math when we want 64 bit math. +ifeq (32,$(BINARY)) +JCFLAGS += -mfpmath=sse +endif DIST_ARCH:=$(BINARY) endif ifneq (,$(findstring arm,$(ARCH))) @@ -860,9 +896,11 @@ endif ifneq (,$(findstring aarch64,$(ARCH))) OPENBLAS_DYNAMIC_ARCH:=0 OPENBLAS_TARGET_ARCH:=ARMV8 +USE_BLAS64:=1 +BINARY:=64 ifeq ($(OS),Darwin) # Apple Chips are all at least A12Z -MCPU:=apple-a12 +MCPU:=apple-m1 endif endif @@ -909,7 +947,6 @@ JCXXFLAGS += -D_FILE_OFFSET_BITS=64 endif # Set some ARCH-specific flags -ifneq ($(USEICC),1) ifeq ($(ISX86),1) CC += -m$(BINARY) CXX += -m$(BINARY) @@ -918,7 +955,6 @@ CC_ARG += -m$(BINARY) CXX_ARG += -m$(BINARY) FC_ARG += -m$(BINARY) endif -endif ifeq ($(OS),WINNT) ifneq ($(ARCH),x86_64) @@ -949,8 +985,8 @@ JCPPFLAGS+=-DSYSTEM_LIBUNWIND endif else ifeq ($(OS),Darwin) -LIBUNWIND:=-losxunwind -JCPPFLAGS+=-DLIBOSXUNWIND +LIBUNWIND:=-lunwind +JCPPFLAGS+=-DLLVMLIBUNWIND else LIBUNWIND:=-lunwind endif @@ -969,19 +1005,21 @@ ifeq ($(USE_SYSTEM_LLVM), 1) JCPPFLAGS+=-DSYSTEM_LLVM endif # SYSTEM_LLVM -ifeq ($(BUILD_OS),$(OS)) -LLVM_CONFIG_HOST := $(LLVM_CONFIG) -else -LLVM_CONFIG_HOST := $(basename $(LLVM_CONFIG))-host$(BUILD_EXE) -ifeq (exists, $(shell [ -f '$(LLVM_CONFIG_HOST)' ] && echo exists )) -ifeq ($(shell $(LLVM_CONFIG_HOST) --version),3.3) -# llvm-config-host <= 3.3 is broken, use llvm-config instead (in an emulator) +# Windows builds need a little help finding the LLVM libraries for llvm-config # use delayed expansion (= not :=) because spawn isn't defined until later -LLVM_CONFIG_HOST = $(call spawn,$(LLVM_CONFIG)) +# WINEPATH is only needed for a wine-based cross compile +LLVM_CONFIG_PATH_FIX = +ifeq ($(OS),WINNT) +LLVM_CONFIG_PATH_FIX = PATH="$(build_bindir):$(PATH)" WINEPATH="$(call cygpath_w,$(build_bindir));$(WINEPATH)" endif + +ifeq ($(BUILD_OS),$(OS)) +LLVM_CONFIG_HOST = $(LLVM_CONFIG_PATH_FIX) $(LLVM_CONFIG) else +LLVM_CONFIG_HOST := $(basename $(LLVM_CONFIG))-host$(BUILD_EXE) +ifneq (exists, $(shell [ -f '$(LLVM_CONFIG_HOST)' ] && echo exists )) # llvm-config-host does not exist (cmake build) -LLVM_CONFIG_HOST = $(call spawn,$(LLVM_CONFIG)) +LLVM_CONFIG_HOST = $(LLVM_CONFIG_PATH_FIX) $(call spawn,$(LLVM_CONFIG)) endif endif @@ -997,6 +1035,12 @@ else PATCHELF := $(build_depsbindir)/patchelf endif +ifeq ($(USE_SYSTEM_LIBWHICH), 1) +LIBWHICH := libwhich +else +LIBWHICH := $(build_depsbindir)/libwhich +endif + # On aarch64 and powerpc64le, we assume the page size is 64K. Our binutils linkers # and such already assume this, but `patchelf` seems to be behind the times. We # explicitly tell it to use this large page size so that when we rewrite rpaths and @@ -1008,11 +1052,7 @@ endif # Use ILP64 BLAS interface when building openblas from source on 64-bit architectures ifeq ($(BINARY), 64) ifeq ($(USE_SYSTEM_BLAS), 1) -ifeq ($(USE_INTEL_MKL), 1) -USE_BLAS64 ?= 1 -else # non MKL system blas is most likely LP64 USE_BLAS64 ?= 0 -endif else USE_BLAS64 ?= 1 endif @@ -1099,8 +1139,21 @@ endif USE_BINARYBUILDER ?= 0 endif +# Auto-detect triplet once, create different versions that we use as defaults below for each BB install target +FC_VERSION := $(shell $(FC) --version 2>/dev/null | head -1) +FC_OR_CC_VERSION := $(or $(FC_VERSION),$(shell $(CC) --version 2>/dev/null | head -1)) +BB_TRIPLET_LIBGFORTRAN_CXXABI := $(shell $(call invoke_python,$(JULIAHOME)/contrib/normalize_triplet.py) $(or $(XC_HOST),$(XC_HOST),$(BUILD_MACHINE)) "$(FC_OR_CC_VERSION)" "$(or $(shell echo '\#include ' | $(CXX) $(CXXFLAGS) -x c++ -dM -E - | grep _GLIBCXX_USE_CXX11_ABI | awk '{ print $$3 }' ),1)") +BB_TRIPLET_LIBGFORTRAN := $(subst $(SPACE),-,$(filter-out cxx%,$(subst -,$(SPACE),$(BB_TRIPLET_LIBGFORTRAN_CXXABI)))) +BB_TRIPLET_CXXABI := $(subst $(SPACE),-,$(filter-out libgfortran%,$(subst -,$(SPACE),$(BB_TRIPLET_LIBGFORTRAN_CXXABI)))) +BB_TRIPLET := $(subst $(SPACE),-,$(filter-out cxx%,$(filter-out libgfortran%,$(subst -,$(SPACE),$(BB_TRIPLET_LIBGFORTRAN_CXXABI))))) + +LIBGFORTRAN_VERSION := $(subst libgfortran,,$(filter libgfortran%,$(subst -,$(SPACE),$(BB_TRIPLET_LIBGFORTRAN)))) + # This is the set of projects that BinaryBuilder dependencies are hooked up for. -BB_PROJECTS := OPENBLAS LLVM SUITESPARSE OPENLIBM GMP MBEDTLS LIBSSH2 NGHTTP2 MPFR CURL LIBGIT2 PCRE LIBUV LIBUNWIND DSFMT OBJCONV ZLIB P7ZIP +# Note: we explicitly _do not_ define `CSL` here, since it requires some more +# advanced techniques to decide whether it should be installed from a BB source +# or not. See `deps/csl.mk` for more detail. +BB_PROJECTS := BLASTRAMPOLINE OPENBLAS LLVM LIBSUITESPARSE OPENLIBM GMP MBEDTLS LIBSSH2 NGHTTP2 MPFR CURL LIBGIT2 PCRE LIBUV LIBUNWIND DSFMT OBJCONV ZLIB P7ZIP define SET_BB_DEFAULT # First, check to see if BB is disabled on a global setting ifeq ($$(USE_BINARYBUILDER),0) @@ -1117,10 +1170,12 @@ endef $(foreach proj,$(BB_PROJECTS),$(eval $(call SET_BB_DEFAULT,$(proj)))) - -# Use the Assertions build -BINARYBUILDER_LLVM_ASSERTS ?= 0 - +# Warn if the user tries to build something that requires `gfortran` but they don't have it installed. +ifeq ($(FC_VERSION),) +ifneq ($(USE_BINARYBUILDER_OPENBLAS)$(USE_BINARYBUILDER_LIBSUITESPARSE),11) +$(error "Attempting to build OpenBLAS or SuiteSparse without a functioning fortran compiler!") +endif +endif # OS specific stuff @@ -1155,51 +1210,49 @@ else ifeq ($(OS), Darwin) RPATH := -Wl,-rpath,'@executable_path/$(build_libdir_rel)' RPATH_ORIGIN := -Wl,-rpath,'@loader_path/' RPATH_ESCAPED_ORIGIN := $(RPATH_ORIGIN) - RPATH_LIB := -Wl,-rpath,'@loader_path/julia/' -Wl,-rpath,'@loader_path/' + RPATH_LIB := -Wl,-rpath,'@loader_path/' else RPATH := -Wl,-rpath,'$$ORIGIN/$(build_libdir_rel)' -Wl,-rpath,'$$ORIGIN/$(build_private_libdir_rel)' -Wl,-rpath-link,$(build_shlibdir) -Wl,-z,origin RPATH_ORIGIN := -Wl,-rpath,'$$ORIGIN' -Wl,-z,origin RPATH_ESCAPED_ORIGIN := -Wl,-rpath,'\$$\$$ORIGIN' -Wl,-z,origin -Wl,-rpath-link,$(build_shlibdir) - RPATH_LIB := -Wl,-rpath,'$$ORIGIN/julia' -Wl,-rpath,'$$ORIGIN' -Wl,-z,origin + RPATH_LIB := -Wl,-rpath,'$$ORIGIN/' -Wl,-z,origin endif # --whole-archive ifeq ($(OS), Darwin) WHOLE_ARCHIVE := -Xlinker -all_load NO_WHOLE_ARCHIVE := -else ifneq ($(USEMSVC), 1) +else WHOLE_ARCHIVE := -Wl,--whole-archive NO_WHOLE_ARCHIVE := -Wl,--no-whole-archive endif ifeq ($(OS), Linux) -OSLIBS += -Wl,--no-as-needed -ldl -lrt -lpthread -Wl,--export-dynamic,--as-needed,--no-whole-archive +OSLIBS += -Wl,--no-as-needed -ldl -lrt -lpthread -latomic -Wl,--export-dynamic,--as-needed,--no-whole-archive # Detect if ifunc is supported IFUNC_DETECT_SRC := 'void (*f0(void))(void) { return (void(*)(void))0L; }; void f(void) __attribute__((ifunc("f0")));' ifeq (supported, $(shell echo $(IFUNC_DETECT_SRC) | $(CC) -Werror -x c - -S -o /dev/null > /dev/null 2>&1 && echo supported)) JCPPFLAGS += -DJULIA_HAS_IFUNC_SUPPORT=1 endif -JLDFLAGS := -Wl,-Bdynamic -ifneq ($(SANITIZE),1) -ifneq ($(SANITIZE_MEMORY),1) -ifneq ($(LLVM_SANITIZE),1) +JLDFLAGS += -Wl,-Bdynamic OSLIBS += -Wl,--version-script=$(JULIAHOME)/src/julia.expmap +ifneq ($(SANITIZE),1) JLDFLAGS += -Wl,-no-undefined endif -endif -endif ifeq (-Bsymbolic-functions, $(shell $(LD) --help | grep -o -e "-Bsymbolic-functions")) JLIBLDFLAGS := -Wl,-Bsymbolic-functions else JLIBLDFLAGS := endif +# Linker doesn't detect automatically that Julia doesn't need executable stack +JLIBLDFLAGS += -Wl,-z,noexecstack else ifneq ($(OS), Darwin) JLIBLDFLAGS := endif ifeq ($(OS), FreeBSD) -JLDFLAGS := -Wl,-Bdynamic -OSLIBS += -lelf -lkvm -lrt -lpthread +JLDFLAGS += -Wl,-Bdynamic +OSLIBS += -lelf -lkvm -lrt -lpthread -latomic # Tweak order of libgcc_s in DT_NEEDED, # make it loaded first to @@ -1216,24 +1269,18 @@ SHLIB_EXT := dylib OSLIBS += -framework CoreFoundation WHOLE_ARCHIVE := -Xlinker -all_load NO_WHOLE_ARCHIVE := -JLDFLAGS := HAVE_SSP := 1 JLIBLDFLAGS := -Wl,-compatibility_version,$(SOMAJOR) -Wl,-current_version,$(JULIA_MAJOR_VERSION).$(JULIA_MINOR_VERSION).$(JULIA_PATCH_VERSION) endif ifeq ($(OS), WINNT) -ifneq ($(USEMSVC), 1) HAVE_SSP := 1 OSLIBS += -Wl,--export-all-symbols -Wl,--version-script=$(JULIAHOME)/src/julia.expmap \ - $(NO_WHOLE_ARCHIVE) -lpsapi -lkernel32 -lws2_32 -liphlpapi -lwinmm -ldbghelp -luserenv -lsecur32 -JLDFLAGS := -Wl,--stack,8388608 + $(NO_WHOLE_ARCHIVE) -lpsapi -lkernel32 -lws2_32 -liphlpapi -lwinmm -ldbghelp -luserenv -lsecur32 -latomic +JLDFLAGS += -Wl,--stack,8388608 ifeq ($(ARCH),i686) JLDFLAGS += -Wl,--large-address-aware endif -else #USEMSVC -OSLIBS += kernel32.lib ws2_32.lib psapi.lib advapi32.lib iphlpapi.lib shell32.lib winmm.lib userenv.lib secur32.lib -JLDFLAGS := -stack:8388608 -endif JCPPFLAGS += -D_WIN32_WINNT=0x0502 UNTRUSTED_SYSTEM_LIBM := 1 endif @@ -1262,38 +1309,6 @@ ifeq ($(USE_PERF_JITEVENTS), 1) JCPPFLAGS += -DJL_USE_PERF_JITEVENTS endif - -# Intel libraries - -ifeq ($(USE_INTEL_LIBM), 1) -USE_SYSTEM_LIBM := 1 -LIBM := -L$(MKLROOT)/../compiler/lib/intel64 -limf -LIBMNAME := libimf -endif - -ifeq ($(USE_INTEL_MKL), 1) -ifeq ($(USE_BLAS64), 1) -export MKL_INTERFACE_LAYER := ILP64 -MKLLIB := $(MKLROOT)/lib/intel64 -else -MKLLIB := $(MKLROOT)/lib/ia32 -endif -USE_SYSTEM_BLAS:=1 -USE_SYSTEM_LAPACK:=1 -LIBBLASNAME := libmkl_rt -LIBLAPACKNAME := libmkl_rt -MKL_LDFLAGS := -L$(MKLLIB) -lmkl_rt -ifneq ($(strip $(MKLLIB)),) - ifeq ($(OS), Linux) - RPATH_MKL := -Wl,-rpath,$(MKLLIB) - RPATH += $(RPATH_MKL) - MKL_LDFLAGS += $(RPATH_MKL) - endif -endif -LIBBLAS := $(MKL_LDFLAGS) -LIBLAPACK := $(MKL_LDFLAGS) -endif - ifeq ($(HAVE_SSP),1) JCPPFLAGS += -DHAVE_SSP=1 ifeq ($(USEGCC),1) @@ -1301,24 +1316,6 @@ OSLIBS += -lssp endif endif -# ATLAS - -# ATLAS must have been previously installed to usr/lib/libatlas -# (built as a shared library, for your platform, single threaded) -USE_ATLAS := 0 -ATLAS_LIBDIR := $(build_libdir) -#or ATLAS_LIBDIR := /path/to/system/atlas/lib - -ifeq ($(USE_ATLAS), 1) -USE_BLAS64 := 0 -USE_SYSTEM_BLAS := 1 -USE_SYSTEM_LAPACK := 1 -LIBBLAS := -L$(ATLAS_LIBDIR) -lsatlas -LIBLAPACK := $(LIBBLAS) -LIBBLASNAME := libsatlas -LIBLAPACKNAME := $(LIBBLASNAME) -endif - # Renaming OpenBLAS symbols, see #4923 and #8734 ifeq ($(USE_SYSTEM_BLAS), 0) ifeq ($(USE_BLAS64), 1) @@ -1333,6 +1330,7 @@ endif # Custom libcxx ifeq ($(BUILD_CUSTOM_LIBCXX),1) +$(error BUILD_CUSTOM_LIBCXX is currently not supported, BUILD_LIBCXX will provide LIBCXX but not link it) LDFLAGS += -L$(build_libdir) CXXLDFLAGS += -L$(build_libdir) -lc++abi -lc++ ifeq ($(USECLANG),1) @@ -1343,19 +1341,14 @@ $(error BUILD_CUSTOM_LIBCXX is currently only supported with Clang. Try setting endif endif # Clang CUSTOM_LD_LIBRARY_PATH := LD_LIBRARY_PATH="$(build_libdir)" -ifeq ($(USEICC),1) -CXXFLAGS += -cxxlib-nostd -static-intel -CLDFLAGS += -static-intel -LDFLAGS += -cxxlib-nostd -static-intel -endif endif # Some special restrictions on BB usage: ifeq ($(USE_SYSTEM_BLAS),1) # Since the names don't line up (`BLAS` vs. `OPENBLAS`), manually gate: USE_BINARYBUILDER_OPENBLAS := 0 -# Disable BB SuiteSparse if we're using system BLAS -USE_BINARYBUILDER_SUITESPARSE := 0 +# Disable BB LIBSUITESPARSE if we're using system BLAS +USE_BINARYBUILDER_LIBSUITESPARSE := 0 endif ifeq ($(USE_SYSTEM_LIBM),1) @@ -1401,7 +1394,7 @@ clean-$$(abspath $(2)/$(3)): ifeq ($(BUILD_OS), WINNT) -cmd //C rmdir $$(call mingw_to_dos,$(2)/$(3),cd $(2) &&) else - -rm -r $$(abspath $(2)/$(3)) + rm -rf $$(abspath $(2)/$(3)) endif $$(abspath $(2)/$(3)): | $$(abspath $(2)) ifeq ($$(BUILD_OS), WINNT) @@ -1409,7 +1402,7 @@ ifeq ($$(BUILD_OS), WINNT) else ifneq (,$$(findstring CYGWIN,$$(BUILD_OS))) @cmd /C mklink /J $$(call cygpath_w,$(2)/$(3)) $$(call cygpath_w,$(1)) else ifdef JULIA_VAGRANT_BUILD - @rm -r $$@ + @rm -rf $$@ @cp -R $$(abspath $(1)) $$@.tmp @mv $$@.tmp $$@ else @@ -1417,6 +1410,9 @@ else endif endef +# Overridable in Make.user +WINE ?= wine + # many of the following targets must be = not := because the expansion of the makefile functions (and $1) shouldn't happen until later ifeq ($(BUILD_OS), WINNT) # MSYS spawn = $(1) @@ -1426,8 +1422,8 @@ spawn = $(1) cygpath_w = `cygpath -w $(1)` else ifeq ($(OS), WINNT) # unix-to-Windows cross-compile -spawn = wine $(1) -cygpath_w = `winepath -w $(1)` +spawn = $(WINE) $(1) +cygpath_w = `$(WINE) winepath.exe -w $(1)` else # not Windows spawn = $(1) cygpath_w = $(1) @@ -1451,6 +1447,75 @@ JULIA_SYSIMG_debug := $(build_private_libdir)/sys-debug.$(SHLIB_EXT) JULIA_SYSIMG_release := $(build_private_libdir)/sys.$(SHLIB_EXT) JULIA_SYSIMG := $(JULIA_SYSIMG_$(JULIA_BUILD_MODE)) +define dep_lib_path +$$($(PYTHON) $(call python_cygpath,$(JULIAHOME)/contrib/relative_path.py) $(1) $(2)) +endef + +LIBJULIAINTERNAL_BUILD_DEPLIB := $(call dep_lib_path,$(build_libdir),$(build_shlibdir)/libjulia-internal.$(JL_MAJOR_SHLIB_EXT)) +LIBJULIAINTERNAL_INSTALL_DEPLIB := $(call dep_lib_path,$(libdir),$(private_shlibdir)/libjulia-internal.$(JL_MAJOR_SHLIB_EXT)) + +LIBJULIAINTERNAL_DEBUG_BUILD_DEPLIB := $(call dep_lib_path,$(build_libdir),$(build_shlibdir)/libjulia-internal-debug.$(JL_MAJOR_SHLIB_EXT)) +LIBJULIAINTERNAL_DEBUG_INSTALL_DEPLIB := $(call dep_lib_path,$(libdir),$(private_shlibdir)/libjulia-internal-debug.$(JL_MAJOR_SHLIB_EXT)) + +LIBJULIACODEGEN_BUILD_DEPLIB := $(call dep_lib_path,$(build_libdir),$(build_shlibdir)/libjulia-codegen.$(JL_MAJOR_SHLIB_EXT)) +LIBJULIACODEGEN_INSTALL_DEPLIB := $(call dep_lib_path,$(libdir),$(private_shlibdir)/libjulia-codegen.$(JL_MAJOR_SHLIB_EXT)) + +LIBJULIACODEGEN_DEBUG_BUILD_DEPLIB := $(call dep_lib_path,$(build_libdir),$(build_shlibdir)/libjulia-codegen-debug.$(JL_MAJOR_SHLIB_EXT)) +LIBJULIACODEGEN_DEBUG_INSTALL_DEPLIB := $(call dep_lib_path,$(libdir),$(private_shlibdir)/libjulia-codegen-debug.$(JL_MAJOR_SHLIB_EXT)) + +ifeq ($(OS),WINNT) +ifeq ($(BINARY),32) +LIBGCC_NAME := libgcc_s_sjlj-1.$(SHLIB_EXT) +else +LIBGCC_NAME := libgcc_s_seh-1.$(SHLIB_EXT) +endif +endif +ifeq ($(OS),Darwin) +ifeq ($(ARCH),aarch64) +LIBGCC_NAME := libgcc_s.1.1.$(SHLIB_EXT) +else +LIBGCC_NAME := libgcc_s.1.$(SHLIB_EXT) +endif +endif +ifneq ($(findstring $(OS),Linux FreeBSD),) +LIBGCC_NAME := libgcc_s.$(SHLIB_EXT).1 +endif + +# USE_SYSTEM_CSL causes it to get symlinked into build_private_shlibdir +ifeq ($(USE_SYSTEM_CSL),1) +LIBGCC_BUILD_DEPLIB := $(call dep_lib_path,$(build_libdir),$(build_private_shlibdir)/$(LIBGCC_NAME)) +else +LIBGCC_BUILD_DEPLIB := $(call dep_lib_path,$(build_libdir),$(build_shlibdir)/$(LIBGCC_NAME)) +endif +LIBGCC_INSTALL_DEPLIB := $(call dep_lib_path,$(libdir),$(private_shlibdir)/$(LIBGCC_NAME)) + +# USE_SYSTEM_LIBM and USE_SYSTEM_OPENLIBM causes it to get symlinked into build_private_shlibdir +ifeq ($(USE_SYSTEM_LIBM),1) +LIBM_BUILD_DEPLIB := $(call dep_lib_path,$(build_libdir),$(build_private_shlibdir)/$(LIBMNAME).$(SHLIB_EXT)) +else ifeq ($(USE_SYSTEM_OPENLIBM),1) +LIBM_BUILD_DEPLIB := $(call dep_lib_path,$(build_libdir),$(build_private_shlibdir)/$(LIBMNAME).$(SHLIB_EXT)) +else +LIBM_BUILD_DEPLIB := $(call dep_lib_path,$(build_libdir),$(build_shlibdir)/$(LIBMNAME).$(SHLIB_EXT)) +endif +LIBM_INSTALL_DEPLIB := $(call dep_lib_path,$(libdir),$(private_shlibdir)/$(LIBMNAME).$(SHLIB_EXT)) + +# We list: +# * libgcc_s, because FreeBSD needs to load ours, not the system one. +# * libopenlibm, because Windows has an untrustworthy libm, and we want to use ours more than theirs +# * libjulia-internal, which must always come second-to-last. +# * libjulia-codegen, which must always come last +# +# We need these four separate variables because: +# * debug builds must link against libjuliadebug, not libjulia +# * install time relative paths are not equal to build time relative paths (../lib vs. ../lib/julia) +# That second point will no longer be true for most deps once they are placed within Artifacts directories. +# Note that we prefix `libjulia-codegen` and `libjulia-internal` with `@` to signify to the loader that it +# should not automatically dlopen() it in its loading loop. +LOADER_BUILD_DEP_LIBS = $(LIBGCC_BUILD_DEPLIB):$(LIBM_BUILD_DEPLIB):@$(LIBJULIAINTERNAL_BUILD_DEPLIB):@$(LIBJULIACODEGEN_BUILD_DEPLIB): +LOADER_DEBUG_BUILD_DEP_LIBS = $(LIBGCC_BUILD_DEPLIB):$(LIBM_BUILD_DEPLIB):@$(LIBJULIAINTERNAL_DEBUG_BUILD_DEPLIB):@$(LIBJULIACODEGEN_DEBUG_BUILD_DEPLIB): +LOADER_INSTALL_DEP_LIBS = $(LIBGCC_INSTALL_DEPLIB):$(LIBM_INSTALL_DEPLIB):@$(LIBJULIAINTERNAL_INSTALL_DEPLIB):@$(LIBJULIACODEGEN_INSTALL_DEPLIB): +LOADER_DEBUG_INSTALL_DEP_LIBS = $(LIBGCC_INSTALL_DEPLIB):$(LIBM_INSTALL_DEPLIB):@$(LIBJULIAINTERNAL_DEBUG_INSTALL_DEPLIB):@$(LIBJULIACODEGEN_DEBUG_INSTALL_DEPLIB): + # Colors for make ifndef VERBOSE VERBOSE := 0 @@ -1468,6 +1533,7 @@ LINKCOLOR:="\033[34;1m" PERLCOLOR:="\033[35m" FLISPCOLOR:="\033[32m" JULIACOLOR:="\033[32;1m" +DTRACECOLOR:="\033[32;1m" SRCCOLOR:="\033[33m" BINCOLOR:="\033[37;1m" @@ -1481,6 +1547,7 @@ PRINT_LINK = printf ' %b %b\n' $(LINKCOLOR)LINK$(ENDCOLOR) $(BINCOLOR)$(GOAL) PRINT_PERL = printf ' %b %b\n' $(PERLCOLOR)PERL$(ENDCOLOR) $(BINCOLOR)$(GOAL)$(ENDCOLOR); $(1) PRINT_FLISP = printf ' %b %b\n' $(FLISPCOLOR)FLISP$(ENDCOLOR) $(BINCOLOR)$(GOAL)$(ENDCOLOR); $(1) PRINT_JULIA = printf ' %b %b\n' $(JULIACOLOR)JULIA$(ENDCOLOR) $(BINCOLOR)$(GOAL)$(ENDCOLOR); $(1) +PRINT_DTRACE = printf ' %b %b\n' $(DTRACECOLOR)DTRACE$(ENDCOLOR) $(BINCOLOR)$(GOAL)$(ENDCOLOR); $(1) else QUIET_MAKE = @@ -1490,20 +1557,12 @@ PRINT_LINK = echo '$(subst ','\'',$(1))'; $(1) PRINT_PERL = echo '$(subst ','\'',$(1))'; $(1) PRINT_FLISP = echo '$(subst ','\'',$(1))'; $(1) PRINT_JULIA = echo '$(subst ','\'',$(1))'; $(1) +PRINT_DTRACE = echo '$(subst ','\'',$(1))'; $(1) endif -define newline # a literal \n - - -endef - # Makefile debugging trick: # call print-VARIABLE to see the runtime value of any variable # (hardened against any special characters appearing in the output) print-%: @echo '$*=$(subst ','\'',$(subst $(newline),\n,$($*)))' - -# Literal values that are hard to use in Makefiles otherwise: -COMMA:=, -SPACE:=$(eval) $(eval) diff --git a/Makefile b/Makefile index 30dfac383bed80..1bd8b66a009be7 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ all: debug release # sort is used to remove potential duplicates DIRS := $(sort $(build_bindir) $(build_depsbindir) $(build_libdir) $(build_private_libdir) $(build_libexecdir) $(build_includedir) $(build_includedir)/julia $(build_sysconfdir)/julia $(build_datarootdir)/julia $(build_datarootdir)/julia/stdlib $(build_man1dir)) ifneq ($(BUILDROOT),$(JULIAHOME)) -BUILDDIRS := $(BUILDROOT) $(addprefix $(BUILDROOT)/,base src src/flisp src/support src/clangsa ui doc deps stdlib test test/embedding test/llvmpasses) +BUILDDIRS := $(BUILDROOT) $(addprefix $(BUILDROOT)/,base src src/flisp src/support src/clangsa cli doc deps stdlib test test/clangsa test/embedding test/llvmpasses) BUILDDIRMAKE := $(addsuffix /Makefile,$(BUILDDIRS)) $(BUILDROOT)/sysimage.mk DIRS := $(DIRS) $(BUILDDIRS) $(BUILDDIRMAKE): | $(BUILDDIRS) @@ -46,9 +46,9 @@ julia_flisp.boot.inc.phony: julia-deps $(BUILDROOT)/doc/_build/html/en/index.html: $(shell find $(BUILDROOT)/base $(BUILDROOT)/doc \( -path $(BUILDROOT)/doc/_build -o -path $(BUILDROOT)/doc/deps -o -name *_constants.jl -o -name *_h.jl -o -name version_git.jl \) -prune -o -type f -print) @$(MAKE) docs -julia-symlink: julia-ui-$(JULIA_BUILD_MODE) +julia-symlink: julia-cli-$(JULIA_BUILD_MODE) ifeq ($(OS),WINNT) - @echo '@"%~dp0"\'"$$(echo $(call rel_path,$(BUILDROOT),$(JULIA_EXECUTABLE)) | tr / '\\')" '%*' > $(BUILDROOT)/julia.bat + echo '@"%~dp0/'"$$(echo '$(call rel_path,$(BUILDROOT),$(JULIA_EXECUTABLE))')"'" %*' | tr / '\\' > $(BUILDROOT)/julia.bat chmod a+x $(BUILDROOT)/julia.bat else ifndef JULIA_VAGRANT_BUILD @@ -59,7 +59,8 @@ endif julia-deps: | $(DIRS) $(build_datarootdir)/julia/base $(build_datarootdir)/julia/test @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/deps -julia-stdlib: | $(DIRS) +# `julia-stdlib` depends on `julia-deps` so that the fake JLL stdlibs can copy in their Artifacts.toml files. +julia-stdlib: | $(DIRS) julia-deps @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/stdlib julia-base: julia-deps $(build_sysconfdir)/julia/startup.jl $(build_man1dir)/julia.1 $(build_datarootdir)/julia/julia-config.jl @@ -71,31 +72,36 @@ julia-libccalltest: julia-deps julia-libllvmcalltest: julia-deps @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libllvmcalltest -julia-src-release julia-src-debug : julia-src-% : julia-deps julia_flisp.boot.inc.phony - @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libjulia-$* +julia-src-release julia-src-debug : julia-src-% : julia-deps julia_flisp.boot.inc.phony julia-cli-% + @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src $* -julia-ui-release julia-ui-debug : julia-ui-% : julia-src-% - @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/ui julia-$* +julia-cli-release julia-cli-debug: julia-cli-% : julia-deps + @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/cli $* -julia-sysimg-ji : julia-stdlib julia-base julia-ui-$(JULIA_BUILD_MODE) | $(build_private_libdir) +julia-sysimg-ji : julia-stdlib julia-base julia-cli-$(JULIA_BUILD_MODE) julia-src-$(JULIA_BUILD_MODE) | $(build_private_libdir) @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT) -f sysimage.mk sysimg-ji JULIA_EXECUTABLE='$(JULIA_EXECUTABLE)' -julia-sysimg-bc : julia-stdlib julia-base julia-ui-$(JULIA_BUILD_MODE) | $(build_private_libdir) +julia-sysimg-bc : julia-stdlib julia-base julia-cli-$(JULIA_BUILD_MODE) julia-src-$(JULIA_BUILD_MODE) | $(build_private_libdir) @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT) -f sysimage.mk sysimg-bc JULIA_EXECUTABLE='$(JULIA_EXECUTABLE)' -julia-sysimg-release julia-sysimg-debug : julia-sysimg-% : julia-sysimg-ji julia-ui-% +julia-sysimg-release julia-sysimg-debug : julia-sysimg-% : julia-sysimg-ji julia-src-% @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT) -f sysimage.mk sysimg-$* -julia-debug julia-release : julia-% : julia-sysimg-% julia-symlink julia-libccalltest julia-libllvmcalltest julia-base-cache +julia-debug julia-release : julia-% : julia-sysimg-% julia-src-% julia-symlink julia-libccalltest julia-libllvmcalltest julia-base-cache debug release : % : julia-% docs: julia-sysimg-$(JULIA_BUILD_MODE) @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/doc JULIA_EXECUTABLE='$(call spawn,$(JULIA_EXECUTABLE_$(JULIA_BUILD_MODE))) --startup-file=no' +docs-revise: + @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/doc JULIA_EXECUTABLE='$(call spawn,$(JULIA_EXECUTABLE_$(JULIA_BUILD_MODE))) --startup-file=no' revise=true + check-whitespace: ifneq ($(NO_GIT), 1) - @$(JULIAHOME)/contrib/check-whitespace.sh + @# Append the directory containing the julia we just built to the end of `PATH`, + @# to give us the best chance of being able to run this check. + @PATH=$(PATH):$(dirname $(JULIA_EXECUTABLE)) $(JULIAHOME)/contrib/check-whitespace.jl else $(warn "Skipping whitespace check because git is unavailable") endif @@ -123,15 +129,16 @@ release-candidate: release testall @echo 2. Update references to the julia version in the source directories, such as in README.md @echo 3. Bump VERSION @echo 4. Increase SOMAJOR and SOMINOR if needed. - @echo 5. Create tag, push to github "\(git tag v\`cat VERSION\` && git push --tags\)" #"` # These comments deal with incompetent syntax highlighting rules - @echo 6. Clean out old .tar.gz files living in deps/, "\`git clean -fdx\`" seems to work #"` - @echo 7. Replace github release tarball with tarballs created from make light-source-dist and make full-source-dist - @echo 8. Check that 'make && make install && make test' succeed with unpacked tarballs even without Internet access. - @echo 9. Follow packaging instructions in doc/build/distributing.md to create binary packages for all platforms - @echo 10. Upload to AWS, update https://julialang.org/downloads and http://status.julialang.org/stable links - @echo 11. Update checksums on AWS for tarball and packaged binaries - @echo 12. Announce on mailing lists - @echo 13. Change master to release-0.X in base/version.jl and base/version_git.sh as in 4cb1e20 + @echo 5. Update SPDX document by running the script contrib/updateSPDX.jl + @echo 6. Create tag, push to github "\(git tag v\`cat VERSION\` && git push --tags\)" #"` # These comments deal with incompetent syntax highlighting rules + @echo 7. Clean out old .tar.gz files living in deps/, "\`git clean -fdx\`" seems to work #"` + @echo 8. Replace github release tarball with tarballs created from make light-source-dist and make full-source-dist with USE_BINARYBUILDER=0 + @echo 9. Check that 'make && make install && make test' succeed with unpacked tarballs even without Internet access. + @echo 10. Follow packaging instructions in doc/build/distributing.md to create binary packages for all platforms + @echo 11. Upload to AWS, update https://julialang.org/downloads and http://status.julialang.org/stable links + @echo 12. Update checksums on AWS for tarball and packaged binaries + @echo 13. Announce on mailing lists + @echo 14. Change master to release-0.X in base/version.jl and base/version_git.sh as in 4cb1e20 @echo $(build_man1dir)/julia.1: $(JULIAHOME)/doc/man/julia.1 | $(build_man1dir) @@ -150,7 +157,8 @@ $(build_depsbindir)/stringreplace: $(JULIAHOME)/contrib/stringreplace.c | $(buil @$(call PRINT_CC, $(HOSTCC) -o $(build_depsbindir)/stringreplace $(JULIAHOME)/contrib/stringreplace.c) julia-base-cache: julia-sysimg-$(JULIA_BUILD_MODE) | $(DIRS) $(build_datarootdir)/julia - @JULIA_BINDIR=$(call cygpath_w,$(build_bindir)) $(call spawn, $(JULIA_EXECUTABLE) --startup-file=no $(call cygpath_w,$(JULIAHOME)/etc/write_base_cache.jl) \ + @JULIA_BINDIR=$(call cygpath_w,$(build_bindir)) WINEPATH="$(call cygpath_w,$(build_bindir));$$WINEPATH" \ + $(call spawn, $(JULIA_EXECUTABLE) --startup-file=no $(call cygpath_w,$(JULIAHOME)/etc/write_base_cache.jl) \ $(call cygpath_w,$(build_datarootdir)/julia/base.cache)) # public libraries, that are installed in $(prefix)/lib @@ -160,33 +168,33 @@ JL_TARGETS += julia-debug endif # private libraries, that are installed in $(prefix)/lib/julia -JL_PRIVATE_LIBS-0 := libccalltest libllvmcalltest +JL_PRIVATE_LIBS-0 := libccalltest libllvmcalltest libjulia-internal libjulia-codegen +ifeq ($(BUNDLE_DEBUG_LIBS),1) +JL_PRIVATE_LIBS-0 += libjulia-internal-debug libjulia-codegen-debug +endif ifeq ($(USE_GPL_LIBS), 1) -JL_PRIVATE_LIBS-0 += libsuitesparse_wrapper -JL_PRIVATE_LIBS-$(USE_SYSTEM_SUITESPARSE) += libamd libcamd libccolamd libcholmod libcolamd libumfpack libspqr libsuitesparseconfig +JL_PRIVATE_LIBS-$(USE_SYSTEM_LIBSUITESPARSE) += libamd libbtf libcamd libccolamd libcholmod libcolamd libklu libldl librbio libspqr libsuitesparseconfig libumfpack endif +JL_PRIVATE_LIBS-$(USE_SYSTEM_LIBBLASTRAMPOLINE) += libblastrampoline JL_PRIVATE_LIBS-$(USE_SYSTEM_PCRE) += libpcre2-8 JL_PRIVATE_LIBS-$(USE_SYSTEM_DSFMT) += libdSFMT -JL_PRIVATE_LIBS-$(USE_SYSTEM_GMP) += libgmp +JL_PRIVATE_LIBS-$(USE_SYSTEM_GMP) += libgmp libgmpxx JL_PRIVATE_LIBS-$(USE_SYSTEM_MPFR) += libmpfr JL_PRIVATE_LIBS-$(USE_SYSTEM_LIBSSH2) += libssh2 JL_PRIVATE_LIBS-$(USE_SYSTEM_NGHTTP2) += libnghttp2 JL_PRIVATE_LIBS-$(USE_SYSTEM_MBEDTLS) += libmbedtls libmbedcrypto libmbedx509 JL_PRIVATE_LIBS-$(USE_SYSTEM_CURL) += libcurl JL_PRIVATE_LIBS-$(USE_SYSTEM_LIBGIT2) += libgit2 +JL_PRIVATE_LIBS-$(USE_SYSTEM_LIBUV) += libuv ifeq ($(OS),WINNT) JL_PRIVATE_LIBS-$(USE_SYSTEM_ZLIB) += zlib else JL_PRIVATE_LIBS-$(USE_SYSTEM_ZLIB) += libz endif ifeq ($(USE_LLVM_SHLIB),1) -JL_PRIVATE_LIBS-$(USE_SYSTEM_LLVM) += libLLVM libLLVM-9jl +JL_PRIVATE_LIBS-$(USE_SYSTEM_LLVM) += libLLVM libLLVM-13jl endif -ifeq ($(OS),Darwin) -JL_PRIVATE_LIBS-$(USE_SYSTEM_LIBUNWIND) += libosxunwind -else JL_PRIVATE_LIBS-$(USE_SYSTEM_LIBUNWIND) += libunwind -endif ifeq ($(USE_SYSTEM_LIBM),0) JL_PRIVATE_LIBS-$(USE_SYSTEM_OPENLIBM) += libopenlibm @@ -197,91 +205,29 @@ ifneq ($(LIBLAPACKNAME),$(LIBBLASNAME)) JL_PRIVATE_LIBS-$(USE_SYSTEM_LAPACK) += $(LIBLAPACKNAME) endif +JL_PRIVATE_LIBS-$(USE_SYSTEM_CSL) += libgfortran libquadmath libstdc++ libgcc_s libgomp libssp libatomic ifeq ($(OS),Darwin) -ifeq ($(USE_SYSTEM_BLAS),1) -ifeq ($(USE_SYSTEM_LAPACK),0) -JL_PRIVATE_LIBS-0 += libgfortblas -endif +JL_PRIVATE_LIBS-$(USE_SYSTEM_CSL) += libc++ endif -endif - -# On FreeBSD, /lib/libgcc_s.so.1 is incompatible with Fortran; to use Fortran on FreeBSD, -# we need to link to the libgcc_s that ships with the same GCC version used by libgfortran. -# To work around this, we copy the GCC libraries we need, namely libgfortran, libgcc_s, -# and libquadmath, into our build library directory, $(build_libdir). We also add them to -# JL_PRIVATE_LIBS-0 so that they know where they need to live at install time. -ifeq ($(OS),FreeBSD) -define std_so -julia-deps: | $$(build_libdir)/$(1).so -$$(build_libdir)/$(1).so: | $$(build_libdir) - $$(INSTALL_M) $$(GCCPATH)/$(1).so* $$(build_libdir) -JL_PRIVATE_LIBS-0 += $(1) -endef - -$(eval $(call std_so,libgfortran)) -$(eval $(call std_so,libgcc_s)) -$(eval $(call std_so,libquadmath)) -endif # FreeBSD - ifeq ($(OS),WINNT) -# find the standard .dll folders -ifeq ($(XC_HOST),) -STD_LIB_PATH ?= $(PATH) +JL_PRIVATE_LIBS-$(USE_SYSTEM_CSL) += libwinpthread else -STD_LIB_PATH := $(shell LANG=C $(CC) -print-search-dirs | grep '^programs: =' | sed -e "s/^programs: =//") -STD_LIB_PATH += :$(shell LANG=C $(CC) -print-search-dirs | grep '^libraries: =' | sed -e "s/^libraries: =//") -ifneq (,$(findstring CYGWIN,$(BUILD_OS))) # the cygwin-mingw32 compiler lies about it search directory paths -STD_LIB_PATH := $(shell echo '$(STD_LIB_PATH)' | sed -e "s!/lib/!/bin/!g") -endif +JL_PRIVATE_LIBS-$(USE_SYSTEM_CSL) += libpthread endif -pathsearch = $(firstword $(wildcard $(addsuffix /$(1),$(subst :, ,$(2))))) - -define std_dll -julia-deps-libs: | $$(build_bindir)/lib$(1).dll $$(build_depsbindir)/lib$(1).dll -$$(build_bindir)/lib$(1).dll: | $$(build_bindir) - cp $$(or $$(call pathsearch,lib$(1).dll,$$(STD_LIB_PATH)),$$(error can't find lib$1.dll)) $$(build_bindir) -$$(build_depsbindir)/lib$(1).dll: | $$(build_depsbindir) - cp $$(or $$(call pathsearch,lib$(1).dll,$$(STD_LIB_PATH)),$$(error can't find lib$1.dll)) $$(build_depsbindir) -JL_TARGETS += $(1) -endef -julia-deps: julia-deps-libs -# Given a list of space-separated libraries, return the first library name that is -# correctly found through `pathsearch`. -define select_std_dll -$(firstword $(foreach name,$(1),$(if $(call pathsearch,lib$(name).dll,$(STD_LIB_PATH)),$(name),))) -endef - -$(eval $(call std_dll,$(call select_std_dll,gfortran-3 gfortran-4 gfortran-5))) -$(eval $(call std_dll,quadmath-0)) -$(eval $(call std_dll,stdc++-6)) -ifeq ($(ARCH),i686) -$(eval $(call std_dll,gcc_s_sjlj-1)) -else -$(eval $(call std_dll,gcc_s_seh-1)) +ifeq ($(OS),Darwin) +ifeq ($(USE_SYSTEM_BLAS),1) +ifeq ($(USE_SYSTEM_LAPACK),0) +JL_PRIVATE_LIBS-0 += libgfortblas +endif endif -$(eval $(call std_dll,ssp-0)) -$(eval $(call std_dll,winpthread-1)) -$(eval $(call std_dll,atomic-1)) endif - define stringreplace - $(build_depsbindir)/stringreplace $$(strings -t x - $1 | grep '$2' | awk '{print $$1;}') '$3' 255 "$(call cygpath_w,$1)" + $(build_depsbindir)/stringreplace $$(strings -t x - $1 | grep $2 | awk '{print $$1;}') $3 255 "$(call cygpath_w,$1)" endef -# Run fixup-libgfortran on all platforms but Windows and FreeBSD. On FreeBSD we -# pull in the GCC libraries earlier and use them for the build to make sure we -# don't inadvertently link to /lib/libgcc_s.so.1, which is incompatible with -# libgfortran, and on Windows we copy them in earlier as well. -ifeq (,$(findstring $(OS),FreeBSD WINNT)) -julia-base: $(build_libdir)/libgfortran*.$(SHLIB_EXT)* -$(build_libdir)/libgfortran*.$(SHLIB_EXT)*: | $(build_libdir) julia-deps - -$(CUSTOM_LD_LIBRARY_PATH) PATH="$(PATH):$(build_depsbindir)" PATCHELF="$(PATCHELF)" FC="$(FC)" $(JULIAHOME)/contrib/fixup-libgfortran.sh --verbose $(build_libdir) -JL_PRIVATE_LIBS-0 += libgfortran libgcc_s libquadmath -endif - install: $(build_depsbindir)/stringreplace $(BUILDROOT)/doc/_build/html/en/index.html ifeq ($(BUNDLE_DEBUG_LIBS),1) @@ -348,8 +294,11 @@ endif done \ done for suffix in $(JL_PRIVATE_LIBS-1) ; do \ - lib=$(build_private_libdir)/$${suffix}.$(SHLIB_EXT); \ - $(INSTALL_M) $$lib $(DESTDIR)$(private_libdir) ; \ + for lib in $(build_private_libdir)/$${suffix}.$(SHLIB_EXT)*; do \ + if [ "$${lib##*.}" != "dSYM" ]; then \ + $(INSTALL_M) $$lib $(DESTDIR)$(private_libdir) ; \ + fi \ + done \ done endif # Install `7z` into libexec/ @@ -401,22 +350,48 @@ else ifneq (,$(findstring $(OS),Linux FreeBSD)) done endif - # Overwrite JL_SYSTEM_IMAGE_PATH in julia library - if [ $(DARWIN_FRAMEWORK) = 0 ]; then \ - RELEASE_TARGET=$(DESTDIR)$(libdir)/libjulia.$(SHLIB_EXT); \ - DEBUG_TARGET=$(DESTDIR)$(libdir)/libjulia-debug.$(SHLIB_EXT); \ + # Overwrite JL_SYSTEM_IMAGE_PATH in libjulia-internal + if [ "$(DARWIN_FRAMEWORK)" = "0" ]; then \ + RELEASE_TARGET=$(DESTDIR)$(private_libdir)/libjulia-internal.$(SHLIB_EXT); \ + DEBUG_TARGET=$(DESTDIR)$(private_libdir)/libjulia-internal-debug.$(SHLIB_EXT); \ else \ RELEASE_TARGET=$(DESTDIR)$(prefix)/$(framework_dylib); \ DEBUG_TARGET=$(DESTDIR)$(prefix)/$(framework_dylib)_debug; \ fi; \ $(call stringreplace,$${RELEASE_TARGET},sys.$(SHLIB_EXT)$$,$(private_libdir_rel)/sys.$(SHLIB_EXT)); \ - if [ $(BUNDLE_DEBUG_LIBS) = 1 ]; then \ + if [ "$(BUNDLE_DEBUG_LIBS)" = "1" ]; then \ $(call stringreplace,$${DEBUG_TARGET},sys-debug.$(SHLIB_EXT)$$,$(private_libdir_rel)/sys-debug.$(SHLIB_EXT)); \ fi; +endif + + # Set rpath for libjulia-internal, which is moving from `../lib` to `../lib/julia`. We only need to do this for Linux/FreeBSD +ifneq (,$(findstring $(OS),Linux FreeBSD)) + $(PATCHELF) --set-rpath '$$ORIGIN:$$ORIGIN/$(reverse_private_libdir_rel)' $(DESTDIR)$(private_libdir)/libjulia-internal.$(SHLIB_EXT) +ifeq ($(BUNDLE_DEBUG_LIBS),1) + $(PATCHELF) --set-rpath '$$ORIGIN:$$ORIGIN/$(reverse_private_libdir_rel)' $(DESTDIR)$(private_libdir)/libjulia-internal-debug.$(SHLIB_EXT) +endif +endif + +ifneq ($(LOADER_BUILD_DEP_LIBS),$(LOADER_INSTALL_DEP_LIBS)) + # Next, overwrite relative path to libjulia-internal in our loader if $$(LOADER_BUILD_DEP_LIBS) != $$(LOADER_INSTALL_DEP_LIBS) + $(call stringreplace,$(DESTDIR)$(shlibdir)/libjulia.$(JL_MAJOR_MINOR_SHLIB_EXT),$(LOADER_BUILD_DEP_LIBS)$$,$(LOADER_INSTALL_DEP_LIBS)) +ifeq ($(OS),Darwin) + # Codesign the libjulia we just modified + $(JULIAHOME)/contrib/codesign.sh "$(MACOS_CODESIGN_IDENTITY)" "$(DESTDIR)$(shlibdir)/libjulia.$(JL_MAJOR_MINOR_SHLIB_EXT)" endif - # On FreeBSD, remove the build's libdir from each library's RPATH + +ifeq ($(BUNDLE_DEBUG_LIBS),1) + $(call stringreplace,$(DESTDIR)$(shlibdir)/libjulia-debug.$(JL_MAJOR_MINOR_SHLIB_EXT),$(LOADER_DEBUG_BUILD_DEP_LIBS)$$,$(LOADER_DEBUG_INSTALL_DEP_LIBS)) +ifeq ($(OS),Darwin) + # Codesign the libjulia we just modified + $(JULIAHOME)/contrib/codesign.sh "$(MACOS_CODESIGN_IDENTITY)" "$(DESTDIR)$(shlibdir)/libjulia-debug.$(JL_MAJOR_MINOR_SHLIB_EXT)" +endif +endif +endif + ifeq ($(OS),FreeBSD) + # On FreeBSD, remove the build's libdir from each library's RPATH $(JULIAHOME)/contrib/fixup-rpath.sh "$(PATCHELF)" $(DESTDIR)$(libdir) $(build_libdir) $(JULIAHOME)/contrib/fixup-rpath.sh "$(PATCHELF)" $(DESTDIR)$(private_libdir) $(build_libdir) $(JULIAHOME)/contrib/fixup-rpath.sh "$(PATCHELF)" $(DESTDIR)$(bindir) $(build_libdir) @@ -457,16 +432,19 @@ endif @$(MAKE) -C $(BUILDROOT) -f $(JULIAHOME)/Makefile install cp $(JULIAHOME)/LICENSE.md $(BUILDROOT)/julia-$(JULIA_COMMIT) ifeq ($(OS), Linux) - -$(JULIAHOME)/contrib/fixup-libstdc++.sh $(DESTDIR)$(libdir) $(DESTDIR)$(private_libdir) - - # Copy over any bundled ca certs we picked up from the system during buildi + # Copy over any bundled ca certs we picked up from the system during build -cp $(build_datarootdir)/julia/cert.pem $(DESTDIR)$(datarootdir)/julia/ endif ifeq ($(OS), WINNT) cd $(BUILDROOT)/julia-$(JULIA_COMMIT)/bin && rm -f llvm* llc.exe lli.exe opt.exe LTO.dll bugpoint.exe macho-dump.exe +endif +ifeq ($(OS),Darwin) + # If we're on macOS, and we have a codesigning identity, then codesign the binary-dist tarball! + $(JULIAHOME)/contrib/codesign.sh "$(MACOS_CODESIGN_IDENTITY)" "$(BUILDROOT)/julia-$(JULIA_COMMIT)" endif cd $(BUILDROOT) && $(TAR) zcvf $(JULIA_BINARYDIST_FILENAME).tar.gz julia-$(JULIA_COMMIT) + exe: # run Inno Setup to compile installer $(call spawn,$(JULIAHOME)/dist-extras/inno/iscc.exe /DAppVersion=$(JULIA_VERSION) /DSourceDir="$(call cygpath_w,$(BUILDROOT)/julia-$(JULIA_COMMIT))" /DRepoDir="$(call cygpath_w,$(JULIAHOME))" /F"$(JULIA_BINARYDIST_FILENAME)" /O"$(call cygpath_w,$(BUILDROOT))" $(INNO_ARGS) $(call cygpath_w,$(JULIAHOME)/contrib/windows/build-installer.iss)) @@ -490,11 +468,14 @@ endif echo "base/version_git.jl" > light-source-dist.tmp # Download all stdlibs and include the tarball filenames in light-source-dist.tmp - @$(MAKE) -C stdlib getall NO_GIT=1 + @$(MAKE) -C stdlib getall DEPS_GIT=0 USE_BINARYBUILDER=0 -ls stdlib/srccache/*.tar.gz >> light-source-dist.tmp + -ls stdlib/*/StdlibArtifacts.toml >> light-source-dist.tmp - # Exclude git, github and CI config files - git ls-files | sed -E -e '/^\..+/d' -e '/\/\..+/d' -e '/appveyor.yml/d' >> light-source-dist.tmp + # Include all git-tracked filenames + git ls-files >> light-source-dist.tmp + + # Include documentation filenames find doc/_build/html >> light-source-dist.tmp # Make tarball with only Julia code + stdlib tarballs @@ -513,7 +494,7 @@ source-dist: # Make tarball with Julia code plus all dependencies full-source-dist: light-source-dist.tmp # Get all the dependencies downloaded - @$(MAKE) -C deps getall NO_GIT=1 + @$(MAKE) -C deps getall DEPS_GIT=0 USE_BINARYBUILDER=0 # Create file full-source-dist.tmp to hold all the filenames that go into the tarball cp light-source-dist.tmp full-source-dist.tmp @@ -531,7 +512,7 @@ clean: | $(CLEAN_TARGETS) @-$(MAKE) -C $(BUILDROOT)/base clean @-$(MAKE) -C $(BUILDROOT)/doc clean @-$(MAKE) -C $(BUILDROOT)/src clean - @-$(MAKE) -C $(BUILDROOT)/ui clean + @-$(MAKE) -C $(BUILDROOT)/cli clean @-$(MAKE) -C $(BUILDROOT)/test clean @-$(MAKE) -C $(BUILDROOT)/stdlib clean -rm -f $(BUILDROOT)/julia @@ -555,7 +536,7 @@ distcleanall: cleanall .PHONY: default debug release check-whitespace release-candidate \ julia-debug julia-release julia-stdlib julia-deps julia-deps-libs \ - julia-ui-release julia-ui-debug julia-src-release julia-src-debug \ + julia-cli-release julia-cli-debug julia-src-release julia-src-debug \ julia-symlink julia-base julia-sysimg julia-sysimg-ji julia-sysimg-release julia-sysimg-debug \ test testall testall1 test test-* test-revise-* \ clean distcleanall cleanall clean-* \ @@ -600,6 +581,9 @@ else LLVM_SIZE := $(build_depsbindir)/llvm-size$(EXE) endif build-stats: +ifeq ($(USE_BINARYBUILDER_LLVM),1) + @$(MAKE) -C deps install-llvm-tools +endif @printf $(JULCOLOR)' ==> ./julia binary sizes\n'$(ENDCOLOR) $(call spawn,$(LLVM_SIZE) -A $(call cygpath_w,$(build_private_libdir)/sys.$(SHLIB_EXT)) \ $(call cygpath_w,$(build_shlibdir)/libjulia.$(SHLIB_EXT)) \ @@ -608,3 +592,6 @@ build-stats: @time $(call spawn,$(build_bindir)/julia$(EXE) -e '') @time $(call spawn,$(build_bindir)/julia$(EXE) -e '') @time $(call spawn,$(build_bindir)/julia$(EXE) -e '') + +print-locale: + @locale diff --git a/NEWS.md b/NEWS.md index 15229f5c28f58e..c2e60b4bc07450 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,188 +1,126 @@ -Julia v1.6 Release Notes +Julia v1.9 Release Notes ======================== New language features --------------------- -* Types written with `where` syntax can now be used to define constructors, e.g. - `(Foo{T} where T)(x) = ...`. -* `<--` and `<-->` are now available as infix operators, with the same precedence - and associativity as other arrow-like operators ([#36666]). -* Compilation and type inference can now be enabled or disabled at the module level - using the experimental macro `Base.Experimental.@compiler_options` ([#37041]). -* The library name passed to `ccall` or `@ccall` can now be an expression involving - global variables and function calls. The expression will be evaluated the first - time the `ccall` executes ([#36458]). -* `ꜛ` (U+A71B), `ꜜ` (U+A71C) and `ꜝ` (U+A71D) can now also be used as operator - suffixes. They can be tab-completed from `\^uparrow`, `\^downarrow` and `\^!` in the REPL - ([#37542]). +* It is now possible to assign to bindings in another module using `setproperty!(::Module, ::Symbol, x)`. ([#44137]) +* Slurping in assignments is now also allowed in non-final position. This is + handled via `Base.split_rest`. ([#42902]) Language changes ---------------- -* The `-->` operator now lowers to a `:call` expression, so it can be defined as - a function like other operators. The dotted version `.-->` is now parsed as well. - For backwards compatibility, `-->` still parses using its own expression head - instead of `:call`. +* New builtins `getglobal(::Module, ::Symbol[, order])` and `setglobal!(::Module, ::Symbol, x[, order])` + for reading from and writing to globals. `getglobal` should now be preferred for accessing globals over + `getfield`. ([#44137]) Compiler/Runtime improvements ----------------------------- -* All platforms can now use `@executable_path` within `jl_load_dynamic_library()`. - This allows executable-relative paths to be embedded within executables on all - platforms, not just MacOS, which the syntax is borrowed from. ([#35627]) -* Constant propogation now occurs through keyword arguments ([#35976]) -* The precompilation cache is now created atomically ([#36416]). Invoking _n_ - Julia processes simultaneously may create _n_ temporary caches. Command-line option changes --------------------------- -* There is no longer a concept of "home project": starting `julia --project=dir` - is now exactly equivalent to starting `julia` and then doing `pkg> activate - $dir` and `julia --project` is exactly equivalent to doing that where - `dir = Base.current_project()`. In particular, this means that if you do - `pkg> activate` after starting `julia` with the `--project` option (or with - `JULIA_PROJECT` set) it will take you to the default active project, which is - `@v1.5` unless you have modified `LOAD_PATH`. ([#36434]) +* In Linux and Windows, `--threads=auto` now tries to infer usable number of CPUs from the + process affinity which is set typically in HPC and cloud environments ([#42340]). +* `--math-mode=fast` is now a no-op ([#41638]). Users are encouraged to use the @fastmath macro instead, which has more well-defined semantics. +* The `--threads` command-line option now accepts `auto|N[,auto|M]` where `M` specifies the + number of interactive threads to create (`auto` currently means 1) ([#42302]). Multi-threading changes ----------------------- +* `Threads.@spawn` now accepts an optional first argument: `:default` or `:interactive`. + An interactive task desires low latency and implicitly agrees to be short duration or to + yield frequently. Interactive tasks will run on interactive threads, if any are specified + when Julia is started ([#42302]). Build system changes -------------------- -* Windows Installer now has the option to 'Add Julia to Path'. To unselect this option - from the commandline simply remove the tasks you do not want to be installed: e.g. - `./julia-installer.exe /TASKS="desktopicon,startmenu,addtopath"`, adds a desktop - icon, a startmenu group icon, and adds Julia to system PATH. - - -Library functions ------------------ - -* The `Base.download` function has been deprecated (silently, by default) in favor of the new `Downloads.download` standard library function ([#37340]). -* The `Base.Grisu` code has been officially removed (float printing was switched to the ryu algorithm code in 1.4) New library functions --------------------- -* New function `Base.kron!` and corresponding overloads for various matrix types for performing Kronecker product in-place. ([#31069]). -* New function `Base.Threads.foreach(f, channel::Channel)` for multithreaded `Channel` consumption. ([#34543]). -* New function `Base.readeach(io, T)` for iteratively performing `read(io, T)`. ([#36150]) -* `Iterators.map` is added. It provides another syntax `Iterators.map(f, iterators...)` - for writing `(f(args...) for args in zip(iterators...))`, i.e. a lazy `map` ([#34352]). -* New function `sincospi` for simultaneously computing `sinpi(x)` and `cospi(x)` more - efficiently ([#35816]). -* New function `addenv` for adding environment mappings into a `Cmd` object, returning the new `Cmd` object. +* `Iterators.flatmap` was added ([#44792]). -New library features --------------------- +Library changes +--------------- -* The `redirect_*` functions can now be called on `IOContext` objects. -* New constructor `NamedTuple(iterator)` that constructs a named tuple from a key-value pair iterator. +* A known concurrency issue of `iterate` methods on `Dict` and other derived objects such + as `keys(::Dict)`, `values(::Dict)`, and `Set` is fixed. These methods of `iterate` can + now be called on a dictionary or set shared by arbitrary tasks provided that there are no + tasks mutating the dictionary or set ([#44534]). +* Predicate function negation `!f` now returns a composed function `(!) ∘ f` instead of an anonymous function ([#44752]). +* `RoundFromZero` now works for non-`BigFloat` types ([#41246]). +* `Dict` can be now shrunk manually by `sizehint!` ([#45004]). +* `@time` now separates out % time spent recompiling invalidated methods ([#45015]). +* `@time_imports` now shows any compilation and recompilation time percentages per import ([#45064]). Standard library changes ------------------------ -* The `nextprod` function now accepts tuples and other array types for its first argument ([#35791]). -* The `reverse(A; dims)` function for multidimensional `A` can now reverse multiple dimensions at once - by passing a tuple for `dims`, and defaults to reversing all dimensions; there is also a multidimensional - in-place `reverse!(A; dims)` ([#37367]). -* The function `isapprox(x,y)` now accepts the `norm` keyword argument also for numeric (i.e., non-array) arguments `x` and `y` ([#35883]). -* `view`, `@view`, and `@views` now work on `AbstractString`s, returning a `SubString` when appropriate ([#35879]). -* All `AbstractUnitRange{<:Integer}`s now work with `SubString`, `view`, `@view` and `@views` on strings ([#35879]). -* `sum`, `prod`, `maximum`, and `minimum` now support `init` keyword argument ([#36188], [#35839]). -* `unique(f, itr; seen=Set{T}())` now allows you to declare the container type used for - keeping track of values returned by `f` on elements of `itr` ([#36280]). -* `Libdl` has been moved to `Base.Libc.Libdl`, however it is still accessible as an stdlib ([#35628]). -* `first` and `last` functions now accept an integer as second argument to get that many - leading or trailing elements of any iterable ([#34868]). -* `intersect` on `CartesianIndices` now returns `CartesianIndices` instead of `Vector{<:CartesianIndex}` ([#36643]). -* `push!(c::Channel, v)` now returns channel `c`. Previously, it returned the pushed value `v` ([#34202]). -* `RegexMatch` objects can now be probed for whether a named capture group exists within it through `haskey()` ([#36717]). -* For consistency `haskey(r::RegexMatch, i::Integer)` has also been added and returns if the capture group for `i` exists ([#37300]). -* A new standard library `TOML` has been added for parsing and printing [TOML files](https://toml.io) ([#37034]). -* A new standard library `Downloads` has been added, which replaces the old `Base.download` function with `Downloads.download`, providing cross-platform, multi-protocol, in-process download functionality implemented with [libcurl](https://curl.haxx.se/libcurl/) ([#37340]). -* The `Pkg.BinaryPlatforms` module has been moved into `Base` as `Base.BinaryPlatforms` and heavily reworked. - Applications that want to be compatible with the old API should continue to import `Pkg.BinaryPlatforms`, - however new users should use `Base.BinaryPlatforms` directly. ([#37320]) -* The `Pkg.Artifacts` module has been imported as a separate standard library. It is still available as - `Pkg.Artifacts`, however starting from Julia v1.6+, packages may import simply `Artifacts` without importing - all of `Pkg` alongside. ([#37320]) +#### Package Manager #### LinearAlgebra -* New method `LinearAlgebra.issuccess(::CholeskyPivoted)` for checking whether pivoted Cholesky factorization was successful ([#36002]). -* `UniformScaling` can now be indexed into using ranges to return dense matrices and vectors ([#24359]). -* New function `LinearAlgebra.BLAS.get_num_threads()` for getting the number of BLAS threads. ([#36360]) -* `(+)(::UniformScaling)` is now defined, making `+I` a valid unary operation. ([#36784]) +* The methods `a / b` and `b \ a` with `a` a scalar and `b` a vector, + which were equivalent to `a * pinv(b)`, have been removed due to the + risk of confusion with elementwise division ([#44358]). +* We are now wholly reliant on libblastrampoline (LBT) for calling + BLAS and LAPACK. OpenBLAS is shipped by default, but building the + system image with other BLAS/LAPACK libraries is not + supported. Instead, it is recommended that the LBT mechanism be used + for swapping BLAS/LAPACK with vendor provided ones. ([#44360]) +* `normalize(x, p=2)` now supports any normed vector space `x`, including scalars ([#44925]). #### Markdown #### Printf -* Complete overhaul of internal code to use the ryu float printing algorithms (from Julia 1.4); leads to consistent 2-5x performance improvements -* New `Printf.tofloat` function allowing custom float types to more easily integrate with Printf formatting by converting their type to `Float16`, `Float32`, `Float64`, or `BigFloat` -* New `Printf.format"..."` and `Printf.Format(...)` functions that allow creating `Printf.Format` objects that can be passed to `Printf.format` for easier dynamic printf formatting -* `Printf.format(f::Printf.Format, args...)` as a non-macro function that applies a printf format `f` to provided `args` - - #### Random +* `randn` and `randexp` now work for any `AbstractFloat` type defining `rand` ([#44714]). #### REPL -* The `AbstractMenu` extension interface of `REPL.TerminalMenus` has been extensively - overhauled. The new interface does not rely on global configuration variables, is more - consistent in delegating printing of the navigation/selection markers, and provides - improved support for dynamic menus. These changes are compatible with the previous - (deprecated) interface, so are non-breaking. - - The new API offers several enhancements: - - + Menus are configured in their constructors via keyword arguments - + For custom menu types, the new `Config` and `MultiSelectConfig` replace the global `CONFIG` Dict - + `request(menu; cursor=1)` allows you to control the initial cursor position in the menu (defaults to first item) - + `MultiSelectMenu` allows you to pass a list of initially-selected items with the `selected` keyword argument - + `writeLine` was deprecated to `writeline`, and `writeline` methods are not expected to print the cursor indicator. - The old `writeLine` continues to work, and any of its method extensions should print the cursor indicator as before. - + `printMenu` has been deprecated to `printmenu`, and it both accepts a state input and returns a state output - that controls the number of terminal lines erased when the menu is next refreshed. This plus related changes - makes `printmenu` work properly when the number of menu items might change depending on user choices. - + `numoptions`, returning the number of items in the menu, has been added as an alternative to implementing `options` - + `suppress_output` (primarily a testing option) has been added as a keyword argument to `request`, - rather than a configuration option - -* Windows REPL now supports 24-bit colors, by correctly interpreting virtual terminal escapes. - - #### SparseArrays -* Display large sparse matrices with a Unicode "spy" plot of their nonzero patterns, and display small sparse matrices by an `Matrix`-like 2d layout of their contents. - #### Dates -* `Quarter` period is defined ([#35519]). -* Zero-valued `FixedPeriod`s and `OtherPeriod`s now compare equal, e.g., - `Year(0) == Day(0)`. The behavior of non-zero `Period`s is not changed. ([#37486]) -#### Statistics +#### Downloads +#### Statistics #### Sockets +#### Tar #### Distributed +* The package environment (active project, `LOAD_PATH`, `DEPOT_PATH`) are now propagated + when adding *local* workers (e.g. with `addprocs(N::Int)` or through the `--procs=N` + command line flag) ([#43270]). +* `addprocs` for local workers now accept the `env` keyword argument for passing + environment variables to the workers processes. This was already supported for + remote workers ([#43270]). #### UUIDs -* Change `uuid1` and `uuid4` to use `Random.RandomDevice()` as default random number generator ([#35872]). -* Added `parse(::Type{UUID}, ::AbstractString)` method + +#### Unicode + +* `graphemes(s, m:n)` returns a substring of the `m`-th to `n`-th graphemes in `s` ([#44266]). + +#### Mmap + +#### DelimitedFiles + Deprecated or removed --------------------- + External dependencies --------------------- @@ -190,5 +128,4 @@ External dependencies Tooling Improvements --------------------- - diff --git a/README.md b/README.md index a600f0f43fd838..abc6e9730f1e6c 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,31 @@ -Code coverage: -[![coveralls][coveralls-img]](https://coveralls.io/r/JuliaLang/julia?branch=master) -[![codecov][codecov-img]](https://codecov.io/github/JuliaLang/julia?branch=master) - -Documentation: -[![version 1][docs-img]](https://docs.julialang.org) - -[travis-img]: https://img.shields.io/travis/JuliaLang/julia/master.svg?label=Linux+/+macOS -[appveyor-img]: https://img.shields.io/appveyor/ci/JuliaLang/julia/master.svg?label=Windows -[coveralls-img]: https://img.shields.io/coveralls/github/JuliaLang/julia/master.svg?label=coveralls -[codecov-img]: https://img.shields.io/codecov/c/github/JuliaLang/julia/master.svg?label=codecov -[docs-img]: https://img.shields.io/badge/docs-v1-blue.svg + + + + + + + + + + + + + + + + +
Documentation + +
Continuous integration + +
Code coverage + +
## The Julia Language @@ -34,7 +47,7 @@ and installing Julia, below. - **Documentation:** - **Packages:** - **Discussion forum:** -- **Slack:** (get an invite from ) +- **Slack:** (get an invite from ) - **YouTube:** - **Code coverage:** @@ -46,7 +59,6 @@ helpful to start contributing to the Julia codebase. - [**StackOverflow**](https://stackoverflow.com/questions/tagged/julia-lang) - [**Twitter**](https://twitter.com/JuliaLanguage) -- [**Meetup**](https://julia.meetup.com/) - [**Learning resources**](https://julialang.org/learning/) ## Binary Installation @@ -71,17 +83,17 @@ recommend you use the official Julia binaries instead. ## Building Julia First, make sure you have all the [required -dependencies](https://github.com/JuliaLang/julia/blob/master/doc/build/build.md#required-build-tools-and-external-libraries) installed. +dependencies](https://github.com/JuliaLang/julia/blob/master/doc/src/devdocs/build/build.md#required-build-tools-and-external-libraries) installed. Then, acquire the source code by cloning the git repository: - git clone git://github.com/JuliaLang/julia.git + git clone https://github.com/JuliaLang/julia.git By default you will be building the latest unstable version of -Julia. However, most users should use the most recent stable version +Julia. However, most users should use the [most recent stable version](https://github.com/JuliaLang/julia/releases) of Julia. You can get this version by changing to the Julia directory and running: - git checkout v1.5.0 + git checkout v1.7.2 Now run `make` to build the `julia` executable. @@ -103,8 +115,8 @@ You can read about [getting started](https://docs.julialang.org/en/v1/manual/getting-started/) in the manual. -In case this default build path did not work, detailed build instructions -are included in the [build documentation](https://github.com/JuliaLang/julia/blob/master/doc/build). +Detailed build instructions, should they be necessary, +are included in the [build documentation](https://github.com/JuliaLang/julia/blob/master/doc/src/devdocs/build/). ### Uninstalling Julia @@ -121,13 +133,12 @@ The Julia source code is organized as follows: | - | - | | `base/` | source code for the Base module (part of Julia's standard library) | | `stdlib/` | source code for other standard library packages | -| `contrib/` | editor support for Julia source, miscellaneous scripts | +| `cli/` | source for the command line interface/REPL | +| `contrib/` | miscellaneous scripts | | `deps/` | external dependencies | -| `doc/src/manual/` | source for the user manual | -| `doc/build/` | detailed notes for building Julia | +| `doc/src/` | source for the user manual | | `src/` | source for Julia language core | | `test/` | test suites | -| `ui/` | source for various front ends | | `usr/` | binaries and shared libraries loaded by Julia's standard libraries | ## Terminal, Editors and IDEs @@ -146,10 +157,8 @@ Support for editing Julia is available for many [Sublime Text](https://github.com/JuliaEditorSupport/Julia-sublime), and many others. -Supported IDEs include: [Juno](http://junolab.org/) (Atom plugin), -[julia-vscode](https://github.com/JuliaEditorSupport/julia-vscode) (VS -Code plugin), and -[julia-intellij](https://github.com/JuliaEditorSupport/julia-intellij) -(IntelliJ IDEA plugin). The popular [Jupyter](https://jupyter.org/) -notebook interface is available through -[IJulia](https://github.com/JuliaLang/IJulia.jl). +For users who prefer IDEs, we recommend using VS Code with the +[julia-vscode](https://www.julia-vscode.org/) plugin. +For notebook users, [Jupyter](https://jupyter.org/) notebook support is available through the +[IJulia](https://github.com/JuliaLang/IJulia.jl) package, and +the [Pluto.jl](https://github.com/fonsp/Pluto.jl) package provides Pluto notebooks. diff --git a/THIRDPARTY.md b/THIRDPARTY.md new file mode 100644 index 00000000000000..4a35bbdb1b7cee --- /dev/null +++ b/THIRDPARTY.md @@ -0,0 +1,56 @@ +The Julia language is licensed under the MIT License (see [LICENSE.md](./LICENSE.md) ). The "language" consists +of the compiler (the contents of src/), most of the standard library (base/), +and some utilities (most of the rest of the files in this repository). See below +for exceptions. + +- [crc32c.c](https://stackoverflow.com/questions/17645167/implementing-sse-4-2s-crc32c-in-software) (CRC-32c checksum code by Mark Adler) [[ZLib](https://opensource.org/licenses/Zlib)]. +- [LDC](https://github.com/ldc-developers/ldc/blob/master/LICENSE) (for ccall/cfunction ABI definitions) [BSD-3]. The portion of code that Julia uses from LDC is [BSD-3] licensed. +- [LLVM](https://releases.llvm.org/3.9.0/LICENSE.TXT) (for parts of src/disasm.cpp) [UIUC] +- [MINGW](https://sourceforge.net/p/mingw/mingw-org-wsl/ci/legacy/tree/mingwrt/mingwex/dirname.c) (for dirname implementation on Windows) [MIT] +- [NetBSD](https://www.netbsd.org/about/redistribution.html) (for setjmp, longjmp, and strptime implementations on Windows) [BSD-3] +- [Python](https://docs.python.org/3/license.html) (for strtod implementation on Windows) [PSF] +- [FEMTOLISP](https://github.com/JeffBezanson/femtolisp) [BSD-3] + +The following components included in Julia `Base` have their own separate licenses: + +- base/ryu/* [Boost] (see [ryu](https://github.com/ulfjack/ryu/blob/master/LICENSE-Boost)) +- base/special/{rem_pio2,hyperbolic}.jl [Freely distributable with preserved copyright notice] (see [FDLIBM](https://www.netlib.org/fdlibm)) + +The Julia language links to the following external libraries, which have their +own licenses: + +- [LIBUNWIND](https://github.com/libunwind/libunwind/blob/master/LICENSE) [MIT] +- [LIBUV](https://github.com/JuliaLang/libuv/blob/julia-uv2-1.39.0/LICENSE) [MIT] +- [LLVM](https://releases.llvm.org/12.0.1/LICENSE.TXT) [APACHE 2.0 with LLVM Exception] +- [UTF8PROC](https://github.com/JuliaStrings/utf8proc) [MIT] + +Julia's `stdlib` uses the following external libraries, which have their own licenses: + +- [DSFMT](https://github.com/MersenneTwister-Lab/dSFMT/blob/master/LICENSE.txt) [BSD-3] +- [OPENLIBM](https://github.com/JuliaMath/openlibm/blob/master/LICENSE.md) [MIT, BSD-2, ISC] +- [GMP](https://gmplib.org/manual/Copying.html#Copying) [LGPL3+ or GPL2+] +- [LIBGIT2](https://github.com/libgit2/libgit2/blob/development/COPYING) [GPL2+ with unlimited linking exception] +- [CURL](https://curl.haxx.se/docs/copyright.html) [MIT/X derivative] +- [LIBSSH2](https://github.com/libssh2/libssh2/blob/master/COPYING) [BSD-3] +- [MBEDTLS](https://github.com/ARMmbed/mbedtls/blob/development/LICENSE) [Apache 2.0] +- [MPFR](https://www.mpfr.org/mpfr-current/mpfr.html#Copying) [LGPL3+] +- [OPENBLAS](https://raw.github.com/xianyi/OpenBLAS/master/LICENSE) [BSD-3] +- [LAPACK](https://netlib.org/lapack/LICENSE.txt) [BSD-3] +- [PCRE](https://www.pcre.org/licence.txt) [BSD-3] +- [SUITESPARSE](https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/LICENSE.txt) [mix of LGPL2+ and GPL2+; see individual module licenses] +- [LIBBLASTRAMPOLINE](https://github.com/staticfloat/libblastrampoline/blob/main/LICENSE) [MIT] +- [NGHTTP2](https://github.com/nghttp2/nghttp2/blob/master/COPYING) [MIT] + +Julia's build process uses the following external tools: + +- [PATCHELF](https://nixos.org/patchelf.html) +- [OBJCONV](https://www.agner.org/optimize/#objconv) +- [LIBWHICH](https://github.com/vtjnash/libwhich/blob/master/LICENSE) [MIT] + +Julia bundles the following external programs and libraries: + +- [7-Zip](https://www.7-zip.org/license.txt) +- [ZLIB](https://zlib.net/zlib_license.html) + +On some platforms, distributions of Julia contain SSL certificate authority certificates, +released under the [Mozilla Public License](https://en.wikipedia.org/wiki/Mozilla_Public_License). diff --git a/VERSION b/VERSION index 17b2cde89054fd..e889581dd8a308 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.0-DEV +1.9.0-DEV diff --git a/base/Base.jl b/base/Base.jl index 057c512887c6c2..e3fec462215ef6 100644 --- a/base/Base.jl +++ b/base/Base.jl @@ -20,19 +20,48 @@ include(path::String) = include(Base, path) const is_primary_base_module = ccall(:jl_module_parent, Ref{Module}, (Any,), Base) === Core.Main ccall(:jl_set_istopmod, Cvoid, (Any, Bool), Base, is_primary_base_module) +# The @inline/@noinline macros that can be applied to a function declaration are not available +# until after array.jl, and so we will mark them within a function body instead. +macro inline() Expr(:meta, :inline) end +macro noinline() Expr(:meta, :noinline) end + # Try to help prevent users from shooting them-selves in the foot # with ambiguities by defining a few common and critical operations # (and these don't need the extra convert code) -getproperty(x::Module, f::Symbol) = getfield(x, f) -setproperty!(x::Module, f::Symbol, v) = setfield!(x, f, v) -getproperty(x::Type, f::Symbol) = getfield(x, f) -setproperty!(x::Type, f::Symbol, v) = setfield!(x, f, v) -getproperty(x::Tuple, f::Int) = getfield(x, f) +getproperty(x::Module, f::Symbol) = (@inline; getglobal(x, f)) +getproperty(x::Type, f::Symbol) = (@inline; getfield(x, f)) +setproperty!(x::Type, f::Symbol, v) = error("setfield! fields of Types should not be changed") +getproperty(x::Tuple, f::Int) = (@inline; getfield(x, f)) setproperty!(x::Tuple, f::Int, v) = setfield!(x, f, v) # to get a decent error -getproperty(x, f::Symbol) = getfield(x, f) +getproperty(x, f::Symbol) = (@inline; getfield(x, f)) setproperty!(x, f::Symbol, v) = setfield!(x, f, convert(fieldtype(typeof(x), f), v)) +dotgetproperty(x, f) = getproperty(x, f) + +getproperty(x::Module, f::Symbol, order::Symbol) = (@inline; getglobal(x, f, order)) +function setproperty!(x::Module, f::Symbol, v, order::Symbol=:monotonic) + @inline + val::Core.get_binding_type(x, f) = v + return setglobal!(x, f, val, order) +end +getproperty(x::Type, f::Symbol, order::Symbol) = (@inline; getfield(x, f, order)) +setproperty!(x::Type, f::Symbol, v, order::Symbol) = error("setfield! fields of Types should not be changed") +getproperty(x::Tuple, f::Int, order::Symbol) = (@inline; getfield(x, f, order)) +setproperty!(x::Tuple, f::Int, v, order::Symbol) = setfield!(x, f, v, order) # to get a decent error + +getproperty(x, f::Symbol, order::Symbol) = (@inline; getfield(x, f, order)) +setproperty!(x, f::Symbol, v, order::Symbol) = (@inline; setfield!(x, f, convert(fieldtype(typeof(x), f), v), order)) + +swapproperty!(x, f::Symbol, v, order::Symbol=:notatomic) = + (@inline; Core.swapfield!(x, f, convert(fieldtype(typeof(x), f), v), order)) +modifyproperty!(x, f::Symbol, op, v, order::Symbol=:notatomic) = + (@inline; Core.modifyfield!(x, f, op, v, order)) +replaceproperty!(x, f::Symbol, expected, desired, success_order::Symbol=:notatomic, fail_order::Symbol=success_order) = + (@inline; Core.replacefield!(x, f, expected, convert(fieldtype(typeof(x), f), desired), success_order, fail_order)) + +convert(::Type{Any}, Core.@nospecialize x) = x +convert(::Type{T}, x::T) where {T} = x include("coreio.jl") eval(x) = Core.eval(Base, x) @@ -78,6 +107,9 @@ include("options.jl") include("promotion.jl") include("tuple.jl") include("expr.jl") +Pair{A, B}(@nospecialize(a), @nospecialize(b)) where {A, B} = (@inline; Pair{A, B}(convert(A, a)::A, convert(B, b)::B)) +#Pair{Any, B}(@nospecialize(a::Any), b) where {B} = (@inline; Pair{Any, B}(a, Base.convert(B, b)::B)) +#Pair{A, Any}(a, @nospecialize(b::Any)) where {A} = (@inline; Pair{A, Any}(Base.convert(A, a)::A, b)) include("pair.jl") include("traits.jl") include("range.jl") @@ -95,6 +127,9 @@ include("refpointer.jl") include("checked.jl") using .Checked +# Lazy strings +include("strings/lazy.jl") + # array structures include("indices.jl") include("array.jl") @@ -115,6 +150,24 @@ using .Iterators: Flatten, Filter, product # for generators include("namedtuple.jl") +# For OS specific stuff +# We need to strcat things here, before strings are really defined +function strcat(x::String, y::String) + out = ccall(:jl_alloc_string, Ref{String}, (Csize_t,), Core.sizeof(x) + Core.sizeof(y)) + GC.@preserve x y out begin + out_ptr = unsafe_convert(Ptr{UInt8}, out) + unsafe_copyto!(out_ptr, unsafe_convert(Ptr{UInt8}, x), Core.sizeof(x)) + unsafe_copyto!(out_ptr + Core.sizeof(x), unsafe_convert(Ptr{UInt8}, y), Core.sizeof(y)) + end + return out +end +include(strcat((length(Core.ARGS)>=2 ? Core.ARGS[2] : ""), "build_h.jl")) # include($BUILDROOT/base/build_h.jl) +include(strcat((length(Core.ARGS)>=2 ? Core.ARGS[2] : ""), "version_git.jl")) # include($BUILDROOT/base/version_git.jl) + +# These used to be in build_h.jl and are retained for backwards compatibility +const libblas_name = "libblastrampoline" +const liblapack_name = "libblastrampoline" + # numeric operations include("hashing.jl") include("rounding.jl") @@ -130,7 +183,7 @@ include("abstractarraymath.jl") include("arraymath.jl") # SIMD loops -@pure sizeof(s::String) = Core.sizeof(s) # needed by gensym as called from simdloop +sizeof(s::String) = Core.sizeof(s) # needed by gensym as called from simdloop include("simdloop.jl") using .SimdLoop @@ -158,15 +211,12 @@ include("dict.jl") include("abstractset.jl") include("set.jl") +# Strings include("char.jl") include("strings/basic.jl") include("strings/string.jl") include("strings/substring.jl") -# For OS specific stuff -include(string((length(Core.ARGS)>=2 ? Core.ARGS[2] : ""), "build_h.jl")) # include($BUILDROOT/base/build_h.jl) -include(string((length(Core.ARGS)>=2 ? Core.ARGS[2] : ""), "version_git.jl")) # include($BUILDROOT/base/version_git.jl) - # Initialize DL_LOAD_PATH as early as possible. We are defining things here in # a slightly more verbose fashion than usual, because we're running so early. const DL_LOAD_PATH = String[] @@ -174,8 +224,6 @@ let os = ccall(:jl_get_UNAME, Any, ()) if os === :Darwin || os === :Apple if Base.DARWIN_FRAMEWORK push!(DL_LOAD_PATH, "@loader_path/Frameworks") - else - push!(DL_LOAD_PATH, "@loader_path/julia") end push!(DL_LOAD_PATH, "@loader_path") end @@ -191,9 +239,9 @@ include("iobuffer.jl") # strings & printing include("intfuncs.jl") include("strings/strings.jl") +include("regex.jl") include("parse.jl") include("shell.jl") -include("regex.jl") include("show.jl") include("arrayshow.jl") include("methodshow.jl") @@ -202,12 +250,11 @@ include("methodshow.jl") include("cartesian.jl") using .Cartesian include("multidimensional.jl") -include("permuteddimsarray.jl") -using .PermutedDimsArrays include("broadcast.jl") using .Broadcast -using .Broadcast: broadcasted, broadcasted_kwsyntax, materialize, materialize! +using .Broadcast: broadcasted, broadcasted_kwsyntax, materialize, materialize!, + broadcast_preserving_zero_d, andand, oror # missing values include("missing.jl") @@ -220,7 +267,9 @@ include("sysinfo.jl") include("libc.jl") using .Libc: getpid, gethostname, time -include("env.jl") +# Logging +include("logging.jl") +using .CoreLogging # Concurrency include("linked_list.jl") @@ -228,16 +277,12 @@ include("condition.jl") include("threads.jl") include("lock.jl") include("channels.jl") +include("partr.jl") include("task.jl") include("threads_overloads.jl") include("weakkeydict.jl") -# Logging -include("logging.jl") -using .CoreLogging - -# BinaryPlatforms, used by Artifacts -include("binaryplatforms.jl") +include("env.jl") # functions defined in Random function rand end @@ -266,7 +311,7 @@ const (∛)=cbrt delete_method(which(include, (Module, String))) let SOURCE_PATH = "" global function include(mod::Module, path::String) - prev = SOURCE_PATH + prev = SOURCE_PATH::String path = normpath(joinpath(dirname(prev), path)) Core.println(path) ccall(:jl_uv_flush, Nothing, (Ptr{Nothing},), Core.io_pointer(Core.stdout)) @@ -279,9 +324,12 @@ let SOURCE_PATH = "" end # reduction along dims -include("reducedim.jl") # macros in this file relies on string.jl +include("reducedim.jl") # macros in this file rely on string.jl include("accumulate.jl") +include("permuteddimsarray.jl") +using .PermutedDimsArrays + # basic data structures include("ordering.jl") using .Order @@ -290,6 +338,9 @@ using .Order include("sort.jl") using .Sort +# BinaryPlatforms, used by Artifacts. Needs `Sort`. +include("binaryplatforms.jl") + # Fast math include("fastmath.jl") using .FastMath @@ -314,9 +365,6 @@ using .MPFR include("combinatorics.jl") -# more hashing definitions -include("hashing2.jl") - # irrational mathematical constants include("irrationals.jl") include("mathconstants.jl") @@ -325,16 +373,19 @@ using .MathConstants: ℯ, π, pi # metaprogramming include("meta.jl") +# Stack frames and traces +include("stacktraces.jl") +using .StackTraces + +# experimental API's +include("experimental.jl") + # utilities include("deepcopy.jl") include("download.jl") include("summarysize.jl") include("errorshow.jl") -# Stack frames and traces -include("stacktraces.jl") -using .StackTraces - include("initdefs.jl") # worker threads @@ -352,9 +403,6 @@ include("util.jl") include("asyncmap.jl") -# experimental API's -include("experimental.jl") - # deprecated functions include("deprecated.jl") @@ -381,23 +429,66 @@ include(mapexpr::Function, mod::Module, _path::AbstractString) = _include(mapexp end_base_include = time_ns() +const _sysimage_modules = PkgId[] +in_sysimage(pkgid::PkgId) = pkgid in _sysimage_modules + +# Precompiles for Revise and other packages +# TODO: move these to contrib/generate_precompile.jl +# The problem is they don't work there +for match = _methods(+, (Int, Int), -1, get_world_counter()) + m = match.method + delete!(push!(Set{Method}(), m), m) + copy(Core.Compiler.retrieve_code_info(Core.Compiler.specialize_method(match))) + + empty!(Set()) + push!(push!(Set{Union{GlobalRef,Symbol}}(), :two), GlobalRef(Base, :two)) + (setindex!(Dict{String,Base.PkgId}(), Base.PkgId(Base), "file.jl"))["file.jl"] + (setindex!(Dict{Symbol,Vector{Int}}(), [1], :two))[:two] + (setindex!(Dict{Base.PkgId,String}(), "file.jl", Base.PkgId(Base)))[Base.PkgId(Base)] + (setindex!(Dict{Union{GlobalRef,Symbol}, Vector{Int}}(), [1], :two))[:two] + (setindex!(IdDict{Type, Union{Missing, Vector{Tuple{LineNumberNode, Expr}}}}(), missing, Int))[Int] + Dict{Symbol, Union{Nothing, Bool, Symbol}}(:one => false)[:one] + Dict(Base => [:(1+1)])[Base] + Dict(:one => [1])[:one] + Dict("abc" => Set())["abc"] + pushfirst!([], sum) + get(Base.pkgorigins, Base.PkgId(Base), nothing) + sort!([1,2,3]) + unique!([1,2,3]) + cumsum([1,2,3]) + append!(Int[], BitSet()) + isempty(BitSet()) + delete!(BitSet([1,2]), 3) + deleteat!(Int32[1,2,3], [1,3]) + deleteat!(Any[1,2,3], [1,3]) + Core.svec(1, 2) == Core.svec(3, 4) + any(t->t[1].line > 1, [(LineNumberNode(2,:none), :(1+1))]) + + # Code loading uses this + sortperm(mtime.(readdir(".")), rev=true) + # JLLWrappers uses these + Dict{UUID,Set{String}}()[UUID("692b3bcd-3c85-4b1f-b108-f13ce0eb3210")] = Set{String}() + get!(Set{String}, Dict{UUID,Set{String}}(), UUID("692b3bcd-3c85-4b1f-b108-f13ce0eb3210")) + eachindex(IndexLinear(), Expr[]) + push!(Expr[], Expr(:return, false)) + vcat(String[], String[]) + k, v = (:hello => nothing) + precompile(indexed_iterate, (Pair{Symbol, Union{Nothing, String}}, Int)) + precompile(indexed_iterate, (Pair{Symbol, Union{Nothing, String}}, Int, Int)) + # Preferences uses these + precompile(get_preferences, (UUID,)) + precompile(record_compiletime_preference, (UUID, String)) + get(Dict{String,Any}(), "missing", nothing) + delete!(Dict{String,Any}(), "missing") + for (k, v) in Dict{String,Any}() + println(k) + end + + break # only actually need to do this once +end + if is_primary_base_module function __init__() - # try to ensuremake sure OpenBLAS does not set CPU affinity (#1070, #9639) - if !haskey(ENV, "OPENBLAS_MAIN_FREE") && !haskey(ENV, "GOTOBLAS_MAIN_FREE") - ENV["OPENBLAS_MAIN_FREE"] = "1" - end - # And try to prevent openblas from starting too many threads, unless/until specifically requested - if !haskey(ENV, "OPENBLAS_NUM_THREADS") && !haskey(ENV, "OMP_NUM_THREADS") - cpu_threads = Sys.CPU_THREADS::Int - if cpu_threads > 8 # always at most 8 - ENV["OPENBLAS_NUM_THREADS"] = "8" - elseif haskey(ENV, "JULIA_CPU_THREADS") # or exactly as specified - ENV["OPENBLAS_NUM_THREADS"] = cpu_threads - end # otherwise, trust that openblas will pick CPU_THREADS anyways, without any intervention - end - # for the few uses of Libc.rand in Base: - Libc.srand() # Base library init reinit_stdio() Multimedia.reinit_displays() # since Multimedia.displays uses stdout as fallback @@ -405,9 +496,17 @@ function __init__() init_depot_path() init_load_path() init_active_project() + append!(empty!(_sysimage_modules), keys(loaded_modules)) + if haskey(ENV, "JULIA_MAX_NUM_PRECOMPILE_FILES") + MAX_NUM_PRECOMPILE_FILES[] = parse(Int, ENV["JULIA_MAX_NUM_PRECOMPILE_FILES"]) + end nothing end +# enable threads support +@eval PCRE PCRE_COMPILE_LOCK = Threads.SpinLock() + end + end # baremodule Base diff --git a/base/Enums.jl b/base/Enums.jl index 06860402fbcb1c..f0a3c4c9f3a308 100644 --- a/base/Enums.jl +++ b/base/Enums.jl @@ -25,10 +25,16 @@ Base.isless(x::T, y::T) where {T<:Enum} = isless(basetype(T)(x), basetype(T)(y)) Base.Symbol(x::Enum) = namemap(typeof(x))[Integer(x)]::Symbol -Base.print(io::IO, x::Enum) = print(io, Symbol(x)) +function _symbol(x::Enum) + names = namemap(typeof(x)) + x = Integer(x) + get(() -> Symbol(""), names, x)::Symbol +end + +Base.print(io::IO, x::Enum) = print(io, _symbol(x)) function Base.show(io::IO, x::Enum) - sym = Symbol(x) + sym = _symbol(x) if !(get(io, :compact, false)::Bool) from = get(io, :module, Main) def = typeof(x).name.module @@ -119,6 +125,13 @@ To list all the instances of an enum use `instances`, e.g. julia> instances(Fruit) (apple, orange, kiwi) ``` + +It is possible to construct a symbol from an enum instance: + +```jldoctest fruitenum +julia> Symbol(apple) +:apple +``` """ macro enum(T::Union{Symbol,Expr}, syms...) if isempty(syms) @@ -138,8 +151,7 @@ macro enum(T::Union{Symbol,Expr}, syms...) values = Vector{basetype}() seen = Set{Symbol}() namemap = Dict{basetype,Symbol}() - lo = hi = 0 - i = zero(basetype) + lo = hi = i = zero(basetype) hasexpr = false if length(syms) == 1 && syms[1] isa Expr && syms[1].head === :block @@ -180,7 +192,6 @@ macro enum(T::Union{Symbol,Expr}, syms...) if length(values) == 1 lo = hi = i else - lo = min(lo, i) hi = max(hi, i) end i += oneunit(i) diff --git a/base/Makefile b/base/Makefile index 9ba9ee4be2a78a..5c12ab1c149d83 100644 --- a/base/Makefile +++ b/base/Makefile @@ -6,12 +6,6 @@ include $(JULIAHOME)/Make.inc TAGGED_RELEASE_BANNER := "" -ifneq ($(USEMSVC), 1) -CPP_STDOUT := $(CPP) -P -else -CPP_STDOUT := $(CPP) -E -endif - all: $(addprefix $(BUILDDIR)/,pcre_h.jl errno_h.jl build_h.jl.phony features_h.jl file_constants.jl uv_constants.jl version_git.jl.phony) PCRE_CONST := 0x[0-9a-fA-F]+|[0-9]+|\([\-0-9]+\) @@ -23,11 +17,11 @@ endif define parse_features @echo "# $(2) features" >> $@ -@$(call PRINT_PERL, cat ../src/features_$(1).h | perl -lne 'print "const JL_$(2)_$$1 = UInt32($$2)" if /^\s*JL_FEATURE_DEF(?:_NAME)?\(\s*(\w+)\s*,\s*([^,]+)\s*,.*\)\s*(?:\/\/.*)?$$/' >> $@) +@$(call PRINT_PERL, cat $(SRCDIR)/../src/features_$(1).h | perl -lne 'print "const JL_$(2)_$$1 = UInt32($$2)" if /^\s*JL_FEATURE_DEF(?:_NAME)?\(\s*(\w+)\s*,\s*([^,]+)\s*,.*\)\s*(?:\/\/.*)?$$/' >> $@) @echo >> $@ endef -$(BUILDDIR)/features_h.jl: ../src/features_x86.h ../src/features_aarch32.h ../src/features_aarch64.h +$(BUILDDIR)/features_h.jl: $(SRCDIR)/../src/features_x86.h $(SRCDIR)/../src/features_aarch32.h $(SRCDIR)/../src/features_aarch64.h @-rm -f $@ @$(call parse_features,x86,X86) @$(call parse_features,aarch32,AArch32) @@ -42,7 +36,7 @@ $(BUILDDIR)/errno_h.jl: $(BUILDDIR)/file_constants.jl: $(SRCDIR)/../src/file_constants.h @$(call PRINT_PERL, $(CPP_STDOUT) -DJULIA $< | perl -nle 'print "$$1 0o$$2" if /^(\s*const\s+[A-z_]+\s+=)\s+(0[0-9]*)\s*$$/; print "$$1" if /^\s*(const\s+[A-z_]+\s+=\s+([1-9]|0x)[0-9A-z]*)\s*$$/' > $@) -$(BUILDDIR)/uv_constants.jl: $(SRCDIR)/../src/uv_constants.h $(build_includedir)/uv/errno.h +$(BUILDDIR)/uv_constants.jl: $(SRCDIR)/../src/uv_constants.h $(LIBUV_INC)/uv/errno.h @$(call PRINT_PERL, $(CPP_STDOUT) "-I$(LIBUV_INC)" -DJULIA $< | tail -n 16 > $@) $(BUILDDIR)/build_h.jl.phony: @@ -53,8 +47,6 @@ else @echo "const MACHINE = \"$(XC_HOST)\"" >> $@ endif @echo "const libm_name = \"$(LIBMNAME)\"" >> $@ - @echo "const libblas_name = \"$(LIBBLASNAME)\"" >> $@ - @echo "const liblapack_name = \"$(LIBLAPACKNAME)\"" >> $@ ifeq ($(USE_BLAS64), 1) @echo "const USE_BLAS64 = true" >> $@ else @@ -91,6 +83,7 @@ ifeq ($(DARWIN_FRAMEWORK), 1) else @echo "const DARWIN_FRAMEWORK = false" >> $@ endif + @echo "const BUILD_TRIPLET = \"$(BB_TRIPLET_LIBGFORTRAN_CXXABI)\"" >> $@ @# This to ensure that we always rebuild this file, but only when it is modified do we touch build_h.jl, @# ensuring we rebuild the system image as infrequently as possible @@ -104,7 +97,7 @@ endif $(BUILDDIR)/version_git.jl.phony: $(SRCDIR)/version_git.sh ifneq ($(NO_GIT), 1) sh $< $(SRCDIR) > $@ - @# This to avoid touching git_version.jl when it is not modified, + @# This to avoid touching version_git.jl when it is not modified, @# so that the system image does not need to be rebuilt. @if ! cmp -s $@ version_git.jl; then \ $(call PRINT_PERL,) \ @@ -168,52 +161,87 @@ endif # echo "$$P" define symlink_system_library -symlink_$1: $$(build_private_libdir)/$1.$$(SHLIB_EXT) -$$(build_private_libdir)/$1.$$(SHLIB_EXT): - REALPATH=`$$(call spawn,$$(build_depsbindir)/libwhich) -p $$(notdir $$@)` && \ - $$(call resolve_path,REALPATH) && \ - [ -e "$$$$REALPATH" ] && \ - ([ ! -e "$$@" ] || rm "$$@") && \ - echo ln -sf "$$$$REALPATH" "$$@" && \ - ln -sf "$$$$REALPATH" "$$@" -ifneq ($2,) -ifneq ($$(USE_SYSTEM_$2),0) -SYMLINK_SYSTEM_LIBRARIES += symlink_$1 -endif +libname_$2 := $$(notdir $(call versioned_libname,$2,$3)) +libpath_$2 := $$(shell $$(call spawn,$$(LIBWHICH)) -p $$(libname_$2) 2>/dev/null) +symlink_$2: $$(build_private_libdir)/$$(libname_$2) +$$(build_private_libdir)/$$(libname_$2): + @if [ -e "$$(libpath_$2)" ]; then \ + REALPATH=$$(libpath_$2); \ + $$(call resolve_path,REALPATH) && \ + [ -e "$$$$REALPATH" ] && \ + rm -f "$$@" && \ + echo ln -sf "$$$$REALPATH" "$$@" && \ + ln -sf "$$$$REALPATH" "$$@"; \ + else \ + if [ "$4" != "ALLOW_FAILURE" ]; then \ + echo "System library symlink failure: Unable to locate $$(libname_$2) on your system!" >&2; \ + false; \ + fi; \ + fi +ifneq ($$(USE_SYSTEM_$1),0) +SYMLINK_SYSTEM_LIBRARIES += symlink_$2 endif endef +# libexec executables +symlink_p7zip: $(build_bindir)/7z$(EXE) + +ifneq ($(USE_SYSTEM_P7ZIP),0) +SYMLINK_SYSTEM_LIBRARIES += symlink_p7zip +7Z_PATH := $(shell which 7z$(EXE)) +endif + +$(build_bindir)/7z$(EXE): + [ -e "$(7Z_PATH)" ] && \ + rm -f "$@" && \ + ln -svf "$(7Z_PATH)" "$@" + # the following excludes: libuv.a, libutf8proc.a -$(eval $(call symlink_system_library,$(LIBMNAME))) ifneq ($(USE_SYSTEM_LIBM),0) -SYMLINK_SYSTEM_LIBRARIES += symlink_$(LIBMNAME) +$(eval $(call symlink_system_library,LIBM,$(LIBMNAME))) else ifneq ($(USE_SYSTEM_OPENLIBM),0) -SYMLINK_SYSTEM_LIBRARIES += symlink_$(LIBMNAME) +$(eval $(call symlink_system_library,OPENLIBM,$(LIBMNAME))) endif -$(eval $(call symlink_system_library,libpcre2-8,PCRE)) -$(eval $(call symlink_system_library,libdSFMT,DSFMT)) -$(eval $(call symlink_system_library,$(LIBBLASNAME),BLAS)) +ifeq ($(APPLE_ARCH),arm64) +$(eval $(call symlink_system_library,CSL,libgcc_s,1.1)) +else +$(eval $(call symlink_system_library,CSL,libgcc_s,1)) +endif +ifneq (,$(LIBGFORTRAN_VERSION)) +$(eval $(call symlink_system_library,CSL,libgfortran,$(LIBGFORTRAN_VERSION))) +endif +$(eval $(call symlink_system_library,CSL,libquadmath,0)) +$(eval $(call symlink_system_library,CSL,libstdc++,6)) +# We allow libssp, libatomic and libgomp to fail as they are not available on all systems +$(eval $(call symlink_system_library,CSL,libssp,0,ALLOW_FAILURE)) +$(eval $(call symlink_system_library,CSL,libatomic,1,ALLOW_FAILURE)) +$(eval $(call symlink_system_library,CSL,libgomp,1,ALLOW_FAILURE)) +$(eval $(call symlink_system_library,PCRE,libpcre2-8)) +$(eval $(call symlink_system_library,DSFMT,libdSFMT)) +$(eval $(call symlink_system_library,LIBBLASTRAMPOLINE,libblastrampoline)) +$(eval $(call symlink_system_library,BLAS,$(LIBBLASNAME))) ifneq ($(LIBLAPACKNAME),$(LIBBLASNAME)) -$(eval $(call symlink_system_library,$(LIBLAPACKNAME),LAPACK)) +$(eval $(call symlink_system_library,LAPACK,$(LIBLAPACKNAME))) endif -$(eval $(call symlink_system_library,libgmp,GMP)) -$(eval $(call symlink_system_library,libmpfr,MPFR)) -$(eval $(call symlink_system_library,libmbedtls,MBEDTLS)) -$(eval $(call symlink_system_library,libmbedcrypto,MBEDTLS)) -$(eval $(call symlink_system_library,libmbedx509,MBEDTLS)) -$(eval $(call symlink_system_library,libssh2,LIBSSH2)) -$(eval $(call symlink_system_library,libcurl,CURL)) -$(eval $(call symlink_system_library,libgit2,LIBGIT2)) -$(eval $(call symlink_system_library,libamd,SUITESPARSE)) -$(eval $(call symlink_system_library,libcamd,SUITESPARSE)) -$(eval $(call symlink_system_library,libccolamd,SUITESPARSE)) -$(eval $(call symlink_system_library,libcholmod,SUITESPARSE)) -$(eval $(call symlink_system_library,libcolamd,SUITESPARSE)) -$(eval $(call symlink_system_library,libumfpack,SUITESPARSE)) -$(eval $(call symlink_system_library,libspqr,SUITESPARSE)) -$(eval $(call symlink_system_library,libsuitesparseconfig,SUITESPARSE)) +$(eval $(call symlink_system_library,GMP,libgmp)) +$(eval $(call symlink_system_library,MPFR,libmpfr)) +$(eval $(call symlink_system_library,MBEDTLS,libmbedtls)) +$(eval $(call symlink_system_library,MBEDTLS,libmbedcrypto)) +$(eval $(call symlink_system_library,MBEDTLS,libmbedx509)) +$(eval $(call symlink_system_library,LIBSSH2,libssh2)) +$(eval $(call symlink_system_library,NGHTTP2,libnghttp2)) +$(eval $(call symlink_system_library,CURL,libcurl)) +$(eval $(call symlink_system_library,LIBGIT2,libgit2)) +$(eval $(call symlink_system_library,LIBSUITESPARSE,libamd)) +$(eval $(call symlink_system_library,LIBSUITESPARSE,libcamd)) +$(eval $(call symlink_system_library,LIBSUITESPARSE,libccolamd)) +$(eval $(call symlink_system_library,LIBSUITESPARSE,libcholmod)) +$(eval $(call symlink_system_library,LIBSUITESPARSE,libcolamd)) +$(eval $(call symlink_system_library,LIBSUITESPARSE,libumfpack)) +$(eval $(call symlink_system_library,LIBSUITESPARSE,libspqr)) +$(eval $(call symlink_system_library,LIBSUITESPARSE,libsuitesparseconfig)) # EXCLUDED LIBRARIES (installed/used, but not vendored for use with dlopen): # libunwind endif # WINNT @@ -230,7 +258,7 @@ $(build_private_libdir)/libLLVM.$(SHLIB_EXT): REALPATH=$(LLVM_CONFIG_HOST_LIBS) && \ $(call resolve_path,REALPATH) && \ [ -e "$$REALPATH" ] && \ - ([ ! -e "$@" ] || rm "$@") && \ + rm -f "$@" && \ echo ln -sf "$$REALPATH" "$@" && \ ln -sf "$$REALPATH" "$@" ifneq ($(USE_SYSTEM_LLVM),0) diff --git a/base/abstractarray.jl b/base/abstractarray.jl index 478130f4834da1..239e75df525101 100644 --- a/base/abstractarray.jl +++ b/base/abstractarray.jl @@ -8,6 +8,8 @@ Supertype for `N`-dimensional arrays (or array-like types) with elements of type `T`. [`Array`](@ref) and other types are subtypes of this. See the manual section on the [`AbstractArray` interface](@ref man-interface-array). + +See also: [`AbstractVector`](@ref), [`AbstractMatrix`](@ref), [`eltype`](@ref), [`ndims`](@ref). """ AbstractArray @@ -24,6 +26,8 @@ dimension to just get the length of that dimension. Note that `size` may not be defined for arrays with non-standard indices, in which case [`axes`](@ref) may be useful. See the manual chapter on [arrays with custom indices](@ref man-custom-indices). +See also: [`length`](@ref), [`ndims`](@ref), [`eachindex`](@ref), [`sizeof`](@ref). + # Examples ```jldoctest julia> A = fill(1, (2,3,4)); @@ -45,15 +49,28 @@ Return the valid range of indices for array `A` along dimension `d`. See also [`size`](@ref), and the manual chapter on [arrays with custom indices](@ref man-custom-indices). # Examples + ```jldoctest julia> A = fill(1, (5,6,7)); julia> axes(A, 2) Base.OneTo(6) ``` + +# Usage note + +Each of the indices has to be an `AbstractUnitRange{<:Integer}`, but at the same time can be +a type that uses custom indices. So, for example, if you need a subset, use generalized +indexing constructs like `begin`/`end` or [`firstindex`](@ref)/[`lastindex`](@ref): + +```julia +ix = axes(v, 1) +ix[2:end] # will work for eg Vector, but may fail in general +ix[(begin+1):end] # works for generalized indexes +``` """ function axes(A::AbstractArray{T,N}, d) where {T,N} - @_inline_meta + @inline d::Integer <= N ? axes(A)[d] : OneTo(1) end @@ -62,7 +79,10 @@ end Return the tuple of valid indices for array `A`. +See also: [`size`](@ref), [`keys`](@ref), [`eachindex`](@ref). + # Examples + ```jldoctest julia> A = fill(1, (5,6,7)); @@ -71,8 +91,8 @@ julia> axes(A) ``` """ function axes(A) - @_inline_meta - map(OneTo, size(A)) + @inline + map(oneto, size(A)) end """ @@ -82,7 +102,8 @@ end Return `true` if the indices of `A` start with something other than 1 along any axis. If multiple arguments are passed, equivalent to `has_offset_axes(A) | has_offset_axes(B) | ...`. """ -has_offset_axes(A) = _tuple_any(x->Int(first(x))::Int != 1, axes(A)) +has_offset_axes(A) = _tuple_any(x->Int(first(x))::Int != 1, axes(A)) +has_offset_axes(A::AbstractVector) = Int(firstindex(A))::Int != 1 # improve performance of a common case (ranges) has_offset_axes(A...) = _tuple_any(has_offset_axes, A) has_offset_axes(::Colon) = false @@ -92,12 +113,22 @@ require_one_based_indexing(A...) = !has_offset_axes(A...) || throw(ArgumentError # for d=1. 1d arrays are heavily used, and the first dimension comes up # in other applications. axes1(A::AbstractArray{<:Any,0}) = OneTo(1) -axes1(A::AbstractArray) = (@_inline_meta; axes(A)[1]) -axes1(iter) = OneTo(length(iter)) +axes1(A::AbstractArray) = (@inline; axes(A)[1]) +axes1(iter) = oneto(length(iter)) -unsafe_indices(A) = axes(A) -unsafe_indices(r::AbstractRange) = (OneTo(unsafe_length(r)),) # Ranges use checked_sub for size +""" + keys(a::AbstractArray) + +Return an efficient array describing all valid indices for `a` arranged in the shape of `a` itself. + +They keys of 1-dimensional arrays (vectors) are integers, whereas all other N-dimensional +arrays use [`CartesianIndex`](@ref) to describe their locations. Often the special array +types [`LinearIndices`](@ref) and [`CartesianIndices`](@ref) are used to efficiently +represent these arrays of integers and `CartesianIndex`es, respectively. +Note that the `keys` of an array might not be the most efficient index type; for maximum +performance use [`eachindex`](@ref) instead. +""" keys(a::AbstractArray) = CartesianIndices(axes(a)) keys(a::AbstractVector) = LinearIndices(a) @@ -149,7 +180,45 @@ valtype(A::Type{<:AbstractArray}) = eltype(A) prevind(::AbstractArray, i::Integer) = Int(i)-1 nextind(::AbstractArray, i::Integer) = Int(i)+1 + +""" + eltype(type) + +Determine the type of the elements generated by iterating a collection of the given `type`. +For dictionary types, this will be a `Pair{KeyType,ValType}`. The definition +`eltype(x) = eltype(typeof(x))` is provided for convenience so that instances can be passed +instead of types. However the form that accepts a type argument should be defined for new +types. + +See also: [`keytype`](@ref), [`typeof`](@ref). + +# Examples +```jldoctest +julia> eltype(fill(1f0, (2,2))) +Float32 + +julia> eltype(fill(0x1, (2,2))) +UInt8 +``` +""" +eltype(::Type) = Any +eltype(::Type{Bottom}) = throw(ArgumentError("Union{} does not have elements")) +eltype(x) = eltype(typeof(x)) eltype(::Type{<:AbstractArray{E}}) where {E} = @isdefined(E) ? E : Any + +""" + elsize(type) + +Compute the memory stride in bytes between consecutive elements of `eltype` +stored inside the given `type`, if the array elements are stored densely with a +uniform linear stride. + +# Examples +```jldoctest +julia> Base.elsize(rand(Float32, 10)) +4 +``` +""" elsize(A::AbstractArray) = elsize(typeof(A)) """ @@ -157,6 +226,8 @@ elsize(A::AbstractArray) = elsize(typeof(A)) Return the number of dimensions of `A`. +See also: [`size`](@ref), [`axes`](@ref). + # Examples ```jldoctest julia> A = fill(1, (3,4,5)); @@ -166,7 +237,7 @@ julia> ndims(A) ``` """ ndims(::AbstractArray{T,N}) where {T,N} = N -ndims(::Type{<:AbstractArray{T,N}}) where {T,N} = N +ndims(::Type{<:AbstractArray{<:Any,N}}) where {N} = N """ length(collection) -> Integer @@ -175,6 +246,8 @@ Return the number of elements in the collection. Use [`lastindex`](@ref) to get the last valid index of an indexable collection. +See also: [`size`](@ref), [`ndims`](@ref), [`eachindex`](@ref). + # Examples ```jldoctest julia> length(1:5) @@ -203,13 +276,13 @@ julia> length([1 2; 3 4]) 4 ``` """ -length(t::AbstractArray) = (@_inline_meta; prod(size(t))) +length(t::AbstractArray) = (@inline; prod(size(t))) # `eachindex` is mostly an optimization of `keys` eachindex(itrs...) = keys(itrs...) # eachindex iterates over all indices. IndexCartesian definitions are later. -eachindex(A::AbstractVector) = (@_inline_meta(); axes1(A)) +eachindex(A::AbstractVector) = (@inline(); axes1(A)) @noinline function throw_eachindex_mismatch_indices(::IndexLinear, inds...) @@ -233,7 +306,7 @@ If you supply more than one `AbstractArray` argument, `eachindex` will create an iterable object that is fast for all arguments (a [`UnitRange`](@ref) if all inputs have fast linear indexing, a [`CartesianIndices`](@ref) otherwise). -If the arrays have different sizes and/or dimensionalities, a DimensionMismatch exception +If the arrays have different sizes and/or dimensionalities, a `DimensionMismatch` exception will be thrown. # Examples ```jldoctest @@ -254,27 +327,27 @@ CartesianIndex(1, 1) CartesianIndex(2, 1) ``` """ -eachindex(A::AbstractArray) = (@_inline_meta(); eachindex(IndexStyle(A), A)) +eachindex(A::AbstractArray) = (@inline(); eachindex(IndexStyle(A), A)) function eachindex(A::AbstractArray, B::AbstractArray) - @_inline_meta + @inline eachindex(IndexStyle(A,B), A, B) end function eachindex(A::AbstractArray, B::AbstractArray...) - @_inline_meta + @inline eachindex(IndexStyle(A,B...), A, B...) end -eachindex(::IndexLinear, A::AbstractArray) = (@_inline_meta; OneTo(length(A))) -eachindex(::IndexLinear, A::AbstractVector) = (@_inline_meta; axes1(A)) +eachindex(::IndexLinear, A::AbstractArray) = (@inline; oneto(length(A))) +eachindex(::IndexLinear, A::AbstractVector) = (@inline; axes1(A)) function eachindex(::IndexLinear, A::AbstractArray, B::AbstractArray...) - @_inline_meta + @inline indsA = eachindex(IndexLinear(), A) _all_match_first(X->eachindex(IndexLinear(), X), indsA, B...) || throw_eachindex_mismatch_indices(IndexLinear(), eachindex(A), eachindex.(B)...) indsA end function _all_match_first(f::F, inds, A, B...) where F<:Function - @_inline_meta + @inline (inds == f(A)) & _all_match_first(f, inds, B...) end _all_match_first(f::F, inds) where F<:Function = true @@ -291,6 +364,8 @@ Return the last index of `collection`. If `d` is given, return the last index of The syntaxes `A[end]` and `A[end, end]` lower to `A[lastindex(A)]` and `A[lastindex(A, 1), lastindex(A, 2)]`, respectively. +See also: [`axes`](@ref), [`firstindex`](@ref), [`eachindex`](@ref), [`prevind`](@ref). + # Examples ```jldoctest julia> lastindex([1,2,4]) @@ -300,8 +375,8 @@ julia> lastindex(rand(3,4,5), 2) 4 ``` """ -lastindex(a::AbstractArray) = (@_inline_meta; last(eachindex(IndexLinear(), a))) -lastindex(a::AbstractArray, d) = (@_inline_meta; last(axes(a, d))) +lastindex(a::AbstractArray) = (@inline; last(eachindex(IndexLinear(), a))) +lastindex(a, d) = (@inline; last(axes(a, d))) """ firstindex(collection) -> Integer @@ -309,6 +384,11 @@ lastindex(a::AbstractArray, d) = (@_inline_meta; last(axes(a, d))) Return the first index of `collection`. If `d` is given, return the first index of `collection` along dimension `d`. +The syntaxes `A[begin]` and `A[1, begin]` lower to `A[firstindex(A)]` and +`A[1, firstindex(A, 2)]`, respectively. + +See also: [`first`](@ref), [`axes`](@ref), [`lastindex`](@ref), [`nextind`](@ref). + # Examples ```jldoctest julia> firstindex([1,2,4]) @@ -318,8 +398,8 @@ julia> firstindex(rand(3,4,5), 2) 1 ``` """ -firstindex(a::AbstractArray) = (@_inline_meta; first(eachindex(IndexLinear(), a))) -firstindex(a::AbstractArray, d) = (@_inline_meta; first(axes(a, d))) +firstindex(a::AbstractArray) = (@inline; first(eachindex(IndexLinear(), a))) +firstindex(a, d) = (@inline; first(axes(a, d))) first(a::AbstractArray) = a[first(eachindex(a))] @@ -329,6 +409,8 @@ first(a::AbstractArray) = a[first(eachindex(a))] Get the first element of an iterable collection. Return the start point of an [`AbstractRange`](@ref) even if it is empty. +See also: [`only`](@ref), [`firstindex`](@ref), [`last`](@ref). + # Examples ```jldoctest julia> first(2:2:10) @@ -347,9 +429,14 @@ end """ first(itr, n::Integer) -Get the first `n` elements of the iterable collection `itr`, or fewer elements if `v` is not +Get the first `n` elements of the iterable collection `itr`, or fewer elements if `itr` is not long enough. +See also: [`startswith`](@ref), [`Iterators.take`](@ref). + +!!! compat "Julia 1.6" + This method requires at least Julia 1.6. + # Examples ```jldoctest julia> first(["foo", "bar", "qux"], 2) @@ -378,6 +465,8 @@ Get the last element of an ordered collection, if it can be computed in O(1) tim accomplished by calling [`lastindex`](@ref) to get the last index. Return the end point of an [`AbstractRange`](@ref) even if it is empty. +See also [`first`](@ref), [`endswith`](@ref). + # Examples ```jldoctest julia> last(1:2:10) @@ -392,9 +481,12 @@ last(a) = a[end] """ last(itr, n::Integer) -Get the last `n` elements of the iterable collection `itr`, or fewer elements if `v` is not +Get the last `n` elements of the iterable collection `itr`, or fewer elements if `itr` is not long enough. +!!! compat "Julia 1.6" + This method requires at least Julia 1.6. + # Examples ```jldoctest julia> last(["foo", "bar", "qux"], 2) @@ -421,6 +513,8 @@ end Return a tuple of the memory strides in each dimension. +See also: [`stride`](@ref). + # Examples ```jldoctest julia> A = fill(1, (3,4,5)); @@ -436,6 +530,8 @@ function strides end Return the distance in memory (in number of elements) between adjacent elements in dimension `k`. +See also: [`strides`](@ref). + # Examples ```jldoctest julia> A = fill(1, (3,4,5)); @@ -450,7 +546,13 @@ julia> stride(A,3) function stride(A::AbstractArray, k::Integer) st = strides(A) k ≤ ndims(A) && return st[k] - return sum(st .* size(A)) + ndims(A) == 0 && return 1 + sz = size(A) + s = st[1] * sz[1] + for i in 2:ndims(A) + s += st[i] * sz[i] + end + return s end @inline size_to_strides(s, d, sz...) = (s, size_to_strides(s * d, sz...)...) @@ -487,14 +589,14 @@ end function trailingsize(inds::Indices, n) s = 1 for i=n:length(inds) - s *= unsafe_length(inds[i]) + s *= length(inds[i]) end return s end # This version is type-stable even if inds is heterogeneous function trailingsize(inds::Indices) - @_inline_meta - prod(map(unsafe_length, inds)) + @inline + prod(map(length, inds)) end ## Bounds checking ## @@ -541,18 +643,18 @@ false ``` """ function checkbounds(::Type{Bool}, A::AbstractArray, I...) - @_inline_meta + @inline checkbounds_indices(Bool, axes(A), I) end # Linear indexing is explicitly allowed when there is only one (non-cartesian) index function checkbounds(::Type{Bool}, A::AbstractArray, i) - @_inline_meta + @inline checkindex(Bool, eachindex(IndexLinear(), A), i) end # As a special extension, allow using logical arrays that match the source array exactly function checkbounds(::Type{Bool}, A::AbstractArray{<:Any,N}, I::AbstractArray{Bool,N}) where N - @_inline_meta + @inline axes(A) == axes(I) end @@ -562,7 +664,7 @@ end Throw an error if the specified indices `I` are not in bounds for the given array `A`. """ function checkbounds(A::AbstractArray, I...) - @_inline_meta + @inline checkbounds(Bool, A, I...) || throw_boundserror(A, I) nothing end @@ -588,17 +690,17 @@ of `IA`. See also [`checkbounds`](@ref). """ function checkbounds_indices(::Type{Bool}, IA::Tuple, I::Tuple) - @_inline_meta + @inline checkindex(Bool, IA[1], I[1])::Bool & checkbounds_indices(Bool, tail(IA), tail(I)) end function checkbounds_indices(::Type{Bool}, ::Tuple{}, I::Tuple) - @_inline_meta + @inline checkindex(Bool, OneTo(1), I[1])::Bool & checkbounds_indices(Bool, (), tail(I)) end -checkbounds_indices(::Type{Bool}, IA::Tuple, ::Tuple{}) = (@_inline_meta; all(x->unsafe_length(x)==1, IA)) +checkbounds_indices(::Type{Bool}, IA::Tuple, ::Tuple{}) = (@inline; all(x->length(x)==1, IA)) checkbounds_indices(::Type{Bool}, ::Tuple{}, ::Tuple{}) = true -throw_boundserror(A, I) = (@_noinline_meta; throw(BoundsError(A, I))) +throw_boundserror(A, I) = (@noinline; throw(BoundsError(A, I))) # check along a single dimension """ @@ -609,6 +711,8 @@ Return `true` if the given `index` is within the bounds of arrays can extend this method in order to provide a specialized bounds checking implementation. +See also [`checkbounds`](@ref). + # Examples ```jldoctest julia> checkindex(Bool, 1:20, 8) @@ -630,7 +734,7 @@ end checkindex(::Type{Bool}, indx::AbstractUnitRange, I::AbstractVector{Bool}) = indx == axes1(I) checkindex(::Type{Bool}, indx::AbstractUnitRange, I::AbstractArray{Bool}) = false function checkindex(::Type{Bool}, inds::AbstractUnitRange, I::AbstractArray) - @_inline_meta + @inline b = true for i in I b &= checkindex(Bool, inds, i) @@ -660,7 +764,7 @@ neither mutable nor support 2 dimensions: ```julia-repl julia> similar(1:10, 1, 4) -1×4 Array{Int64,2}: +1×4 Matrix{Int64}: 4419743872 4374413872 4419743888 0 ``` @@ -679,11 +783,12 @@ different element type it will create a regular `Array` instead: ```julia-repl julia> similar(falses(10), Float64, 2, 4) -2×4 Array{Float64,2}: +2×4 Matrix{Float64}: 2.18425e-314 2.18425e-314 2.18425e-314 2.18425e-314 2.18425e-314 2.18425e-314 2.18425e-314 2.18425e-314 ``` +See also: [`undef`](@ref), [`isassigned`](@ref). """ similar(a::AbstractArray{T}) where {T} = similar(a, T) similar(a::AbstractArray, ::Type{T}) where {T} = similar(a, T, to_shape(axes(a))) @@ -696,6 +801,7 @@ similar(a::AbstractArray, ::Type{T}, dims::DimOrInd...) where {T} = similar(a, # define this method to convert supported axes to Ints, with the expectation that an offset array # package will define a method with dims::Tuple{Union{Integer, UnitRange}, Vararg{Union{Integer, UnitRange}}} similar(a::AbstractArray, ::Type{T}, dims::Tuple{Union{Integer, OneTo}, Vararg{Union{Integer, OneTo}}}) where {T} = similar(a, T, to_shape(dims)) +similar(a::AbstractArray, ::Type{T}, dims::Tuple{Integer, Vararg{Integer}}) where {T} = similar(a, T, to_shape(dims)) # similar creates an Array by default similar(a::AbstractArray, ::Type{T}, dims::Dims{N}) where {T,N} = Array{T,N}(undef, dims) @@ -739,6 +845,8 @@ similar(::Type{T}, dims::Dims) where {T<:AbstractArray} = T(undef, dims) Create an empty vector similar to `v`, optionally changing the `eltype`. +See also: [`empty!`](@ref), [`isempty`](@ref), [`isassigned`](@ref). + # Examples ```jldoctest @@ -763,6 +871,7 @@ elements in `dst`. If `dst` and `src` are of the same type, `dst == src` should hold after the call. If `dst` and `src` are multidimensional arrays, they must have equal [`axes`](@ref). + See also [`copyto!`](@ref). !!! compat "Julia 1.1" @@ -787,6 +896,10 @@ end ## from general iterable to any array +# This is `@Experimental.max_methods 1 function copyto! end`, which is not +# defined at this point in bootstrap. +typeof(function copyto! end).name.max_methods = UInt8(1) + function copyto!(dest::AbstractArray, src) destiter = eachindex(dest) y = iterate(destiter) @@ -811,19 +924,21 @@ end # copy from an some iterable object into an AbstractArray function copyto!(dest::AbstractArray, dstart::Integer, src, sstart::Integer) if (sstart < 1) - throw(ArgumentError(string("source start offset (",sstart,") is < 1"))) + throw(ArgumentError(LazyString("source start offset (",sstart,") is < 1"))) end y = iterate(src) for j = 1:(sstart-1) if y === nothing - throw(ArgumentError(string("source has fewer elements than required, ", - "expected at least ",sstart,", got ",j-1))) + throw(ArgumentError(LazyString( + "source has fewer elements than required, ", + "expected at least ", sstart,", got ", j-1))) end y = iterate(src, y[2]) end if y === nothing - throw(ArgumentError(string("source has fewer elements than required, ", - "expected at least ",sstart,", got ",sstart-1))) + throw(ArgumentError(LazyString( + "source has fewer elements than required, ", + "expected at least ",sstart," got ", sstart-1))) end i = Int(dstart) while y !== nothing @@ -837,19 +952,22 @@ end # this method must be separate from the above since src might not have a length function copyto!(dest::AbstractArray, dstart::Integer, src, sstart::Integer, n::Integer) - n < 0 && throw(ArgumentError(string("tried to copy n=", n, " elements, but n should be nonnegative"))) + n < 0 && throw(ArgumentError(LazyString("tried to copy n=",n, + ", elements, but n should be nonnegative"))) n == 0 && return dest dmax = dstart + n - 1 inds = LinearIndices(dest) if (dstart ∉ inds || dmax ∉ inds) | (sstart < 1) - sstart < 1 && throw(ArgumentError(string("source start offset (",sstart,") is < 1"))) + sstart < 1 && throw(ArgumentError(LazyString("source start offset (", + sstart,") is < 1"))) throw(BoundsError(dest, dstart:dmax)) end y = iterate(src) for j = 1:(sstart-1) if y === nothing - throw(ArgumentError(string("source has fewer elements than required, ", - "expected at least ",sstart,", got ",j-1))) + throw(ArgumentError(LazyString( + "source has fewer elements than required, ", + "expected at least ",sstart,", got ",j-1))) end y = iterate(src, y[2]) end @@ -870,11 +988,12 @@ end """ copyto!(dest::AbstractArray, src) -> dest - Copy all elements from collection `src` to array `dest`, whose length must be greater than or equal to the length `n` of `src`. The first `n` elements of `dest` are overwritten, the other elements are left untouched. +See also [`copy!`](@ref Base.copy!), [`copy`](@ref). + # Examples ```jldoctest julia> x = [1., 0., 3., 0., 5.]; @@ -960,7 +1079,8 @@ function copyto!(dest::AbstractArray, dstart::Integer, src::AbstractArray, sstart::Integer, n::Integer) n == 0 && return dest - n < 0 && throw(ArgumentError(string("tried to copy n=", n, " elements, but n should be nonnegative"))) + n < 0 && throw(ArgumentError(LazyString("tried to copy n=", + n," elements, but n should be nonnegative"))) destinds, srcinds = LinearIndices(dest), LinearIndices(src) (checkbounds(Bool, destinds, dstart) && checkbounds(Bool, destinds, dstart+n-1)) || throw(BoundsError(dest, dstart:dstart+n-1)) (checkbounds(Bool, srcinds, sstart) && checkbounds(Bool, srcinds, sstart+n-1)) || throw(BoundsError(src, sstart:sstart+n-1)) @@ -978,12 +1098,12 @@ end function copyto!(B::AbstractVecOrMat{R}, ir_dest::AbstractRange{Int}, jr_dest::AbstractRange{Int}, A::AbstractVecOrMat{S}, ir_src::AbstractRange{Int}, jr_src::AbstractRange{Int}) where {R,S} if length(ir_dest) != length(ir_src) - throw(ArgumentError(string("source and destination must have same size (got ", - length(ir_src)," and ",length(ir_dest),")"))) + throw(ArgumentError(LazyString("source and destination must have same size (got ", + length(ir_src)," and ",length(ir_dest),")"))) end if length(jr_dest) != length(jr_src) - throw(ArgumentError(string("source and destination must have same size (got ", - length(jr_src)," and ",length(jr_dest),")"))) + throw(ArgumentError(LazyString("source and destination must have same size (got ", + length(jr_src)," and ",length(jr_dest),")"))) end @boundscheck checkbounds(B, ir_dest, jr_dest) @boundscheck checkbounds(A, ir_src, jr_src) @@ -1032,7 +1152,7 @@ function copymutable(a::AbstractArray) end copymutable(itr) = collect(itr) -zero(x::AbstractArray{T}) where {T} = fill!(similar(x), zero(T)) +zero(x::AbstractArray{T}) where {T} = fill!(similar(x, typeof(zero(T))), zero(T)) ## iteration support for arrays by iterating over `eachindex` in the array ## # Allows fast iteration by default for both IndexLinear and IndexCartesian arrays @@ -1065,7 +1185,7 @@ end # convenience in cases that work. pointer(x::AbstractArray{T}) where {T} = unsafe_convert(Ptr{T}, x) function pointer(x::AbstractArray{T}, i::Integer) where T - @_inline_meta + @inline unsafe_convert(Ptr{T}, x) + Int(_memory_offset(x, i))::Int end @@ -1088,9 +1208,9 @@ end """ getindex(A, inds...) -Return a subset of array `A` as specified by `inds`, where each `ind` may be an -`Int`, an [`AbstractRange`](@ref), or a [`Vector`](@ref). See the manual section on -[array indexing](@ref man-array-indexing) for details. +Return a subset of array `A` as specified by `inds`, where each `ind` may be, +for example, an `Int`, an [`AbstractRange`](@ref), or a [`Vector`](@ref). +See the manual section on [array indexing](@ref man-array-indexing) for details. # Examples ```jldoctest @@ -1120,18 +1240,24 @@ function getindex(A::AbstractArray, I...) _getindex(IndexStyle(A), A, to_indices(A, I)...) end # To avoid invalidations from multidimensional.jl: getindex(A::Array, i1::Union{Integer, CartesianIndex}, I::Union{Integer, CartesianIndex}...) -getindex(A::Array, i1::Integer, I::Integer...) = A[to_indices(A, (i1, I...))...] +@propagate_inbounds getindex(A::Array, i1::Integer, I::Integer...) = A[to_indices(A, (i1, I...))...] function unsafe_getindex(A::AbstractArray, I...) - @_inline_meta + @inline @inbounds r = getindex(A, I...) r end +struct CanonicalIndexError + func::String + type::Any + CanonicalIndexError(func::String, @nospecialize(type)) = new(func, type) +end + error_if_canonical_getindex(::IndexLinear, A::AbstractArray, ::Int) = - error("getindex not defined for ", typeof(A)) + throw(CanonicalIndexError("getindex", typeof(A))) error_if_canonical_getindex(::IndexCartesian, A::AbstractArray{T,N}, ::Vararg{Int,N}) where {T,N} = - error("getindex not defined for ", typeof(A)) + throw(CanonicalIndexError("getindex", typeof(A))) error_if_canonical_getindex(::IndexStyle, ::AbstractArray, ::Any...) = nothing ## Internal definitions @@ -1142,19 +1268,19 @@ _getindex(::IndexStyle, A::AbstractArray, I...) = _getindex(::IndexLinear, A::AbstractVector, i::Int) = (@_propagate_inbounds_meta; getindex(A, i)) # ambiguity resolution in case packages specialize this (to be avoided if at all possible, but see Interpolations.jl) _getindex(::IndexLinear, A::AbstractArray, i::Int) = (@_propagate_inbounds_meta; getindex(A, i)) function _getindex(::IndexLinear, A::AbstractArray, I::Vararg{Int,M}) where M - @_inline_meta + @inline @boundscheck checkbounds(A, I...) # generally _to_linear_index requires bounds checking @inbounds r = getindex(A, _to_linear_index(A, I...)) r end _to_linear_index(A::AbstractArray, i::Integer) = i _to_linear_index(A::AbstractVector, i::Integer, I::Integer...) = i -_to_linear_index(A::AbstractArray) = 1 -_to_linear_index(A::AbstractArray, I::Integer...) = (@_inline_meta; _sub2ind(A, I...)) +_to_linear_index(A::AbstractArray) = first(LinearIndices(A)) +_to_linear_index(A::AbstractArray, I::Integer...) = (@inline; _sub2ind(A, I...)) ## IndexCartesian Scalar indexing: Canonical method is full dimensionality of Ints function _getindex(::IndexCartesian, A::AbstractArray, I::Vararg{Int,M}) where M - @_inline_meta + @inline @boundscheck checkbounds(A, I...) # generally _to_subscript_indices requires bounds checking @inbounds r = getindex(A, _to_subscript_indices(A, I...)...) r @@ -1163,13 +1289,13 @@ function _getindex(::IndexCartesian, A::AbstractArray{T,N}, I::Vararg{Int, N}) w @_propagate_inbounds_meta getindex(A, I...) end -_to_subscript_indices(A::AbstractArray, i::Integer) = (@_inline_meta; _unsafe_ind2sub(A, i)) -_to_subscript_indices(A::AbstractArray{T,N}) where {T,N} = (@_inline_meta; fill_to_length((), 1, Val(N))) +_to_subscript_indices(A::AbstractArray, i::Integer) = (@inline; _unsafe_ind2sub(A, i)) +_to_subscript_indices(A::AbstractArray{T,N}) where {T,N} = (@inline; fill_to_length((), 1, Val(N))) _to_subscript_indices(A::AbstractArray{T,0}) where {T} = () _to_subscript_indices(A::AbstractArray{T,0}, i::Integer) where {T} = () _to_subscript_indices(A::AbstractArray{T,0}, I::Integer...) where {T} = () function _to_subscript_indices(A::AbstractArray{T,N}, I::Integer...) where {T,N} - @_inline_meta + @inline J, Jrem = IteratorsMD.split(I, Val(N)) _to_subscript_indices(A, J, Jrem) end @@ -1177,15 +1303,15 @@ _to_subscript_indices(A::AbstractArray, J::Tuple, Jrem::Tuple{}) = __to_subscript_indices(A, axes(A), J, Jrem) function __to_subscript_indices(A::AbstractArray, ::Tuple{AbstractUnitRange,Vararg{AbstractUnitRange}}, J::Tuple, Jrem::Tuple{}) - @_inline_meta + @inline (J..., map(first, tail(_remaining_size(J, axes(A))))...) end _to_subscript_indices(A, J::Tuple, Jrem::Tuple) = J # already bounds-checked, safe to drop _to_subscript_indices(A::AbstractArray{T,N}, I::Vararg{Int,N}) where {T,N} = I _remaining_size(::Tuple{Any}, t::Tuple) = t -_remaining_size(h::Tuple, t::Tuple) = (@_inline_meta; _remaining_size(tail(h), tail(t))) +_remaining_size(h::Tuple, t::Tuple) = (@inline; _remaining_size(tail(h), tail(t))) _unsafe_ind2sub(::Tuple{}, i) = () # _ind2sub may throw(BoundsError()) in this case -_unsafe_ind2sub(sz, i) = (@_inline_meta; _ind2sub(sz, i)) +_unsafe_ind2sub(sz, i) = (@inline; _ind2sub(sz, i)) ## Setindex! is defined similarly. We first dispatch to an internal _setindex! # function that allows dispatch on array storage @@ -1195,7 +1321,7 @@ _unsafe_ind2sub(sz, i) = (@_inline_meta; _ind2sub(sz, i)) A[inds...] = X Store values from array `X` within some subset of `A` as specified by `inds`. -The syntax `A[inds...] = X` is equivalent to `setindex!(A, X, inds...)`. +The syntax `A[inds...] = X` is equivalent to `(setindex!(A, X, inds...); X)`. # Examples ```jldoctest @@ -1217,15 +1343,15 @@ function setindex!(A::AbstractArray, v, I...) _setindex!(IndexStyle(A), A, v, to_indices(A, I)...) end function unsafe_setindex!(A::AbstractArray, v, I...) - @_inline_meta + @inline @inbounds r = setindex!(A, v, I...) r end error_if_canonical_setindex(::IndexLinear, A::AbstractArray, ::Int) = - error("setindex! not defined for ", typeof(A)) + throw(CanonicalIndexError("setindex!", typeof(A))) error_if_canonical_setindex(::IndexCartesian, A::AbstractArray{T,N}, ::Vararg{Int,N}) where {T,N} = - error("setindex! not defined for ", typeof(A)) + throw(CanonicalIndexError("setindex!", typeof(A))) error_if_canonical_setindex(::IndexStyle, ::AbstractArray, ::Any...) = nothing ## Internal definitions @@ -1235,7 +1361,7 @@ _setindex!(::IndexStyle, A::AbstractArray, v, I...) = ## IndexLinear Scalar indexing _setindex!(::IndexLinear, A::AbstractArray, v, i::Int) = (@_propagate_inbounds_meta; setindex!(A, v, i)) function _setindex!(::IndexLinear, A::AbstractArray, v, I::Vararg{Int,M}) where M - @_inline_meta + @inline @boundscheck checkbounds(A, I...) @inbounds r = setindex!(A, v, _to_linear_index(A, I...)) r @@ -1247,7 +1373,7 @@ function _setindex!(::IndexCartesian, A::AbstractArray{T,N}, v, I::Vararg{Int, N setindex!(A, v, I...) end function _setindex!(::IndexCartesian, A::AbstractArray, v, I::Vararg{Int,M}) where M - @_inline_meta + @inline @boundscheck checkbounds(A, I...) @inbounds r = setindex!(A, v, _to_subscript_indices(A, I...)...) r @@ -1258,7 +1384,8 @@ end Return the underlying "parent array”. This parent array of objects of types `SubArray`, `ReshapedArray` or `LinearAlgebra.Transpose` is what was passed as an argument to `view`, `reshape`, `transpose`, etc. -during object creation. If the input is not a wrapped object, return the input itself. +during object creation. If the input is not a wrapped object, return the input itself. If the input is +wrapped multiple times, only the outermost wrapper will be removed. # Examples ```jldoctest @@ -1313,7 +1440,7 @@ much more common case where aliasing does not occur. By default, `Base.unaliascopy(A)`. """ unaliascopy(A::Array) = copy(A) -unaliascopy(A::AbstractArray)::typeof(A) = (@_noinline_meta; _unaliascopy(A, copy(A))) +unaliascopy(A::AbstractArray)::typeof(A) = (@noinline; _unaliascopy(A, copy(A))) _unaliascopy(A::T, C::T) where {T} = C _unaliascopy(A, C) = throw(ArgumentError(""" an array of type `$(typename(typeof(A)).wrapper)` shares memory with another argument @@ -1367,6 +1494,9 @@ RangeVecIntList{A<:AbstractVector{Int}} = Union{Tuple{Vararg{Union{AbstractRange get(A::AbstractArray, i::Integer, default) = checkbounds(Bool, A, i) ? A[i] : default get(A::AbstractArray, I::Tuple{}, default) = checkbounds(Bool, A) ? A[] : default get(A::AbstractArray, I::Dims, default) = checkbounds(Bool, A, I...) ? A[I...] : default +get(f::Callable, A::AbstractArray, i::Integer) = checkbounds(Bool, A, i) ? A[i] : f() +get(f::Callable, A::AbstractArray, I::Tuple{}) = checkbounds(Bool, A) ? A[] : f() +get(f::Callable, A::AbstractArray, I::Dims) = checkbounds(Bool, A, I...) ? A[I...] : f() function get!(X::AbstractVector{T}, A::AbstractVector, I::Union{AbstractRange,AbstractVector{Int}}, default::T) where T # 1d is not linear indexing @@ -1424,10 +1554,10 @@ vcat(X::T...) where {T<:Number} = T[ X[i] for i=1:length(X) ] hcat(X::T...) where {T} = T[ X[j] for i=1:1, j=1:length(X) ] hcat(X::T...) where {T<:Number} = T[ X[j] for i=1:1, j=1:length(X) ] -vcat(X::Number...) = hvcat_fill(Vector{promote_typeof(X...)}(undef, length(X)), X) -hcat(X::Number...) = hvcat_fill(Matrix{promote_typeof(X...)}(undef, 1,length(X)), X) -typed_vcat(::Type{T}, X::Number...) where {T} = hvcat_fill(Vector{T}(undef, length(X)), X) -typed_hcat(::Type{T}, X::Number...) where {T} = hvcat_fill(Matrix{T}(undef, 1,length(X)), X) +vcat(X::Number...) = hvcat_fill!(Vector{promote_typeof(X...)}(undef, length(X)), X) +hcat(X::Number...) = hvcat_fill!(Matrix{promote_typeof(X...)}(undef, 1,length(X)), X) +typed_vcat(::Type{T}, X::Number...) where {T} = hvcat_fill!(Vector{T}(undef, length(X)), X) +typed_hcat(::Type{T}, X::Number...) where {T} = hvcat_fill!(Matrix{T}(undef, 1,length(X)), X) vcat(V::AbstractVector...) = typed_vcat(promote_eltype(V...), V...) vcat(V::AbstractVector{T}...) where {T} = typed_vcat(T, V...) @@ -1437,12 +1567,11 @@ vcat(V::AbstractVector{T}...) where {T} = typed_vcat(T, V...) # but that solution currently fails (see #27188 and #27224) AbstractVecOrTuple{T} = Union{AbstractVector{<:T}, Tuple{Vararg{T}}} -function _typed_vcat(::Type{T}, V::AbstractVecOrTuple{AbstractVector}) where T - n = 0 - for Vk in V - n += Int(length(Vk))::Int - end - a = similar(V[1], T, n) +_typed_vcat_similar(V, ::Type{T}, n) where T = similar(V[1], T, n) +_typed_vcat(::Type{T}, V::AbstractVecOrTuple{AbstractVector}) where T = + _typed_vcat!(_typed_vcat_similar(V, T, sum(map(length, V))), V) + +function _typed_vcat!(a::AbstractVector{T}, V::AbstractVecOrTuple{AbstractVector}) where T pos = 1 for k=1:Int(length(V))::Int Vk = V[k] @@ -1531,12 +1660,23 @@ cat_size(A::AbstractArray) = size(A) cat_size(A, d) = 1 cat_size(A::AbstractArray, d) = size(A, d) +cat_length(::Any) = 1 +cat_length(a::AbstractArray) = length(a) + +cat_ndims(a) = 0 +cat_ndims(a::AbstractArray) = ndims(a) + cat_indices(A, d) = OneTo(1) cat_indices(A::AbstractArray, d) = axes(A, d) -cat_similar(A, T, shape) = Array{T}(undef, shape) -cat_similar(A::AbstractArray, T, shape) = similar(A, T, shape) +cat_similar(A, ::Type{T}, shape::Tuple) where T = Array{T}(undef, shape) +cat_similar(A, ::Type{T}, shape::Vector) where T = Array{T}(undef, shape...) +cat_similar(A::Array, ::Type{T}, shape::Tuple) where T = Array{T}(undef, shape) +cat_similar(A::Array, ::Type{T}, shape::Vector) where T = Array{T}(undef, shape...) +cat_similar(A::AbstractArray, T::Type, shape::Tuple) = similar(A, T, shape) +cat_similar(A::AbstractArray, T::Type, shape::Vector) = similar(A, T, shape...) +# These are for backwards compatibility (even though internal) cat_shape(dims, shape::Tuple{Vararg{Int}}) = shape function cat_shape(dims, shapes::Tuple) out_shape = () @@ -1545,10 +1685,15 @@ function cat_shape(dims, shapes::Tuple) end return out_shape end +# The new way to compute the shape (more inferrable than combining cat_size & cat_shape, due to Varargs + issue#36454) +cat_size_shape(dims) = ntuple(zero, Val(length(dims))) +@inline cat_size_shape(dims, X, tail...) = _cat_size_shape(dims, _cshp(1, dims, (), cat_size(X)), tail...) +_cat_size_shape(dims, shape) = shape +@inline _cat_size_shape(dims, shape, X, tail...) = _cat_size_shape(dims, _cshp(1, dims, shape, cat_size(X)), tail...) _cshp(ndim::Int, ::Tuple{}, ::Tuple{}, ::Tuple{}) = () _cshp(ndim::Int, ::Tuple{}, ::Tuple{}, nshape) = nshape -_cshp(ndim::Int, dims, ::Tuple{}, ::Tuple{}) = ntuple(b -> 1, Val(length(dims))) +_cshp(ndim::Int, dims, ::Tuple{}, ::Tuple{}) = ntuple(Returns(1), Val(length(dims))) @inline _cshp(ndim::Int, dims, shape, ::Tuple{}) = (shape[1] + dims[1], _cshp(ndim + 1, tail(dims), tail(shape), ())...) @inline _cshp(ndim::Int, dims, ::Tuple{}, nshape) = @@ -1571,59 +1716,59 @@ end _cs(d, a, b) = (a == b ? a : throw(DimensionMismatch( "mismatch in dimension $d (expected $a got $b)"))) -function dims2cat(::Val{n}) where {n} - n <= 0 && throw(ArgumentError("cat dimension must be a positive integer, but got $n")) - ntuple(i -> (i == n), Val(n)) -end - +dims2cat(::Val{dims}) where dims = dims2cat(dims) function dims2cat(dims) - if any(dims .<= 0) + if any(≤(0), dims) throw(ArgumentError("All cat dimensions must be positive integers, but got $dims")) end ntuple(in(dims), maximum(dims)) end -_cat(dims, X...) = cat_t(promote_eltypeof(X...), X...; dims=dims) +_cat(dims, X...) = _cat_t(dims, promote_eltypeof(X...), X...) -@inline cat_t(::Type{T}, X...; dims) where {T} = _cat_t(dims, T, X...) @inline function _cat_t(dims, ::Type{T}, X...) where {T} catdims = dims2cat(dims) - shape = cat_shape(catdims, map(cat_size, X)::Tuple{Vararg{Union{Int,Dims}}})::Dims + shape = cat_size_shape(catdims, X...) A = cat_similar(X[1], T, shape) if count(!iszero, catdims)::Int > 1 fill!(A, zero(T)) end return __cat(A, shape, catdims, X...) end +# this version of `cat_t` is not very kind for inference and so its usage should be avoided, +# nevertheless it is here just for compat after https://github.com/JuliaLang/julia/pull/45028 +@inline cat_t(::Type{T}, X...; dims) where {T} = _cat_t(dims, T, X...) -function __cat(A, shape::NTuple{M,Int}, catdims, X...) where M - N = M::Int - offsets = zeros(Int, N) - inds = Vector{UnitRange{Int}}(undef, N) - concat = copyto!(zeros(Bool, N), catdims) - for x in X - for i = 1:N - if concat[i] - inds[i] = offsets[i] .+ cat_indices(x, i) - offsets[i] += cat_size(x, i) - else - inds[i] = 1:shape[i] - end - end - I::NTuple{N, UnitRange{Int}} = (inds...,) - if x isa AbstractArray - A[I...] = x - else - fill!(view(A, I...), x) - end +# Why isn't this called `__cat!`? +__cat(A, shape, catdims, X...) = __cat_offset!(A, shape, catdims, ntuple(zero, length(shape)), X...) + +function __cat_offset!(A, shape, catdims, offsets, x, X...) + # splitting the "work" on x from X... may reduce latency (fewer costly specializations) + newoffsets = __cat_offset1!(A, shape, catdims, offsets, x) + return __cat_offset!(A, shape, catdims, newoffsets, X...) +end +__cat_offset!(A, shape, catdims, offsets) = A + +function __cat_offset1!(A, shape, catdims, offsets, x) + inds = ntuple(length(offsets)) do i + (i <= length(catdims) && catdims[i]) ? offsets[i] .+ cat_indices(x, i) : 1:shape[i] end - return A + if x isa AbstractArray + A[inds...] = x + else + fill!(view(A, inds...), x) + end + newoffsets = ntuple(length(offsets)) do i + (i <= length(catdims) && catdims[i]) ? offsets[i] + cat_size(x, i) : offsets[i] + end + return newoffsets end """ vcat(A...) -Concatenate along dimension 1. +Concatenate along dimension 1. To efficiently concatenate a large vector of arrays, +use `reduce(vcat, x)`. # Examples ```jldoctest @@ -1649,13 +1794,29 @@ julia> vcat(c...) 2×3 Matrix{Int64}: 1 2 3 4 5 6 + +julia> vs = [[1, 2], [3, 4], [5, 6]] +3-element Vector{Vector{Int64}}: + [1, 2] + [3, 4] + [5, 6] + +julia> reduce(vcat, vs) +6-element Vector{Int64}: + 1 + 2 + 3 + 4 + 5 + 6 ``` """ vcat(X...) = cat(X...; dims=Val(1)) """ hcat(A...) -Concatenate along dimension 2. +Concatenate along dimension 2. To efficiently concatenate a large vector of arrays, +use `reduce(hcat, x)`. # Examples ```jldoctest @@ -1700,15 +1861,26 @@ julia> hcat(x, [1; 2; 3]) 1 2 3 + +julia> vs = [[1, 2], [3, 4], [5, 6]] +3-element Vector{Vector{Int64}}: + [1, 2] + [3, 4] + [5, 6] + +julia> reduce(hcat, vs) +2×3 Matrix{Int64}: + 1 3 5 + 2 4 6 ``` """ hcat(X...) = cat(X...; dims=Val(2)) -typed_vcat(::Type{T}, X...) where T = cat_t(T, X...; dims=Val(1)) -typed_hcat(::Type{T}, X...) where T = cat_t(T, X...; dims=Val(2)) +typed_vcat(::Type{T}, X...) where T = _cat_t(Val(1), T, X...) +typed_hcat(::Type{T}, X...) where T = _cat_t(Val(2), T, X...) """ - cat(A...; dims=dims) + cat(A...; dims) Concatenate the input arrays along the specified dimensions in the iterable `dims`. For dimensions not in `dims`, all input arrays should have the same size, which will also be the @@ -1721,9 +1893,28 @@ dimensions for every new input array and putting zero blocks elsewhere. For exam `cat(matrices...; dims=(1,2))` builds a block diagonal matrix, i.e. a block matrix with `matrices[1]`, `matrices[2]`, ... as diagonal blocks and matching zero blocks away from the diagonal. + +See also [`hcat`](@ref), [`vcat`](@ref), [`hvcat`](@ref), [`repeat`](@ref). + +# Examples +```jldoctest +julia> cat([1 2; 3 4], [pi, pi], fill(10, 2,3,1); dims=2) +2×6×1 Array{Float64, 3}: +[:, :, 1] = + 1.0 2.0 3.14159 10.0 10.0 10.0 + 3.0 4.0 3.14159 10.0 10.0 10.0 + +julia> cat(true, trues(2,2), trues(4)', dims=(1,2)) +4×7 Matrix{Bool}: + 1 0 0 0 0 0 0 + 0 1 1 0 0 0 0 + 0 1 1 0 0 0 0 + 0 0 0 1 1 1 1 +``` """ @inline cat(A...; dims) = _cat(dims, A...) -_cat(catdims, A::AbstractArray{T}...) where {T} = cat_t(T, A...; dims=catdims) +# `@constprop :aggressive` allows `catdims` to be propagated as constant improving return type inference +@constprop :aggressive _cat(catdims, A::AbstractArray{T}...) where {T} = _cat_t(catdims, T, A...) # The specializations for 1 and 2 inputs are important # especially when running with --inline=no, see #11158 @@ -1734,22 +1925,26 @@ hcat(A::AbstractArray) = cat(A; dims=Val(2)) hcat(A::AbstractArray, B::AbstractArray) = cat(A, B; dims=Val(2)) hcat(A::AbstractArray...) = cat(A...; dims=Val(2)) -typed_vcat(T::Type, A::AbstractArray) = cat_t(T, A; dims=Val(1)) -typed_vcat(T::Type, A::AbstractArray, B::AbstractArray) = cat_t(T, A, B; dims=Val(1)) -typed_vcat(T::Type, A::AbstractArray...) = cat_t(T, A...; dims=Val(1)) -typed_hcat(T::Type, A::AbstractArray) = cat_t(T, A; dims=Val(2)) -typed_hcat(T::Type, A::AbstractArray, B::AbstractArray) = cat_t(T, A, B; dims=Val(2)) -typed_hcat(T::Type, A::AbstractArray...) = cat_t(T, A...; dims=Val(2)) +typed_vcat(T::Type, A::AbstractArray) = _cat_t(Val(1), T, A) +typed_vcat(T::Type, A::AbstractArray, B::AbstractArray) = _cat_t(Val(1), T, A, B) +typed_vcat(T::Type, A::AbstractArray...) = _cat_t(Val(1), T, A...) +typed_hcat(T::Type, A::AbstractArray) = _cat_t(Val(2), T, A) +typed_hcat(T::Type, A::AbstractArray, B::AbstractArray) = _cat_t(Val(2), T, A, B) +typed_hcat(T::Type, A::AbstractArray...) = _cat_t(Val(2), T, A...) # 2d horizontal and vertical concatenation +# these are produced in lowering if splatting occurs inside hvcat +hvcat_rows(rows::Tuple...) = hvcat(map(length, rows), (rows...)...) +typed_hvcat_rows(T::Type, rows::Tuple...) = typed_hvcat(T, map(length, rows), (rows...)...) + function hvcat(nbc::Integer, as...) # nbc = # of block columns n = length(as) mod(n,nbc) != 0 && throw(ArgumentError("number of arrays $n is not a multiple of the requested number of block columns $nbc")) nbr = div(n,nbc) - hvcat(ntuple(i->nbc, nbr), as...) + hvcat(ntuple(Returns(nbc), nbr), as...) end """ @@ -1774,7 +1969,7 @@ julia> hvcat((3,3), a,b,c,d,e,f) 1 2 3 4 5 6 -julia> [a b;c d; e f] +julia> [a b; c d; e f] 3×2 Matrix{Int64}: 1 2 3 4 @@ -1860,9 +2055,13 @@ function hvcat(rows::Tuple{Vararg{Int}}, xs::T...) where T<:Number a end -function hvcat_fill(a::Array, xs::Tuple) - k = 1 +function hvcat_fill!(a::Array, xs::Tuple) nr, nc = size(a,1), size(a,2) + len = length(xs) + if nr*nc != len + throw(ArgumentError("argument count $(len) does not match specified shape $((nr,nc))")) + end + k = 1 for i=1:nr @inbounds for j=1:nc a[i,j] = xs[k] @@ -1883,11 +2082,7 @@ function typed_hvcat(::Type{T}, rows::Tuple{Vararg{Int}}, xs::Number...) where T throw(ArgumentError("row $(i) has mismatched number of columns (expected $nc, got $(rows[i]))")) end end - len = length(xs) - if nr*nc != len - throw(ArgumentError("argument count $(len) does not match specified shape $((nr,nc))")) - end - hvcat_fill(Matrix{T}(undef, nr, nc), xs) + hvcat_fill!(Matrix{T}(undef, nr, nc), xs) end function typed_hvcat(::Type{T}, rows::Tuple{Vararg{Int}}, as...) where T @@ -1901,6 +2096,486 @@ function typed_hvcat(::Type{T}, rows::Tuple{Vararg{Int}}, as...) where T T[rs...;] end +## N-dimensional concatenation ## + +""" + hvncat(dim::Int, row_first, values...) + hvncat(dims::Tuple{Vararg{Int}}, row_first, values...) + hvncat(shape::Tuple{Vararg{Tuple}}, row_first, values...) + +Horizontal, vertical, and n-dimensional concatenation of many `values` in one call. + +This function is called for block matrix syntax. The first argument either specifies the +shape of the concatenation, similar to `hvcat`, as a tuple of tuples, or the dimensions that +specify the key number of elements along each axis, and is used to determine the output +dimensions. The `dims` form is more performant, and is used by default when the concatenation +operation has the same number of elements along each axis (e.g., [a b; c d;;; e f ; g h]). +The `shape` form is used when the number of elements along each axis is unbalanced +(e.g., [a b ; c]). Unbalanced syntax needs additional validation overhead. The `dim` form +is an optimization for concatenation along just one dimension. `row_first` indicates how +`values` are ordered. The meaning of the first and second elements of `shape` are also +swapped based on `row_first`. + +# Examples +```jldoctest +julia> a, b, c, d, e, f = 1, 2, 3, 4, 5, 6 +(1, 2, 3, 4, 5, 6) + +julia> [a b c;;; d e f] +1×3×2 Array{Int64, 3}: +[:, :, 1] = + 1 2 3 + +[:, :, 2] = + 4 5 6 + +julia> hvncat((2,1,3), false, a,b,c,d,e,f) +2×1×3 Array{Int64, 3}: +[:, :, 1] = + 1 + 2 + +[:, :, 2] = + 3 + 4 + +[:, :, 3] = + 5 + 6 + +julia> [a b;;; c d;;; e f] +1×2×3 Array{Int64, 3}: +[:, :, 1] = + 1 2 + +[:, :, 2] = + 3 4 + +[:, :, 3] = + 5 6 + +julia> hvncat(((3, 3), (3, 3), (6,)), true, a, b, c, d, e, f) +1×3×2 Array{Int64, 3}: +[:, :, 1] = + 1 2 3 + +[:, :, 2] = + 4 5 6 +``` + + +# Examples for construction of the arguments: +```julia +[a b c ; d e f ;;; + g h i ; j k l ;;; + m n o ; p q r ;;; + s t u ; v w x] +=> dims = (2, 3, 4) + +[a b ; c ;;; d ;;;;] + ___ _ _ + 2 1 1 = elements in each row (2, 1, 1) + _______ _ + 3 1 = elements in each column (3, 1) + _____________ + 4 = elements in each 3d slice (4,) + _____________ + 4 = elements in each 4d slice (4,) + => shape = ((2, 1, 1), (3, 1), (4,), (4,)) with `rowfirst` = true +``` +""" +hvncat(dimsshape::Tuple, row_first::Bool, xs...) = _hvncat(dimsshape, row_first, xs...) +hvncat(dim::Int, xs...) = _hvncat(dim, true, xs...) + +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool) = _typed_hvncat(Any, dimsshape, row_first) +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs...) = _typed_hvncat(promote_eltypeof(xs...), dimsshape, row_first, xs...) +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::T...) where T<:Number = _typed_hvncat(T, dimsshape, row_first, xs...) +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::Number...) = _typed_hvncat(promote_typeof(xs...), dimsshape, row_first, xs...) +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::AbstractArray...) = _typed_hvncat(promote_eltype(xs...), dimsshape, row_first, xs...) +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::AbstractArray{T}...) where T = _typed_hvncat(T, dimsshape, row_first, xs...) + + +typed_hvncat(T::Type, dimsshape::Tuple, row_first::Bool, xs...) = _typed_hvncat(T, dimsshape, row_first, xs...) +typed_hvncat(T::Type, dim::Int, xs...) = _typed_hvncat(T, Val(dim), xs...) + +# 1-dimensional hvncat methods + +_typed_hvncat(::Type, ::Val{0}) = _typed_hvncat_0d_only_one() +_typed_hvncat(T::Type, ::Val{0}, x) = fill(convert(T, x)) +_typed_hvncat(T::Type, ::Val{0}, x::Number) = fill(convert(T, x)) +_typed_hvncat(T::Type, ::Val{0}, x::AbstractArray) = convert.(T, x) +_typed_hvncat(::Type, ::Val{0}, ::Any...) = _typed_hvncat_0d_only_one() +_typed_hvncat(::Type, ::Val{0}, ::Number...) = _typed_hvncat_0d_only_one() +_typed_hvncat(::Type, ::Val{0}, ::AbstractArray...) = _typed_hvncat_0d_only_one() + +_typed_hvncat_0d_only_one() = + throw(ArgumentError("a 0-dimensional array may only contain exactly one element")) + +_typed_hvncat(T::Type, dim::Int, ::Bool, xs...) = _typed_hvncat(T, Val(dim), xs...) # catches from _hvncat type promoters + +function _typed_hvncat(::Type{T}, ::Val{N}) where {T, N} + N < 0 && + throw(ArgumentError("concatenation dimension must be nonnegative")) + return Array{T, N}(undef, ntuple(x -> 0, Val(N))) +end + +function _typed_hvncat(T::Type, ::Val{N}, xs::Number...) where N + N < 0 && + throw(ArgumentError("concatenation dimension must be nonnegative")) + A = cat_similar(xs[1], T, (ntuple(x -> 1, Val(N - 1))..., length(xs))) + hvncat_fill!(A, false, xs) + return A +end + +function _typed_hvncat(::Type{T}, ::Val{N}, as::AbstractArray...) where {T, N} + # optimization for arrays that can be concatenated by copying them linearly into the destination + # conditions: the elements must all have 1-length dimensions above N + length(as) > 0 || + throw(ArgumentError("must have at least one element")) + N < 0 && + throw(ArgumentError("concatenation dimension must be nonnegative")) + for a ∈ as + ndims(a) <= N || all(x -> size(a, x) == 1, (N + 1):ndims(a)) || + return _typed_hvncat(T, (ntuple(x -> 1, Val(N - 1))..., length(as), 1), false, as...) + # the extra 1 is to avoid an infinite cycle + end + + nd = N + + Ndim = 0 + for i ∈ eachindex(as) + Ndim += cat_size(as[i], N) + nd = max(nd, cat_ndims(as[i])) + for d ∈ 1:N - 1 + cat_size(as[1], d) == cat_size(as[i], d) || throw(ArgumentError("mismatched size along axis $d in element $i")) + end + end + + A = cat_similar(as[1], T, (ntuple(d -> size(as[1], d), N - 1)..., Ndim, ntuple(x -> 1, nd - N)...)) + k = 1 + for a ∈ as + for i ∈ eachindex(a) + A[k] = a[i] + k += 1 + end + end + return A +end + +function _typed_hvncat(::Type{T}, ::Val{N}, as...) where {T, N} + length(as) > 0 || + throw(ArgumentError("must have at least one element")) + N < 0 && + throw(ArgumentError("concatenation dimension must be nonnegative")) + nd = N + Ndim = 0 + for i ∈ eachindex(as) + Ndim += cat_size(as[i], N) + nd = max(nd, cat_ndims(as[i])) + for d ∈ 1:N-1 + cat_size(as[i], d) == 1 || + throw(ArgumentError("all dimensions of element $i other than $N must be of length 1")) + end + end + + A = Array{T, nd}(undef, ntuple(x -> 1, Val(N - 1))..., Ndim, ntuple(x -> 1, nd - N)...) + + k = 1 + for a ∈ as + if a isa AbstractArray + lena = length(a) + copyto!(A, k, a, 1, lena) + k += lena + else + A[k] = a + k += 1 + end + end + return A +end + +# 0-dimensional cases for balanced and unbalanced hvncat method + +_typed_hvncat(T::Type, ::Tuple{}, ::Bool, x...) = _typed_hvncat(T, Val(0), x...) +_typed_hvncat(T::Type, ::Tuple{}, ::Bool, x::Number...) = _typed_hvncat(T, Val(0), x...) + + +# balanced dimensions hvncat methods + +_typed_hvncat(T::Type, dims::Tuple{Int}, ::Bool, as...) = _typed_hvncat_1d(T, dims[1], Val(false), as...) +_typed_hvncat(T::Type, dims::Tuple{Int}, ::Bool, as::Number...) = _typed_hvncat_1d(T, dims[1], Val(false), as...) + +function _typed_hvncat_1d(::Type{T}, ds::Int, ::Val{row_first}, as...) where {T, row_first} + lengthas = length(as) + ds > 0 || + throw(ArgumentError("`dimsshape` argument must consist of positive integers")) + lengthas == ds || + throw(ArgumentError("number of elements does not match `dimshape` argument; expected $ds, got $lengthas")) + if row_first + return _typed_hvncat(T, Val(2), as...) + else + return _typed_hvncat(T, Val(1), as...) + end +end + +function _typed_hvncat(::Type{T}, dims::NTuple{N, Int}, row_first::Bool, xs::Number...) where {T, N} + all(>(0), dims) || + throw(ArgumentError("`dims` argument must contain positive integers")) + A = Array{T, N}(undef, dims...) + lengtha = length(A) # Necessary to store result because throw blocks are being deoptimized right now, which leads to excessive allocations + lengthx = length(xs) # Cuts from 3 allocations to 1. + if lengtha != lengthx + throw(ArgumentError("argument count does not match specified shape (expected $lengtha, got $lengthx)")) + end + hvncat_fill!(A, row_first, xs) + return A +end + +function hvncat_fill!(A::Array, row_first::Bool, xs::Tuple) + # putting these in separate functions leads to unnecessary allocations + if row_first + nr, nc = size(A, 1), size(A, 2) + nrc = nr * nc + na = prod(size(A)[3:end]) + k = 1 + for d ∈ 1:na + dd = nrc * (d - 1) + for i ∈ 1:nr + Ai = dd + i + for j ∈ 1:nc + A[Ai] = xs[k] + k += 1 + Ai += nr + end + end + end + else + for k ∈ eachindex(xs) + A[k] = xs[k] + end + end +end + +function _typed_hvncat(T::Type, dims::NTuple{N, Int}, row_first::Bool, as...) where {N} + # function barrier after calculating the max is necessary for high performance + nd = max(maximum(cat_ndims(a) for a ∈ as), N) + return _typed_hvncat_dims(T, (dims..., ntuple(x -> 1, nd - N)...), row_first, as) +end + +function _typed_hvncat_dims(::Type{T}, dims::NTuple{N, Int}, row_first::Bool, as::Tuple) where {T, N} + length(as) > 0 || + throw(ArgumentError("must have at least one element")) + all(>(0), dims) || + throw(ArgumentError("`dims` argument must contain positive integers")) + + d1 = row_first ? 2 : 1 + d2 = row_first ? 1 : 2 + + outdims = zeros(Int, N) + + # validate shapes for lowest level of concatenation + d = findfirst(>(1), dims) + if d !== nothing # all dims are 1 + nblocks = length(as) ÷ dims[d] + for b ∈ 1:nblocks + offset = ((b - 1) * dims[d]) + startelementi = offset + 1 + for i ∈ offset .+ (2:dims[d]) + for dd ∈ 1:N + dd == d && continue + if size(as[startelementi], dd) != size(as[i], dd) + throw(ArgumentError("incompatible shape in element $i")) + end + end + end + end + end + + # discover number of rows or columns + for i ∈ 1:dims[d1] + outdims[d1] += cat_size(as[i], d1) + end + + currentdims = zeros(Int, N) + blockcount = 0 + elementcount = 0 + for i ∈ eachindex(as) + elementcount += cat_length(as[i]) + currentdims[d1] += cat_size(as[i], d1) + if currentdims[d1] == outdims[d1] + currentdims[d1] = 0 + for d ∈ (d2, 3:N...) + currentdims[d] += cat_size(as[i], d) + if outdims[d] == 0 # unfixed dimension + blockcount += 1 + if blockcount == dims[d] + outdims[d] = currentdims[d] + currentdims[d] = 0 + blockcount = 0 + else + break + end + else # fixed dimension + if currentdims[d] == outdims[d] # end of dimension + currentdims[d] = 0 + elseif currentdims[d] < outdims[d] # dimension in progress + break + else # exceeded dimension + throw(ArgumentError("argument $i has too many elements along axis $d")) + end + end + end + elseif currentdims[d1] > outdims[d1] # exceeded dimension + throw(ArgumentError("argument $i has too many elements along axis $d1")) + end + end + + outlen = prod(outdims) + elementcount == outlen || + throw(ArgumentError("mismatched number of elements; expected $(outlen), got $(elementcount)")) + + # copy into final array + A = cat_similar(as[1], T, outdims) + # @assert all(==(0), currentdims) + outdims .= 0 + hvncat_fill!(A, currentdims, outdims, d1, d2, as) + return A +end + + +# unbalanced dimensions hvncat methods + +function _typed_hvncat(T::Type, shape::Tuple{Tuple}, row_first::Bool, xs...) + length(shape[1]) > 0 || + throw(ArgumentError("each level of `shape` argument must have at least one value")) + return _typed_hvncat_1d(T, shape[1][1], Val(row_first), xs...) +end + +function _typed_hvncat(T::Type, shape::NTuple{N, Tuple}, row_first::Bool, as...) where {N} + # function barrier after calculating the max is necessary for high performance + nd = max(maximum(cat_ndims(a) for a ∈ as), N) + return _typed_hvncat_shape(T, (shape..., ntuple(x -> shape[end], nd - N)...), row_first, as) +end + +function _typed_hvncat_shape(::Type{T}, shape::NTuple{N, Tuple}, row_first, as::Tuple) where {T, N} + length(as) > 0 || + throw(ArgumentError("must have at least one element")) + all(>(0), tuple((shape...)...)) || + throw(ArgumentError("`shape` argument must consist of positive integers")) + + d1 = row_first ? 2 : 1 + d2 = row_first ? 1 : 2 + + shapev = collect(shape) # saves allocations later + all(!isempty, shapev) || + throw(ArgumentError("each level of `shape` argument must have at least one value")) + length(shapev[end]) == 1 || + throw(ArgumentError("last level of shape must contain only one integer")) + shapelength = shapev[end][1] + lengthas = length(as) + shapelength == lengthas || throw(ArgumentError("number of elements does not match shape; expected $(shapelength), got $lengthas)")) + # discover dimensions + nd = max(N, cat_ndims(as[1])) + outdims = zeros(Int, nd) + currentdims = zeros(Int, nd) + blockcounts = zeros(Int, nd) + shapepos = ones(Int, nd) + + elementcount = 0 + for i ∈ eachindex(as) + elementcount += cat_length(as[i]) + wasstartblock = false + for d ∈ 1:N + ad = (d < 3 && row_first) ? (d == 1 ? 2 : 1) : d + dsize = cat_size(as[i], ad) + blockcounts[d] += 1 + + if d == 1 || i == 1 || wasstartblock + currentdims[d] += dsize + elseif dsize != cat_size(as[i - 1], ad) + throw(ArgumentError("argument $i has a mismatched number of elements along axis $ad; \ + expected $(cat_size(as[i - 1], ad)), got $dsize")) + end + + wasstartblock = blockcounts[d] == 1 # remember for next dimension + + isendblock = blockcounts[d] == shapev[d][shapepos[d]] + if isendblock + if outdims[d] == 0 + outdims[d] = currentdims[d] + elseif outdims[d] != currentdims[d] + throw(ArgumentError("argument $i has a mismatched number of elements along axis $ad; \ + expected $(abs(outdims[d] - (currentdims[d] - dsize))), got $dsize")) + end + currentdims[d] = 0 + blockcounts[d] = 0 + shapepos[d] += 1 + d > 1 && (blockcounts[d - 1] == 0 || + throw(ArgumentError("shape in level $d is inconsistent; level counts must nest \ + evenly into each other"))) + end + end + end + + outlen = prod(outdims) + elementcount == outlen || + throw(ArgumentError("mismatched number of elements; expected $(outlen), got $(elementcount)")) + + if row_first + outdims[1], outdims[2] = outdims[2], outdims[1] + end + + # @assert all(==(0), currentdims) + # @assert all(==(0), blockcounts) + + # copy into final array + A = cat_similar(as[1], T, outdims) + hvncat_fill!(A, currentdims, blockcounts, d1, d2, as) + return A +end + +function hvncat_fill!(A::AbstractArray{T, N}, scratch1::Vector{Int}, scratch2::Vector{Int}, d1::Int, d2::Int, as::Tuple{Vararg}) where {T, N} + outdims = size(A) + offsets = scratch1 + inneroffsets = scratch2 + for a ∈ as + if isa(a, AbstractArray) + for ai ∈ a + Ai = hvncat_calcindex(offsets, inneroffsets, outdims, N) + A[Ai] = ai + + for j ∈ 1:N + inneroffsets[j] += 1 + inneroffsets[j] < cat_size(a, j) && break + inneroffsets[j] = 0 + end + end + else + Ai = hvncat_calcindex(offsets, inneroffsets, outdims, N) + A[Ai] = a + end + + for j ∈ (d1, d2, 3:N...) + offsets[j] += cat_size(a, j) + offsets[j] < outdims[j] && break + offsets[j] = 0 + end + end +end + +@propagate_inbounds function hvncat_calcindex(offsets::Vector{Int}, inneroffsets::Vector{Int}, + outdims::Tuple{Vararg{Int}}, nd::Int) + Ai = inneroffsets[1] + offsets[1] + 1 + for j ∈ 2:nd + increment = inneroffsets[j] + offsets[j] + for k ∈ 1:j-1 + increment *= outdims[k] + end + Ai += increment + end + Ai +end + ## Reductions and accumulates ## function isequal(A::AbstractArray, B::AbstractArray) @@ -1925,6 +2600,11 @@ function cmp(A::AbstractVector, B::AbstractVector) return cmp(length(A), length(B)) end +""" + isless(A::AbstractVector, B::AbstractVector) + +Returns true when `A` is less than `B` in lexicographic order. +""" isless(A::AbstractVector, B::AbstractVector) = cmp(A, B) < 0 function (==)(A::AbstractArray, B::AbstractArray) @@ -1946,12 +2626,12 @@ end # _sub2ind and _ind2sub # fallbacks function _sub2ind(A::AbstractArray, I...) - @_inline_meta + @inline _sub2ind(axes(A), I...) end function _ind2sub(A::AbstractArray, ind) - @_inline_meta + @inline _ind2sub(axes(A), ind) end @@ -1959,49 +2639,49 @@ end _sub2ind(::Tuple{}) = 1 _sub2ind(::DimsInteger) = 1 _sub2ind(::Indices) = 1 -_sub2ind(::Tuple{}, I::Integer...) = (@_inline_meta; _sub2ind_recurse((), 1, 1, I...)) +_sub2ind(::Tuple{}, I::Integer...) = (@inline; _sub2ind_recurse((), 1, 1, I...)) # Generic cases -_sub2ind(dims::DimsInteger, I::Integer...) = (@_inline_meta; _sub2ind_recurse(dims, 1, 1, I...)) -_sub2ind(inds::Indices, I::Integer...) = (@_inline_meta; _sub2ind_recurse(inds, 1, 1, I...)) +_sub2ind(dims::DimsInteger, I::Integer...) = (@inline; _sub2ind_recurse(dims, 1, 1, I...)) +_sub2ind(inds::Indices, I::Integer...) = (@inline; _sub2ind_recurse(inds, 1, 1, I...)) # In 1d, there's a question of whether we're doing cartesian indexing # or linear indexing. Support only the former. _sub2ind(inds::Indices{1}, I::Integer...) = throw(ArgumentError("Linear indexing is not defined for one-dimensional arrays")) -_sub2ind(inds::Tuple{OneTo}, I::Integer...) = (@_inline_meta; _sub2ind_recurse(inds, 1, 1, I...)) # only OneTo is safe +_sub2ind(inds::Tuple{OneTo}, I::Integer...) = (@inline; _sub2ind_recurse(inds, 1, 1, I...)) # only OneTo is safe _sub2ind(inds::Tuple{OneTo}, i::Integer) = i _sub2ind_recurse(::Any, L, ind) = ind function _sub2ind_recurse(::Tuple{}, L, ind, i::Integer, I::Integer...) - @_inline_meta + @inline _sub2ind_recurse((), L, ind+(i-1)*L, I...) end function _sub2ind_recurse(inds, L, ind, i::Integer, I::Integer...) - @_inline_meta + @inline r1 = inds[1] _sub2ind_recurse(tail(inds), nextL(L, r1), ind+offsetin(i, r1)*L, I...) end nextL(L, l::Integer) = L*l -nextL(L, r::AbstractUnitRange) = L*unsafe_length(r) -nextL(L, r::Slice) = L*unsafe_length(r.indices) +nextL(L, r::AbstractUnitRange) = L*length(r) +nextL(L, r::Slice) = L*length(r.indices) offsetin(i, l::Integer) = i-1 offsetin(i, r::AbstractUnitRange) = i-first(r) -_ind2sub(::Tuple{}, ind::Integer) = (@_inline_meta; ind == 1 ? () : throw(BoundsError())) -_ind2sub(dims::DimsInteger, ind::Integer) = (@_inline_meta; _ind2sub_recurse(dims, ind-1)) -_ind2sub(inds::Indices, ind::Integer) = (@_inline_meta; _ind2sub_recurse(inds, ind-1)) +_ind2sub(::Tuple{}, ind::Integer) = (@inline; ind == 1 ? () : throw(BoundsError())) +_ind2sub(dims::DimsInteger, ind::Integer) = (@inline; _ind2sub_recurse(dims, ind-1)) +_ind2sub(inds::Indices, ind::Integer) = (@inline; _ind2sub_recurse(inds, ind-1)) _ind2sub(inds::Indices{1}, ind::Integer) = throw(ArgumentError("Linear indexing is not defined for one-dimensional arrays")) _ind2sub(inds::Tuple{OneTo}, ind::Integer) = (ind,) _ind2sub_recurse(::Tuple{}, ind) = (ind+1,) function _ind2sub_recurse(indslast::NTuple{1}, ind) - @_inline_meta + @inline (_lookup(ind, indslast[1]),) end function _ind2sub_recurse(inds, ind) - @_inline_meta + @inline r1 = inds[1] indnext, f, l = _div(ind, r1) (ind-l*indnext+f, _ind2sub_recurse(tail(inds), indnext)...) @@ -2010,7 +2690,7 @@ end _lookup(ind, d::Integer) = ind+1 _lookup(ind, r::AbstractUnitRange) = ind+first(r) _div(ind, d::Integer) = div(ind, d), 1, d -_div(ind, r::AbstractUnitRange) = (d = unsafe_length(r); (div(ind, d), first(r), d)) +_div(ind, r::AbstractUnitRange) = (d = length(r); (div(ind, d), first(r), d)) # Vectorized forms function _sub2ind(inds::Indices{1}, I1::AbstractVector{T}, I::AbstractVector{T}...) where T<:Integer @@ -2032,7 +2712,7 @@ function _sub2ind_vecs(inds, I::AbstractVector...) end function _sub2ind!(Iout, inds, Iinds, I) - @_noinline_meta + @noinline for i in Iinds # Iout[i] = _sub2ind(inds, map(Ij -> Ij[i], I)...) Iout[i] = sub2ind_vec(inds, i, I) @@ -2040,8 +2720,8 @@ function _sub2ind!(Iout, inds, Iinds, I) Iout end -sub2ind_vec(inds, i, I) = (@_inline_meta; _sub2ind(inds, _sub2ind_vec(i, I...)...)) -_sub2ind_vec(i, I1, I...) = (@_inline_meta; (I1[i], _sub2ind_vec(i, I...)...)) +sub2ind_vec(inds, i, I) = (@inline; _sub2ind(inds, _sub2ind_vec(i, I...)...)) +_sub2ind_vec(i, I1, I...) = (@inline; (I1[i], _sub2ind_vec(i, I...)...)) _sub2ind_vec(i) = () function _ind2sub(inds::Union{DimsInteger{N},Indices{N}}, ind::AbstractVector{<:Integer}) where N @@ -2062,18 +2742,28 @@ end foreach(f, c...) -> Nothing Call function `f` on each element of iterable `c`. -For multiple iterable arguments, `f` is called elementwise. -`foreach` should be used instead of `map` when the results of `f` are not +For multiple iterable arguments, `f` is called elementwise, and iteration stops when +any iterator is finished. + +`foreach` should be used instead of [`map`](@ref) when the results of `f` are not needed, for example in `foreach(println, array)`. # Examples ```jldoctest -julia> a = 1:3:7; +julia> tri = 1:3:7; res = Int[]; -julia> foreach(x -> println(x^2), a) -1 -16 -49 +julia> foreach(x -> push!(res, x^2), tri) + +julia> res +3-element Vector{$(Int)}: + 1 + 16 + 49 + +julia> foreach((x, y) -> println(x, " with ", y), tri, 'a':'z') +1 with a +4 with b +7 with c ``` """ foreach(f) = (f(); nothing) @@ -2094,6 +2784,8 @@ colons go in this expression. The results are concatenated along the remaining d For example, if `dims` is `[1,2]` and `A` is 4-dimensional, `f` is called on `A[:,:,i,j]` for all `i` and `j`. +See also [`eachcol`](@ref), [`eachslice`](@ref). + # Examples ```jldoctest julia> a = reshape(Vector(1:16),(2,2,2,2)) @@ -2172,9 +2864,9 @@ function mapslices(f, A::AbstractArray; dims) end nextra = max(0, length(dims)-ndims(r1)) if eltype(Rsize) == Int - Rsize[dims] = [size(r1)..., ntuple(d->1, nextra)...] + Rsize[dims] = [size(r1)..., ntuple(Returns(1), nextra)...] else - Rsize[dims] = [axes(r1)..., ntuple(d->OneTo(1), nextra)...] + Rsize[dims] = [axes(r1)..., ntuple(Returns(OneTo(1)), nextra)...] end R = similar(r1, tuple(Rsize...,)) @@ -2219,6 +2911,10 @@ end concatenate_setindex!(R, v, I...) = (R[I...] .= (v,); R) concatenate_setindex!(R, X::AbstractArray, I...) = (R[I...] = X) +## 0 arguments + +map(f) = f() + ## 1 argument function map!(f::F, dest::AbstractArray, A::AbstractArray) where F @@ -2235,14 +2931,13 @@ map(f, A::AbstractArray) = collect_similar(A, Generator(f,A)) mapany(f, A::AbstractArray) = map!(f, Vector{Any}(undef, length(A)), A) mapany(f, itr) = Any[f(x) for x in itr] -# default to returning an Array for `map` on general iterators """ map(f, c...) -> collection Transform collection `c` by applying `f` to each element. For multiple collection arguments, -apply `f` elementwise. +apply `f` elementwise, and stop when when any of them is exhausted. -See also: [`mapslices`](@ref) +See also [`map!`](@ref), [`foreach`](@ref), [`mapreduce`](@ref), [`mapslices`](@ref), [`zip`](@ref), [`Iterators.map`](@ref). # Examples ```jldoctest @@ -2252,14 +2947,14 @@ julia> map(x -> x * 2, [1, 2, 3]) 4 6 -julia> map(+, [1, 2, 3], [10, 20, 30]) +julia> map(+, [1, 2, 3], [10, 20, 30, 400, 5000]) 3-element Vector{Int64}: 11 22 33 ``` """ -map(f, A) = collect(Generator(f,A)) +map(f, A) = collect(Generator(f,A)) # default to returning an Array for `map` on general iterators map(f, ::AbstractDict) = error("map is not defined on dictionaries") map(f, ::AbstractSet) = error("map is not defined on sets") @@ -2297,7 +2992,9 @@ end map!(function, destination, collection...) Like [`map`](@ref), but stores the result in `destination` rather than a new -collection. `destination` must be at least as large as the first collection. +collection. `destination` must be at least as large as the smallest collection. + +See also: [`map`](@ref), [`foreach`](@ref), [`zip`](@ref), [`copyto!`](@ref). # Examples ```jldoctest @@ -2310,6 +3007,14 @@ julia> a 2.0 4.0 6.0 + +julia> map!(+, zeros(Int, 5), 100:999, 1:3) +5-element Vector{$(Int)}: + 101 + 103 + 105 + 0 + 0 ``` """ function map!(f::F, dest::AbstractArray, As::AbstractArray...) where {F} @@ -2318,7 +3023,31 @@ function map!(f::F, dest::AbstractArray, As::AbstractArray...) where {F} map_n!(f, dest, As) end -map(f) = f() +""" + map(f, A::AbstractArray...) -> N-array + +When acting on multi-dimensional arrays of the same [`ndims`](@ref), +they must all have the same [`axes`](@ref), and the answer will too. + +See also [`broadcast`](@ref), which allows mismatched sizes. + +# Examples +``` +julia> map(//, [1 2; 3 4], [4 3; 2 1]) +2×2 Matrix{Rational{$Int}}: + 1//4 2//3 + 3//2 4//1 + +julia> map(+, [1 2; 3 4], zeros(2,1)) +ERROR: DimensionMismatch + +julia> map(+, [1 2; 3 4], [1,10,100,1000], zeros(3,1)) # iterates until 3rd is exhausted +3-element Vector{Float64}: + 2.0 + 13.0 + 102.0 +``` +""" map(f, iters...) = collect(Generator(f, iters...)) # multi-item push!, pushfirst! (built on top of type-specific 1-item version) @@ -2330,13 +3059,21 @@ pushfirst!(A, a, b, c...) = pushfirst!(pushfirst!(A, c...), a, b) ## hashing AbstractArray ## +const hash_abstractarray_seed = UInt === UInt64 ? 0x7e2d6fb6448beb77 : 0xd4514ce5 function hash(A::AbstractArray, h::UInt) - h = hash(AbstractArray, h) + h += hash_abstractarray_seed # Axes are themselves AbstractArrays, so hashing them directly would stack overflow # Instead hash the tuple of firsts and lasts along each dimension h = hash(map(first, axes(A)), h) h = hash(map(last, axes(A)), h) - isempty(A) && return h + + # For short arrays, it's not worth doing anything complicated + if length(A) < 8192 + for x in A + h = hash(x, h) + end + return h + end # Goal: Hash approximately log(N) entries with a higher density of hashed elements # weighted towards the end and special consideration for repeated values. Colliding @@ -2367,7 +3104,7 @@ function hash(A::AbstractArray, h::UInt) n = 0 while true n += 1 - # Hash the current key-index and its element + # Hash the element elt = A[keyidx] h = hash(keyidx=>elt, h) @@ -2394,3 +3131,60 @@ function hash(A::AbstractArray, h::UInt) return h end + +# The semantics of `collect` are weird. Better to write our own +function rest(a::AbstractArray{T}, state...) where {T} + v = Vector{T}(undef, 0) + # assume only very few items are taken from the front + sizehint!(v, length(a)) + return foldl(push!, Iterators.rest(a, state...), init=v) +end + + +## keepat! ## + +# NOTE: since these use `@inbounds`, they are actually only intended for Vector and BitVector + +function _keepat!(a::AbstractVector, inds) + local prev + i = firstindex(a) + for k in inds + if @isdefined(prev) + prev < k || throw(ArgumentError("indices must be unique and sorted")) + end + ak = a[k] # must happen even when i==k for bounds checking + if i != k + @inbounds a[i] = ak # k > i, so a[i] is inbounds + end + prev = k + i = nextind(a, i) + end + deleteat!(a, i:lastindex(a)) + return a +end + +function _keepat!(a::AbstractVector, m::AbstractVector{Bool}) + length(m) == length(a) || throw(BoundsError(a, m)) + j = firstindex(a) + for i in eachindex(a, m) + @inbounds begin + if m[i] + i == j || (a[j] = a[i]) + j = nextind(a, j) + end + end + end + deleteat!(a, j:lastindex(a)) +end + +## 1-d circshift ## +function circshift!(a::AbstractVector, shift::Integer) + n = length(a) + n == 0 && return + shift = mod(shift, n) + shift == 0 && return + reverse!(a, 1, shift) + reverse!(a, shift+1, length(a)) + reverse!(a) + return a +end diff --git a/base/abstractarraymath.jl b/base/abstractarraymath.jl index 953c190ab12efd..9690fc0f2e4c4b 100644 --- a/base/abstractarraymath.jl +++ b/base/abstractarraymath.jl @@ -36,7 +36,7 @@ julia> vec(1:3) 1:3 ``` -See also [`reshape`](@ref). +See also [`reshape`](@ref), [`dropdims`](@ref). """ vec(a::AbstractArray) = reshape(a,length(a)) vec(a::AbstractVector) = a @@ -48,9 +48,15 @@ _sub(t::Tuple, s::Tuple) = _sub(tail(t), tail(s)) """ dropdims(A; dims) -Remove the dimensions specified by `dims` from array `A`. -Elements of `dims` must be unique and within the range `1:ndims(A)`. -`size(A,i)` must equal 1 for all `i` in `dims`. +Return an array with the same data as `A`, but with the dimensions specified by +`dims` removed. `size(A,d)` must equal 1 for every `d` in `dims`, +and repeated dimensions or numbers outside `1:ndims(A)` are forbidden. + +The result shares the same underlying data as `A`, such that the +result is mutable if and only if `A` is mutable, and setting elements of one +alters the values of the other. + +See also: [`reshape`](@ref), [`vec`](@ref). # Examples ```jldoctest @@ -60,11 +66,17 @@ julia> a = reshape(Vector(1:4),(2,2,1,1)) 1 3 2 4 -julia> dropdims(a; dims=3) +julia> b = dropdims(a; dims=3) 2×2×1 Array{Int64, 3}: [:, :, 1] = 1 3 2 4 + +julia> b[1,1,1] = 5; a +2×2×1×1 Array{Int64, 4}: +[:, :, 1, 1] = + 5 3 + 2 4 ``` """ dropdims(A; dims) = _dropdims(A, dims) @@ -76,23 +88,134 @@ function _dropdims(A::AbstractArray, dims::Dims) dims[j] == dims[i] && throw(ArgumentError("dropped dims must be unique")) end end - d = () - for i = 1:ndims(A) - if !in(i, dims) - d = tuple(d..., axes(A, i)) - end - end - reshape(A, d::typeof(_sub(axes(A), dims))) + ax = _foldoneto((ds, d) -> d in dims ? ds : (ds..., axes(A,d)), (), Val(ndims(A))) + reshape(A, ax::typeof(_sub(axes(A), dims))) end _dropdims(A::AbstractArray, dim::Integer) = _dropdims(A, (Int(dim),)) ## Unary operators ## -conj(x::AbstractArray{<:Real}) = x +""" + conj!(A) + +Transform an array to its complex conjugate in-place. + +See also [`conj`](@ref). + +# Examples +```jldoctest +julia> A = [1+im 2-im; 2+2im 3+im] +2×2 Matrix{Complex{Int64}}: + 1+1im 2-1im + 2+2im 3+1im + +julia> conj!(A); + +julia> A +2×2 Matrix{Complex{Int64}}: + 1-1im 2+1im + 2-2im 3-1im +``` +""" +conj!(A::AbstractArray{<:Number}) = (@inbounds broadcast!(conj, A, A); A) conj!(x::AbstractArray{<:Real}) = x -real(x::AbstractArray{<:Real}) = x -imag(x::AbstractArray{<:Real}) = zero(x) +""" + conj(A::AbstractArray) + +Return an array containing the complex conjugate of each entry in array `A`. + +Equivalent to `conj.(A)`, except that when `eltype(A) <: Real` +`A` is returned without copying, and that when `A` has zero dimensions, +a 0-dimensional array is returned (rather than a scalar). + +# Examples +```jldoctest +julia> conj([1, 2im, 3 + 4im]) +3-element Vector{Complex{Int64}}: + 1 + 0im + 0 - 2im + 3 - 4im + +julia> conj(fill(2 - im)) +0-dimensional Array{Complex{Int64}, 0}: +2 + 1im +``` +""" +conj(A::AbstractArray) = broadcast_preserving_zero_d(conj, A) +conj(A::AbstractArray{<:Real}) = A + +""" + real(A::AbstractArray) + +Return an array containing the real part of each entry in array `A`. + +Equivalent to `real.(A)`, except that when `eltype(A) <: Real` +`A` is returned without copying, and that when `A` has zero dimensions, +a 0-dimensional array is returned (rather than a scalar). + +# Examples +```jldoctest +julia> real([1, 2im, 3 + 4im]) +3-element Vector{Int64}: + 1 + 0 + 3 + +julia> real(fill(2 - im)) +0-dimensional Array{Int64, 0}: +2 +``` +""" +real(A::AbstractArray) = broadcast_preserving_zero_d(real, A) +real(A::AbstractArray{<:Real}) = A + +""" + imag(A::AbstractArray) + +Return an array containing the imaginary part of each entry in array `A`. + +Equivalent to `imag.(A)`, except that when `A` has zero dimensions, +a 0-dimensional array is returned (rather than a scalar). + +# Examples +```jldoctest +julia> imag([1, 2im, 3 + 4im]) +3-element Vector{Int64}: + 0 + 2 + 4 + +julia> imag(fill(2 - im)) +0-dimensional Array{Int64, 0}: +-1 +``` +""" +imag(A::AbstractArray) = broadcast_preserving_zero_d(imag, A) +imag(A::AbstractArray{<:Real}) = zero(A) + +""" + reim(A::AbstractArray) + +Return a tuple of two arrays containing respectively the real and the imaginary +part of each entry in `A`. + +Equivalent to `(real.(A), imag.(A))`, except that when `eltype(A) <: Real` +`A` is returned without copying to represent the real part, and that when `A` has +zero dimensions, a 0-dimensional array is returned (rather than a scalar). + +# Examples +```jldoctest +julia> reim([1, 2im, 3 + 4im]) +([1, 0, 3], [0, 2, 4]) + +julia> reim(fill(2 - im)) +(fill(2), fill(-1)) +``` +""" +reim(A::AbstractArray) + +-(A::AbstractArray) = broadcast_preserving_zero_d(-, A) +(x::AbstractArray{<:Number}) = x *(x::AbstractArray{<:Number,2}) = x @@ -106,6 +229,8 @@ Return a view of all the data of `A` where the index for dimension `d` equals `i Equivalent to `view(A,:,:,...,i,:,:,...)` where `i` is in position `d`. +See also: [`eachslice`](@ref). + # Examples ```jldoctest julia> A = [1 2 3 4; 5 6 7 8] @@ -117,13 +242,18 @@ julia> selectdim(A, 2, 3) 2-element view(::Matrix{Int64}, :, 3) with eltype Int64: 3 7 + +julia> selectdim(A, 2, 3:4) +2×2 view(::Matrix{Int64}, :, 3:4) with eltype Int64: + 3 4 + 7 8 ``` """ @inline selectdim(A::AbstractArray, d::Integer, i) = _selectdim(A, d, i, _setindex(i, d, map(Slice, axes(A))...)) @noinline function _selectdim(A, d, i, idxs) d >= 1 || throw(ArgumentError("dimension must be ≥ 1, got $d")) nd = ndims(A) - d > nd && (i == 1 || throw(BoundsError(A, (ntuple(k->Colon(),d-1)..., i)))) + d > nd && (i == 1 || throw(BoundsError(A, (ntuple(Returns(Colon()),d-1)..., i)))) return view(A, idxs...) end @@ -138,6 +268,8 @@ Circularly shift, i.e. rotate, the data in an array. The second argument is a tu vector giving the amount to shift in each dimension, or an integer to shift only in the first dimension. +See also: [`circshift!`](@ref), [`circcopy!`](@ref), [`bitrotate`](@ref), [`<<`](@ref). + # Examples ```jldoctest julia> b = reshape(Vector(1:16), (4,4)) @@ -185,8 +317,6 @@ julia> circshift(a, -1) 1 1 ``` - -See also [`circshift!`](@ref). """ function circshift(a::AbstractArray, shiftamt) circshift!(similar(a), a, map(Integer, (shiftamt...,))) @@ -199,6 +329,8 @@ end Construct an array by repeating array `A` a given number of times in each dimension, specified by `counts`. +See also: [`fill`](@ref), [`Iterators.repeated`](@ref), [`Iterators.cycle`](@ref). + # Examples ```jldoctest julia> repeat([1, 2, 3], 2) @@ -225,7 +357,7 @@ function repeat(A::AbstractArray, counts...) end """ - repeat(A::AbstractArray; inner=ntuple(x->1, ndims(A)), outer=ntuple(x->1, ndims(A))) + repeat(A::AbstractArray; inner=ntuple(Returns(1), ndims(A)), outer=ntuple(Returns(1), ndims(A))) Construct an array by repeating the entries of `A`. The i-th element of `inner` specifies the number of times that the individual entries of the i-th dimension of `A` should be @@ -369,7 +501,6 @@ function repeat_outer(arr::AbstractArray{<:Any,N}, dims::NTuple{N,Any}) where {N end function repeat_inner(arr, inner) - basedims = size(arr) outsize = map(*, size(arr), inner) out = similar(arr, outsize) for I in CartesianIndices(arr) @@ -392,7 +523,7 @@ end#module Create a generator that iterates over the first dimension of vector or matrix `A`, returning the rows as `AbstractVector` views. -See also [`eachcol`](@ref) and [`eachslice`](@ref). +See also [`eachcol`](@ref), [`eachslice`](@ref), [`mapslices`](@ref). !!! compat "Julia 1.1" This function requires at least Julia 1.1. @@ -460,7 +591,7 @@ the data from the other dimensions in `A`. Only a single dimension in `dims` is currently supported. Equivalent to `(view(A,:,:,...,i,:,: ...)) for i in axes(A, dims))`, where `i` is in position `dims`. -See also [`eachrow`](@ref), [`eachcol`](@ref), and [`selectdim`](@ref). +See also [`eachrow`](@ref), [`eachcol`](@ref), [`mapslices`](@ref), and [`selectdim`](@ref). !!! compat "Julia 1.1" This function requires at least Julia 1.1. @@ -491,7 +622,7 @@ julia> collect(eachslice(M, dims=2)) length(dims) == 1 || throw(ArgumentError("only single dimensions are supported")) dim = first(dims) dim <= ndims(A) || throw(DimensionMismatch("A doesn't have $dim dimensions")) - inds_before = ntuple(d->(:), dim-1) - inds_after = ntuple(d->(:), ndims(A)-dim) + inds_before = ntuple(Returns(:), dim-1) + inds_after = ntuple(Returns(:), ndims(A)-dim) return (view(A, inds_before..., i, inds_after...) for i in axes(A, dim)) end diff --git a/base/abstractdict.jl b/base/abstractdict.jl index 62bb3b8cf5a2e8..527b422fb56848 100644 --- a/base/abstractdict.jl +++ b/base/abstractdict.jl @@ -31,9 +31,13 @@ function in(p, a::AbstractDict) end function summary(io::IO, t::AbstractDict) - n = length(t) showarg(io, t, true) - print(io, " with ", n, (n==1 ? " entry" : " entries")) + if Base.IteratorSize(t) isa HasLength + n = length(t) + print(io, " with ", n, (n==1 ? " entry" : " entries")) + else + print(io, "(...)") + end end struct KeySet{K, T <: AbstractDict{K}} <: AbstractSet{K} @@ -62,6 +66,8 @@ function iterate(v::Union{KeySet,ValueIterator}, state...) return (y[1][isa(v, KeySet) ? 1 : 2], y[2]) end +copy(v::KeySet) = copymutable(v) + in(k, v::KeySet) = get(v.dict, k, secret_table_token) !== secret_table_token """ @@ -130,6 +136,38 @@ values(a::AbstractDict) = ValueIterator(a) Return an iterator over `key => value` pairs for any collection that maps a set of keys to a set of values. This includes arrays, where the keys are the array indices. + +# Examples +```jldoctest +julia> a = Dict(zip(["a", "b", "c"], [1, 2, 3])) +Dict{String, Int64} with 3 entries: + "c" => 3 + "b" => 2 + "a" => 1 + +julia> pairs(a) +Dict{String, Int64} with 3 entries: + "c" => 3 + "b" => 2 + "a" => 1 + +julia> foreach(println, pairs(["a", "b", "c"])) +1 => "a" +2 => "b" +3 => "c" + +julia> (;a=1, b=2, c=3) |> pairs |> collect +3-element Vector{Pair{Symbol, Int64}}: + :a => 1 + :b => 2 + :c => 3 + +julia> (;a=1, b=2, c=3) |> collect +3-element Vector{Int64}: + 1 + 2 + 3 +``` """ pairs(collection) = Generator(=>, keys(collection), values(collection)) @@ -151,7 +189,10 @@ empty(a::AbstractDict) = empty(a, keytype(a), valtype(a)) empty(a::AbstractDict, ::Type{V}) where {V} = empty(a, keytype(a), V) # Note: this is the form which makes sense for `Vector`. copy(a::AbstractDict) = merge!(empty(a), a) -copy!(dst::AbstractDict, src::AbstractDict) = merge!(empty!(dst), src) +function copy!(dst::AbstractDict, src::AbstractDict) + dst === src && return dst + merge!(empty!(dst), src) +end """ merge!(d::AbstractDict, others::AbstractDict...) @@ -176,6 +217,9 @@ Dict{Int64, Int64} with 3 entries: """ function merge!(d::AbstractDict, others::AbstractDict...) for other in others + if haslength(d) && haslength(other) + sizehint!(d, length(d) + length(other)) + end for (k,v) in other d[k] = v end @@ -230,12 +274,14 @@ Dict{Int64, Int64} with 3 entries: ``` """ function mergewith!(combine, d::AbstractDict, others::AbstractDict...) - for other in others - for (k,v) in other - d[k] = haskey(d, k) ? combine(d[k], v) : v - end + foldl(mergewith!(combine), others; init = d) +end + +function mergewith!(combine, d1::AbstractDict, d2::AbstractDict) + for (k, v) in d2 + d1[k] = haskey(d1, k) ? combine(d1[k], v) : v end - return d + return d1 end mergewith!(combine) = (args...) -> mergewith!(combine, args...) @@ -245,7 +291,7 @@ merge!(combine::Callable, args...) = mergewith!(combine, args...) """ keytype(type) -Get the key type of an dictionary type. Behaves similarly to [`eltype`](@ref). +Get the key type of a dictionary type. Behaves similarly to [`eltype`](@ref). # Examples ```jldoctest @@ -259,7 +305,7 @@ keytype(a::AbstractDict) = keytype(typeof(a)) """ valtype(type) -Get the value type of an dictionary type. Behaves similarly to [`eltype`](@ref). +Get the value type of a dictionary type. Behaves similarly to [`eltype`](@ref). # Examples ```jldoctest @@ -277,6 +323,7 @@ Construct a merged collection from the given collections. If necessary, the types of the resulting collection will be promoted to accommodate the types of the merged collections. If the same key is present in another collection, the value for that key will be the value it has in the last collection listed. +See also [`mergewith`](@ref) for custom handling of values with the same key. # Examples ```jldoctest @@ -477,6 +524,9 @@ function ==(l::AbstractDict, r::AbstractDict) return anymissing ? missing : true end +# Fallback implementation +sizehint!(d::AbstractDict, n) = d + const hasha_seed = UInt === UInt64 ? 0x6d35bb51952d5539 : 0x952d5539 function hash(a::AbstractDict, h::UInt) hv = hasha_seed diff --git a/base/abstractset.jl b/base/abstractset.jl index 05b53009528226..85d81480ab9902 100644 --- a/base/abstractset.jl +++ b/base/abstractset.jl @@ -1,9 +1,12 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license eltype(::Type{<:AbstractSet{T}}) where {T} = @isdefined(T) ? T : Any -sizehint!(s::AbstractSet, n) = nothing +sizehint!(s::AbstractSet, n) = s -copy!(dst::AbstractSet, src::AbstractSet) = union!(empty!(dst), src) +function copy!(dst::AbstractSet, src::AbstractSet) + dst === src && return dst + union!(empty!(dst), src) +end ## set operations (union, intersection, symmetric difference) @@ -11,28 +14,36 @@ copy!(dst::AbstractSet, src::AbstractSet) = union!(empty!(dst), src) union(s, itrs...) ∪(s, itrs...) -Construct the union of sets. Maintain order with arrays. +Construct an object containing all distinct elements from all of the arguments. + +The first argument controls what kind of container is returned. +If this is an array, it maintains the order in which elements first appear. + +Unicode `∪` can be typed by writing `\\cup` then pressing tab in the Julia REPL, and in many editors. +This is an infix operator, allowing `s ∪ itr`. + +See also [`unique`](@ref), [`intersect`](@ref), [`isdisjoint`](@ref), [`vcat`](@ref), [`Iterators.flatten`](@ref). # Examples ```jldoctest -julia> union([1, 2], [3, 4]) -4-element Vector{Int64}: +julia> union([1, 2], [3]) +3-element Vector{Int64}: 1 2 3 - 4 -julia> union([1, 2], [2, 4]) -3-element Vector{Int64}: - 1 - 2 - 4 +julia> union([4 2 3 4 4], 1:3, 3.0) +4-element Vector{Float64}: + 4.0 + 2.0 + 3.0 + 1.0 -julia> union([4, 2], 1:2) -3-element Vector{Int64}: - 4 - 2 - 1 +julia> (0, 0.0) ∪ (-0.0, NaN) +3-element Vector{Real}: + 0 + -0.0 + NaN julia> union(Set([1, 2]), 2:3) Set{Int64} with 3 elements: @@ -43,8 +54,6 @@ Set{Int64} with 3 elements: """ function union end -_in(itr) = x -> x in itr - union(s, sets...) = union!(emptymutable(s, promote_eltype(s, sets...)), s, sets...) union(s::AbstractSet) = copy(s) @@ -53,21 +62,21 @@ const ∪ = union """ union!(s::Union{AbstractSet,AbstractVector}, itrs...) -Construct the union of passed in sets and overwrite `s` with the result. +Construct the [`union`](@ref) of passed in sets and overwrite `s` with the result. Maintain order with arrays. # Examples ```jldoctest -julia> a = Set([1, 3, 4, 5]); +julia> a = Set([3, 4, 5]); -julia> union!(a, 1:2:8); +julia> union!(a, 1:2:7); julia> a Set{Int64} with 5 elements: - 7 + 5 4 + 7 3 - 5 1 ``` """ @@ -81,7 +90,11 @@ end max_values(::Type) = typemax(Int) max_values(T::Union{map(X -> Type{X}, BitIntegerSmall_types)...}) = 1 << (8*sizeof(T)) # saturated addition to prevent overflow with typemax(Int) -max_values(T::Union) = max(max_values(T.a), max_values(T.b), max_values(T.a) + max_values(T.b)) +function max_values(T::Union) + a = max_values(T.a)::Int + b = max_values(T.b)::Int + return max(a, b, a + b) +end max_values(::Type{Bool}) = 2 max_values(::Type{Nothing}) = 1 @@ -98,8 +111,19 @@ end intersect(s, itrs...) ∩(s, itrs...) -Construct the intersection of sets. -Maintain order with arrays. +Construct the set containing those elements which appear in all of the arguments. + +The first argument controls what kind of container is returned. +If this is an array, it maintains the order in which elements first appear. + +Unicode `∩` can be typed by writing `\\cap` then pressing tab in the Julia REPL, and in many editors. +This is an infix operator, allowing `s ∩ itr`. + +See also [`setdiff`](@ref), [`isdisjoint`](@ref), [`issubset`](@ref Base.issubset), [`issetequal`](@ref). + +!!! compat "Julia 1.8" + As of Julia 1.8 intersect returns a result with the eltype of the + type-promoted eltypes of the two inputs # Examples ```jldoctest @@ -107,19 +131,49 @@ julia> intersect([1, 2, 3], [3, 4, 5]) 1-element Vector{Int64}: 3 -julia> intersect([1, 4, 4, 5, 6], [4, 6, 6, 7, 8]) +julia> intersect([1, 4, 4, 5, 6], [6, 4, 6, 7, 8]) 2-element Vector{Int64}: 4 6 -julia> intersect(Set([1, 2]), BitSet([2, 3])) -Set{Int64} with 1 element: - 2 +julia> intersect(1:16, 7:99) +7:16 + +julia> (0, 0.0) ∩ (-0.0, 0) +1-element Vector{Real}: + 0 + +julia> intersect(Set([1, 2]), BitSet([2, 3]), 1.0:10.0) +Set{Float64} with 1 element: + 2.0 ``` """ -intersect(s::AbstractSet, itr, itrs...) = intersect!(intersect(s, itr), itrs...) +function intersect(s::AbstractSet, itr, itrs...) + # heuristics to try to `intersect` with the shortest Set on the left + if length(s)>50 && haslength(itr) && all(haslength, itrs) + min_length, min_idx = findmin(length, itrs) + if length(itr) > min_length + new_itrs = setindex(itrs, itr, min_idx) + return intersect(s, itrs[min_idx], new_itrs...) + end + end + T = promote_eltype(s, itr, itrs...) + if T == promote_eltype(s, itr) + out = intersect(s, itr) + else + out = union!(emptymutable(s, T), s) + intersect!(out, itr) + end + return intersect!(out, itrs...) +end intersect(s) = union(s) -intersect(s::AbstractSet, itr) = mapfilter(_in(s), push!, itr, emptymutable(s)) +function intersect(s::AbstractSet, itr) + if haslength(itr) && hasfastin(itr) && length(s) < length(itr) + return mapfilter(in(itr), push!, s, emptymutable(s, promote_eltype(s, itr))) + else + return mapfilter(in(s), push!, itr, emptymutable(s, promote_eltype(s, itr))) + end +end const ∩ = intersect @@ -135,7 +189,7 @@ function intersect!(s::AbstractSet, itrs...) end return s end -intersect!(s::AbstractSet, s2::AbstractSet) = filter!(_in(s2), s) +intersect!(s::AbstractSet, s2::AbstractSet) = filter!(in(s2), s) intersect!(s::AbstractSet, itr) = intersect!(s, union!(emptymutable(s, eltype(itr)), itr)) @@ -145,6 +199,8 @@ intersect!(s::AbstractSet, itr) = Construct the set of elements in `s` but not in any of the iterables in `itrs`. Maintain order with arrays. +See also [`setdiff!`](@ref), [`union`](@ref) and [`intersect`](@ref). + # Examples ```jldoctest julia> setdiff([1,2,3], [3,4,5]) @@ -194,6 +250,8 @@ Construct the symmetric difference of elements in the passed in sets. When `s` is not an `AbstractSet`, the order is maintained. Note that in this case the multiplicity of elements matters. +See also [`symdiff!`](@ref), [`setdiff`](@ref), [`union`](@ref) and [`intersect`](@ref). + # Examples ```jldoctest julia> symdiff([1,2,3], [3,4,5], [4,5,6]) @@ -246,6 +304,8 @@ function ⊇ end Determine whether every element of `a` is also in `b`, using [`in`](@ref). +See also [`⊊`](@ref), [`⊈`](@ref), [`∩`](@ref intersect), [`∪`](@ref union), [`contains`](@ref). + # Examples ```jldoctest julia> issubset([1, 2], [1, 2, 3]) @@ -262,21 +322,21 @@ issubset, ⊆, ⊇ const FASTIN_SET_THRESHOLD = 70 -function issubset(l, r) - if haslength(r) && (isa(l, AbstractSet) || !hasfastin(r)) - rlen = length(r) # conditions above make this length computed only when needed - # check l for too many unique elements - if isa(l, AbstractSet) && length(l) > rlen +function issubset(a, b) + if haslength(b) && (isa(a, AbstractSet) || !hasfastin(b)) + blen = length(b) # conditions above make this length computed only when needed + # check a for too many unique elements + if isa(a, AbstractSet) && length(a) > blen return false end - # when `in` would be too slow and r is big enough, convert it to a Set + # when `in` would be too slow and b is big enough, convert it to a Set # this threshold was empirically determined (cf. #26198) - if !hasfastin(r) && rlen > FASTIN_SET_THRESHOLD - return issubset(l, Set(r)) + if !hasfastin(b) && blen > FASTIN_SET_THRESHOLD + return issubset(a, Set(b)) end end - for elt in l - elt in r || return false + for elt in a + elt in b || return false end return true end @@ -294,7 +354,7 @@ hasfastin(::Type) = false hasfastin(::Union{Type{<:AbstractSet},Type{<:AbstractDict},Type{<:AbstractRange}}) = true hasfastin(x) = hasfastin(typeof(x)) -⊇(l, r) = r ⊆ l +⊇(a, b) = b ⊆ a ## strict subset comparison @@ -306,6 +366,8 @@ function ⊋ end Determines if `a` is a subset of, but not equal to, `b`. +See also [`issubset`](@ref) (`⊆`), [`⊈`](@ref). + # Examples ```jldoctest julia> (1, 2) ⊊ (1, 2, 3) @@ -317,9 +379,11 @@ false """ ⊊, ⊋ -⊊(l::AbstractSet, r) = length(l) < length(r) && l ⊆ r -⊊(l, r) = Set(l) ⊊ r -⊋(l, r) = r ⊊ l +⊊(a::AbstractSet, b::AbstractSet) = length(a) < length(b) && a ⊆ b +⊊(a::AbstractSet, b) = a ⊊ Set(b) +⊊(a, b::AbstractSet) = Set(a) ⊊ b +⊊(a, b) = Set(a) ⊊ Set(b) +⊋(a, b) = b ⊊ a function ⊈ end function ⊉ end @@ -329,6 +393,8 @@ function ⊉ end Negation of `⊆` and `⊇`, i.e. checks that `a` is not a subset of `b`. +See also [`issubset`](@ref) (`⊆`), [`⊊`](@ref). + # Examples ```jldoctest julia> (1, 2) ⊈ (2, 3) @@ -340,8 +406,8 @@ false """ ⊈, ⊉ -⊈(l, r) = !⊆(l, r) -⊉(l, r) = r ⊈ l +⊈(a, b) = !⊆(a, b) +⊉(a, b) = b ⊈ a ## set equality comparison @@ -351,6 +417,8 @@ false Determine whether `a` and `b` have the same elements. Equivalent to `a ⊆ b && b ⊆ a` but more efficient when possible. +See also: [`isdisjoint`](@ref), [`union`](@ref). + # Examples ```jldoctest julia> issetequal([1, 2], [1, 2, 3]) @@ -360,54 +428,65 @@ julia> issetequal([1, 2], [2, 1]) true ``` """ -issetequal(l::AbstractSet, r::AbstractSet) = l == r -issetequal(l::AbstractSet, r) = issetequal(l, Set(r)) +issetequal(a::AbstractSet, b::AbstractSet) = a == b +issetequal(a::AbstractSet, b) = issetequal(a, Set(b)) -function issetequal(l, r::AbstractSet) - if haslength(l) - # check r for too many unique elements - length(l) < length(r) && return false +function issetequal(a, b::AbstractSet) + if haslength(a) + # check b for too many unique elements + length(a) < length(b) && return false end - return issetequal(Set(l), r) + return issetequal(Set(a), b) end -function issetequal(l, r) - haslength(l) && return issetequal(l, Set(r)) - haslength(r) && return issetequal(r, Set(l)) - return issetequal(Set(l), Set(r)) +function issetequal(a, b) + haslength(a) && return issetequal(a, Set(b)) + haslength(b) && return issetequal(b, Set(a)) + return issetequal(Set(a), Set(b)) end ## set disjoint comparison """ - isdisjoint(v1, v2) -> Bool + isdisjoint(a, b) -> Bool + +Determine whether the collections `a` and `b` are disjoint. +Equivalent to `isempty(a ∩ b)` but more efficient when possible. -Return whether the collections `v1` and `v2` are disjoint, i.e. whether -their intersection is empty. +See also: [`intersect`](@ref), [`isempty`](@ref), [`issetequal`](@ref). !!! compat "Julia 1.5" This function requires at least Julia 1.5. + +# Examples +```jldoctest +julia> isdisjoint([1, 2], [2, 3, 4]) +false + +julia> isdisjoint([3, 1], [2, 4]) +true +``` """ -function isdisjoint(l, r) - function _isdisjoint(l, r) - hasfastin(r) && return !any(in(r), l) - hasfastin(l) && return !any(in(l), r) - haslength(r) && length(r) < FASTIN_SET_THRESHOLD && - return !any(in(r), l) - return !any(in(Set(r)), l) +function isdisjoint(a, b) + function _isdisjoint(a, b) + hasfastin(b) && return !any(in(b), a) + hasfastin(a) && return !any(in(a), b) + haslength(b) && length(b) < FASTIN_SET_THRESHOLD && + return !any(in(b), a) + return !any(in(Set(b)), a) end - if haslength(l) && haslength(r) && length(r) < length(l) - return _isdisjoint(r, l) + if haslength(a) && haslength(b) && length(b) < length(a) + return _isdisjoint(b, a) end - _isdisjoint(l, r) + _isdisjoint(a, b) end ## partial ordering of sets by containment -==(l::AbstractSet, r::AbstractSet) = length(l) == length(r) && l ⊆ r +==(a::AbstractSet, b::AbstractSet) = length(a) == length(b) && a ⊆ b # convenience functions for AbstractSet # (if needed, only their synonyms ⊊ and ⊆ must be specialized) -<( l::AbstractSet, r::AbstractSet) = l ⊊ r -<=(l::AbstractSet, r::AbstractSet) = l ⊆ r +<( a::AbstractSet, b::AbstractSet) = a ⊊ b +<=(a::AbstractSet, b::AbstractSet) = a ⊆ b ## filtering sets diff --git a/base/accumulate.jl b/base/accumulate.jl index fe06dbc1c2c70d..663bd850695a89 100644 --- a/base/accumulate.jl +++ b/base/accumulate.jl @@ -116,35 +116,29 @@ end """ cumsum(itr) -Cumulative sum an iterator. See also [`cumsum!`](@ref) -to use a preallocated output array, both for performance and to control the precision of the -output (e.g. to avoid overflow). +Cumulative sum of an iterator. + +See also [`accumulate`](@ref) to apply functions other than `+`. !!! compat "Julia 1.5" `cumsum` on a non-array iterator requires at least Julia 1.5. # Examples ```jldoctest -julia> cumsum([1, 1, 1]) +julia> cumsum(1:3) 3-element Vector{Int64}: 1 - 2 3 + 6 + +julia> cumsum((true, false, true, false, true)) +(1, 1, 2, 2, 3) -julia> cumsum([fill(1, 2) for i in 1:3]) +julia> cumsum(fill(1, 2) for i in 1:3) 3-element Vector{Vector{Int64}}: [1, 1] [2, 2] [3, 3] - -julia> cumsum((1, 1, 1)) -(1, 2, 3) - -julia> cumsum(x^2 for x in 1:3) -3-element Vector{Int64}: - 1 - 5 - 14 ``` """ cumsum(x::AbstractVector) = cumsum(x, dims=1) @@ -177,10 +171,7 @@ to control the precision of the output (e.g. to avoid overflow). # Examples ```jldoctest -julia> a = [1 2 3; 4 5 6] -2×3 Matrix{Int64}: - 1 2 3 - 4 5 6 +julia> a = Int8[1 2 3; 4 5 6]; julia> cumprod(a, dims=1) 2×3 Matrix{Int64}: @@ -200,9 +191,9 @@ end """ cumprod(itr) -Cumulative product of an iterator. See also -[`cumprod!`](@ref) to use a preallocated output array, both for performance and -to control the precision of the output (e.g. to avoid overflow). +Cumulative product of an iterator. + +See also [`cumprod!`](@ref), [`accumulate`](@ref), [`cumsum`](@ref). !!! compat "Julia 1.5" `cumprod` on a non-array iterator requires at least Julia 1.5. @@ -215,20 +206,16 @@ julia> cumprod(fill(1//2, 3)) 1//4 1//8 -julia> cumprod([fill(1//3, 2, 2) for i in 1:3]) -3-element Vector{Matrix{Rational{Int64}}}: - [1//3 1//3; 1//3 1//3] - [2//9 2//9; 2//9 2//9] - [4//27 4//27; 4//27 4//27] +julia> cumprod((1, 2, 1, 3, 1)) +(1, 2, 2, 6, 6) -julia> cumprod((1, 2, 1)) -(1, 2, 2) - -julia> cumprod(x^2 for x in 1:3) -3-element Vector{Int64}: - 1 - 4 - 36 +julia> cumprod("julia") +5-element Vector{String}: + "j" + "ju" + "jul" + "juli" + "julia" ``` """ cumprod(x::AbstractVector) = cumprod(x, dims=1) @@ -241,8 +228,11 @@ cumprod(itr) = accumulate(mul_prod, itr) Cumulative operation `op` along the dimension `dims` of `A` (providing `dims` is optional for vectors). An initial value `init` may optionally be provided by a keyword argument. See also [`accumulate!`](@ref) to use a preallocated output array, both for performance and -to control the precision of the output (e.g. to avoid overflow). For common operations -there are specialized variants of `accumulate`, see: [`cumsum`](@ref), [`cumprod`](@ref) +to control the precision of the output (e.g. to avoid overflow). + +For common operations there are specialized variants of `accumulate`, +see [`cumsum`](@ref), [`cumprod`](@ref). For a lazy version, see +[`Iterators.accumulate`](@ref). !!! compat "Julia 1.5" `accumulate` on a non-array iterator requires at least Julia 1.5. @@ -255,35 +245,28 @@ julia> accumulate(+, [1,2,3]) 3 6 -julia> accumulate(*, [1,2,3]) -3-element Vector{Int64}: - 1 - 2 - 6 +julia> accumulate(min, (1, -2, 3, -4, 5), init=0) +(0, -2, -2, -4, -4) -julia> accumulate(+, [1,2,3]; init=100) -3-element Vector{Int64}: - 101 - 103 - 106 +julia> accumulate(/, (2, 4, Inf), init=100) +(50.0, 12.5, 0.0) -julia> accumulate(min, [1,2,-1]; init=0) -3-element Vector{Int64}: - 0 - 0 - -1 - -julia> accumulate(+, fill(1, 3, 3), dims=1) -3×3 Matrix{Int64}: - 1 1 1 - 2 2 2 - 3 3 3 - -julia> accumulate(+, fill(1, 3, 3), dims=2) -3×3 Matrix{Int64}: - 1 2 3 - 1 2 3 - 1 2 3 +julia> accumulate(=>, i^2 for i in 1:3) +3-element Vector{Any}: + 1 + 1 => 4 + (1 => 4) => 9 + +julia> accumulate(+, fill(1, 3, 4)) +3×4 Matrix{Int64}: + 1 4 7 10 + 2 5 8 11 + 3 6 9 12 + +julia> accumulate(+, fill(1, 2, 5), dims=2, init=100.0) +2×5 Matrix{Float64}: + 101.0 102.0 103.0 104.0 105.0 + 101.0 102.0 103.0 104.0 105.0 ``` """ function accumulate(op, A; dims::Union{Nothing,Integer}=nothing, kw...) @@ -291,10 +274,10 @@ function accumulate(op, A; dims::Union{Nothing,Integer}=nothing, kw...) # This branch takes care of the cases not handled by `_accumulate!`. return collect(Iterators.accumulate(op, A; kw...)) end - nt = kw.data - if nt isa NamedTuple{()} + nt = values(kw) + if isempty(kw) out = similar(A, promote_op(op, eltype(A), eltype(A))) - elseif nt isa NamedTuple{(:init,)} + elseif keys(nt) === (:init,) out = similar(A, promote_op(op, typeof(nt.init), eltype(A))) else throw(ArgumentError("acccumulate does not support the keyword arguments $(setdiff(keys(nt), (:init,)))")) @@ -316,48 +299,46 @@ end Cumulative operation `op` on `A` along the dimension `dims`, storing the result in `B`. Providing `dims` is optional for vectors. If the keyword argument `init` is given, its -value is used to instantiate the accumulation. See also [`accumulate`](@ref). +value is used to instantiate the accumulation. + +See also [`accumulate`](@ref), [`cumsum!`](@ref), [`cumprod!`](@ref). # Examples ```jldoctest julia> x = [1, 0, 2, 0, 3]; -julia> y = [0, 0, 0, 0, 0]; +julia> y = rand(5); julia> accumulate!(+, y, x); julia> y -5-element Vector{Int64}: - 1 - 1 - 3 - 3 - 6 +5-element Vector{Float64}: + 1.0 + 1.0 + 3.0 + 3.0 + 6.0 -julia> A = [1 2; 3 4]; +julia> A = [1 2 3; 4 5 6]; -julia> B = [0 0; 0 0]; +julia> B = similar(A); -julia> accumulate!(-, B, A, dims=1); - -julia> B -2×2 Matrix{Int64}: - 1 2 - -2 -2 - -julia> accumulate!(-, B, A, dims=2); +julia> accumulate!(-, B, A, dims=1) +2×3 Matrix{Int64}: + 1 2 3 + -3 -3 -3 -julia> B -2×2 Matrix{Int64}: - 1 -1 - 3 -1 +julia> accumulate!(*, B, A, dims=2, init=10) +2×3 Matrix{Int64}: + 10 20 60 + 40 200 1200 ``` """ function accumulate!(op, B, A; dims::Union{Integer, Nothing} = nothing, kw...) - nt = kw.data - if nt isa NamedTuple{()} + nt = values(kw) + if isempty(kw) _accumulate!(op, B, A, dims, nothing) - elseif nt isa NamedTuple{(:init,)} + elseif keys(kw) === (:init,) _accumulate!(op, B, A, dims, Some(nt.init)) else throw(ArgumentError("acccumulate! does not support the keyword arguments $(setdiff(keys(nt), (:init,)))")) @@ -441,7 +422,7 @@ function _accumulate1!(op, B, v1, A::AbstractVector, dim::Integer) inds = LinearIndices(A) inds == LinearIndices(B) || throw(DimensionMismatch("LinearIndices of A and B don't match")) dim > 1 && return copyto!(B, A) - (i1, state) = iterate(inds) # We checked earlier that A isn't empty + (i1, state) = iterate(inds)::NTuple{2,Any} # We checked earlier that A isn't empty cur_val = v1 B[i1] = cur_val next = iterate(inds, state) diff --git a/base/array.jl b/base/array.jl index c88dd8cba5878f..b5a1ba31f0acf7 100644 --- a/base/array.jl +++ b/base/array.jl @@ -9,7 +9,7 @@ The objects called do not have matching dimensionality. Optional argument `msg` descriptive error string. """ struct DimensionMismatch <: Exception - msg::AbstractString + msg::String end DimensionMismatch() = DimensionMismatch("") @@ -54,6 +54,8 @@ Array One-dimensional dense array with elements of type `T`, often used to represent a mathematical vector. Alias for [`Array{T,1}`](@ref). + +See also [`empty`](@ref), [`similar`](@ref) and [`zero`](@ref) for creating vectors. """ const Vector{T} = Array{T,1} @@ -62,12 +64,28 @@ const Vector{T} = Array{T,1} Two-dimensional dense array with elements of type `T`, often used to represent a mathematical matrix. Alias for [`Array{T,2}`](@ref). + +See also [`fill`](@ref), [`zeros`](@ref), [`undef`](@ref) and [`similar`](@ref) +for creating matrices. """ const Matrix{T} = Array{T,2} + """ VecOrMat{T} -Union type of [`Vector{T}`](@ref) and [`Matrix{T}`](@ref). +Union type of [`Vector{T}`](@ref) and [`Matrix{T}`](@ref) which allows functions to accept either a Matrix or a Vector. + +# Examples +```jldoctest +julia> Vector{Float64} <: VecOrMat{Float64} +true + +julia> Matrix{Float64} <: VecOrMat{Float64} +true + +julia> Array{Float64, 3} <: VecOrMat{Float64} +false +``` """ const VecOrMat{T} = Union{Vector{T}, Matrix{T}} @@ -102,29 +120,7 @@ const DenseVecOrMat{T} = Union{DenseVector{T}, DenseMatrix{T}} ## Basic functions ## -""" - eltype(type) - -Determine the type of the elements generated by iterating a collection of the given `type`. -For dictionary types, this will be a `Pair{KeyType,ValType}`. The definition -`eltype(x) = eltype(typeof(x))` is provided for convenience so that instances can be passed -instead of types. However the form that accepts a type argument should be defined for new -types. - -# Examples -```jldoctest -julia> eltype(fill(1f0, (2,2))) -Float32 - -julia> eltype(fill(0x1, (2,2))) -UInt8 -``` -""" -eltype(::Type) = Any -eltype(::Type{Bottom}) = throw(ArgumentError("Union{} does not have elements")) -eltype(x) = eltype(typeof(x)) - -import Core: arraysize, arrayset, arrayref, const_arrayref +using Core: arraysize, arrayset, const_arrayref vect() = Vector{Any}() vect(X::T...) where {T} = T[ X[i] for i = 1:length(X) ] @@ -154,11 +150,11 @@ end size(a::Array, d::Integer) = arraysize(a, convert(Int, d)) size(a::Vector) = (arraysize(a,1),) size(a::Matrix) = (arraysize(a,1), arraysize(a,2)) -size(a::Array{<:Any,N}) where {N} = (@_inline_meta; ntuple(M -> size(a, M), Val(N))::Dims) +size(a::Array{<:Any,N}) where {N} = (@inline; ntuple(M -> size(a, M), Val(N))::Dims) asize_from(a::Array, n) = n > ndims(a) ? () : (arraysize(a,n), asize_from(a, n+1)...) -allocatedinline(T::Type) = (@_pure_meta; ccall(:jl_stored_inline, Cint, (Any,), T) != Cint(0)) +allocatedinline(T::Type) = (@_total_meta; ccall(:jl_stored_inline, Cint, (Any,), T) != Cint(0)) """ Base.isbitsunion(::Type{T}) @@ -178,7 +174,7 @@ isbitsunion(u::Union) = allocatedinline(u) isbitsunion(x) = false function _unsetindex!(A::Array{T}, i::Int) where {T} - @_inline_meta + @inline @boundscheck checkbounds(A, i) t = @_gc_preserve_begin A p = Ptr{Ptr{Cvoid}}(pointer(A, i)) @@ -197,17 +193,17 @@ end """ - Base.bitsunionsize(U::Union) + Base.bitsunionsize(U::Union) -> Int For a `Union` of [`isbitstype`](@ref) types, return the size of the largest type; assumes `Base.isbitsunion(U) == true`. # Examples ```jldoctest julia> Base.bitsunionsize(Union{Float64, UInt8}) -0x0000000000000008 +8 julia> Base.bitsunionsize(Union{Float64, UInt8, Int128}) -0x0000000000000010 +16 ``` """ function bitsunionsize(u::Union) @@ -216,12 +212,11 @@ function bitsunionsize(u::Union) return sz end -length(a::Array) = arraylen(a) -elsize(::Type{<:Array{T}}) where {T} = aligned_sizeof(T) +elsize(@nospecialize _::Type{A}) where {T,A<:Array{T}} = aligned_sizeof(T) sizeof(a::Array) = Core.sizeof(a) function isassigned(a::Array, i::Int...) - @_inline_meta + @inline ii = (_sub2ind(size(a), i...) % UInt) - 1 @boundscheck ii < length(a) % UInt || return false ccall(:jl_array_isassigned, Cint, (Any, UInt), a, ii) == 1 @@ -340,7 +335,7 @@ end # occurs, see discussion in #27874. # It is also mitigated by using a constant string. function _throw_argerror() - @_noinline_meta + @noinline throw(ArgumentError("Number of elements to copy must be nonnegative.")) end @@ -365,6 +360,8 @@ end Create a shallow copy of `x`: the outer structure is copied, but not all internal values. For example, copying an array produces a new array with identically-same elements as the original. + +See also [`copy!`](@ref Base.copy!), [`copyto!`](@ref). """ copy @@ -404,17 +401,20 @@ julia> getindex(Int8, 1, 2, 3) """ function getindex(::Type{T}, vals...) where T a = Vector{T}(undef, length(vals)) - @inbounds for i = 1:length(vals) - a[i] = vals[i] + if vals isa NTuple + @inbounds for i in 1:length(vals) + a[i] = vals[i] + end + else + # use afoldl to avoid type instability inside loop + afoldl(1, vals...) do i, v + @inbounds a[i] = v + return i + 1 + end end return a end -getindex(::Type{T}) where {T} = (@_inline_meta; Vector{T}()) -getindex(::Type{T}, x) where {T} = (@_inline_meta; a = Vector{T}(undef, 1); @inbounds a[1] = x; a) -getindex(::Type{T}, x, y) where {T} = (@_inline_meta; a = Vector{T}(undef, 2); @inbounds (a[1] = x; a[2] = y); a) -getindex(::Type{T}, x, y, z) where {T} = (@_inline_meta; a = Vector{T}(undef, 3); @inbounds (a[1] = x; a[2] = y; a[3] = z); a) - function getindex(::Type{Any}, @nospecialize vals...) a = Vector{Any}(undef, length(vals)) @inbounds for i = 1:length(vals) @@ -433,14 +433,76 @@ to_dim(d::Integer) = d to_dim(d::OneTo) = last(d) """ - fill(x, dims::Tuple) - fill(x, dims...) + fill(value, dims::Tuple) + fill(value, dims...) + +Create an array of size `dims` with every location set to `value`. -Create an array filled with the value `x`. For example, `fill(1.0, (5,5))` returns a 5×5 -array of floats, with each element initialized to `1.0`. +For example, `fill(1.0, (5,5))` returns a 5×5 array of floats, +with `1.0` in every location of the array. -`dims` may be specified as either a tuple or a sequence of arguments. For example, -the common idiom `fill(x)` creates a zero-dimensional array containing the single value `x`. +The dimension lengths `dims` may be specified as either a tuple or a sequence of arguments. +An `N`-length tuple or `N` arguments following the `value` specify an `N`-dimensional +array. Thus, a common idiom for creating a zero-dimensional array with its only location +set to `x` is `fill(x)`. + +Every location of the returned array is set to (and is thus [`===`](@ref) to) +the `value` that was passed; this means that if the `value` is itself modified, +all elements of the `fill`ed array will reflect that modification because they're +_still_ that very `value`. This is of no concern with `fill(1.0, (5,5))` as the +`value` `1.0` is immutable and cannot itself be modified, but can be unexpected +with mutable values like — most commonly — arrays. For example, `fill([], 3)` +places _the very same_ empty array in all three locations of the returned vector: + +```jldoctest +julia> v = fill([], 3) +3-element Vector{Vector{Any}}: + [] + [] + [] + +julia> v[1] === v[2] === v[3] +true + +julia> value = v[1] +Any[] + +julia> push!(value, 867_5309) +1-element Vector{Any}: + 8675309 + +julia> v +3-element Vector{Vector{Any}}: + [8675309] + [8675309] + [8675309] +``` + +To create an array of many independent inner arrays, use a [comprehension](@ref man-comprehensions) instead. +This creates a new and distinct array on each iteration of the loop: + +```jldoctest +julia> v2 = [[] for _ in 1:3] +3-element Vector{Vector{Any}}: + [] + [] + [] + +julia> v2[1] === v2[2] === v2[3] +false + +julia> push!(v2[1], 8675309) +1-element Vector{Any}: + 8675309 + +julia> v2 +3-element Vector{Vector{Any}}: + [8675309] + [] + [] +``` + +See also: [`fill!`](@ref), [`zeros`](@ref), [`ones`](@ref), [`similar`](@ref). # Examples ```jldoctest @@ -452,15 +514,15 @@ julia> fill(1.0, (2,3)) julia> fill(42) 0-dimensional Array{Int64, 0}: 42 -``` -If `x` is an object reference, all elements will refer to the same object: -```jldoctest -julia> A = fill(zeros(2), 2); +julia> A = fill(zeros(2), 2) # sets both elements to the same [0.0, 0.0] vector +2-element Vector{Vector{Float64}}: + [0.0, 0.0] + [0.0, 0.0] -julia> A[1][1] = 42; # modifies both A[1][1] and A[2][1] +julia> A[1][1] = 42; # modifies the filled value to be [42.0, 0.0] -julia> A +julia> A # both A[1] and A[2] are the very same vector 2-element Vector{Vector{Float64}}: [42.0, 0.0] [42.0, 0.0] @@ -478,7 +540,7 @@ fill(v, dims::Tuple{}) = (a=Array{typeof(v),0}(undef, dims); fill!(a, v); a) zeros([T=Float64,] dims...) Create an `Array`, with element type `T`, of all zeros with size specified by `dims`. -See also [`fill`](@ref), [`ones`](@ref). +See also [`fill`](@ref), [`ones`](@ref), [`zero`](@ref). # Examples ```jldoctest @@ -499,7 +561,7 @@ function zeros end ones([T=Float64,] dims...) Create an `Array`, with element type `T`, of all ones with size specified by `dims`. -See also: [`fill`](@ref), [`zeros`](@ref). +See also [`fill`](@ref), [`zeros`](@ref). # Examples ```jldoctest @@ -552,6 +614,7 @@ oneunit(x::AbstractMatrix{T}) where {T} = _one(oneunit(T), x) ## Conversions ## convert(::Type{T}, a::AbstractArray) where {T<:Array} = a isa T ? a : T(a) +convert(::Type{Union{}}, a::AbstractArray) = throw(MethodError(convert, (Union{}, a))) promote_rule(a::Type{Array{T,n}}, b::Type{Array{S,n}}) where {T,n,S} = el_same(promote_type(T,S), a, b) @@ -582,23 +645,38 @@ julia> collect(Float64, 1:2:5) """ collect(::Type{T}, itr) where {T} = _collect(T, itr, IteratorSize(itr)) -_collect(::Type{T}, itr, isz::HasLength) where {T} = copyto!(Vector{T}(undef, Int(length(itr)::Integer)), itr) -_collect(::Type{T}, itr, isz::HasShape) where {T} = copyto!(similar(Array{T}, axes(itr)), itr) +_collect(::Type{T}, itr, isz::Union{HasLength,HasShape}) where {T} = + copyto!(_array_for(T, isz, _similar_shape(itr, isz)), itr) function _collect(::Type{T}, itr, isz::SizeUnknown) where T a = Vector{T}() for x in itr - push!(a,x) + push!(a, x) end return a end # make a collection similar to `c` and appropriate for collecting `itr` -_similar_for(c::AbstractArray, ::Type{T}, itr, ::SizeUnknown) where {T} = similar(c, T, 0) -_similar_for(c::AbstractArray, ::Type{T}, itr, ::HasLength) where {T} = - similar(c, T, Int(length(itr)::Integer)) -_similar_for(c::AbstractArray, ::Type{T}, itr, ::HasShape) where {T} = - similar(c, T, axes(itr)) -_similar_for(c, ::Type{T}, itr, isz) where {T} = similar(c, T) +_similar_for(c, ::Type{T}, itr, isz, shp) where {T} = similar(c, T) + +_similar_shape(itr, ::SizeUnknown) = nothing +_similar_shape(itr, ::HasLength) = length(itr)::Integer +_similar_shape(itr, ::HasShape) = axes(itr) + +_similar_for(c::AbstractArray, ::Type{T}, itr, ::SizeUnknown, ::Nothing) where {T} = + similar(c, T, 0) +_similar_for(c::AbstractArray, ::Type{T}, itr, ::HasLength, len::Integer) where {T} = + similar(c, T, len) +_similar_for(c::AbstractArray, ::Type{T}, itr, ::HasShape, axs) where {T} = + similar(c, T, axs) + +# make a collection appropriate for collecting `itr::Generator` +_array_for(::Type{T}, ::SizeUnknown, ::Nothing) where {T} = Vector{T}(undef, 0) +_array_for(::Type{T}, ::HasLength, len::Integer) where {T} = Vector{T}(undef, Int(len)) +_array_for(::Type{T}, ::HasShape{N}, axs) where {T,N} = similar(Array{T,N}, axs) + +# used by syntax lowering for simple typed comprehensions +_array_for(::Type{T}, itr, isz) where {T} = _array_for(T, isz, _similar_shape(itr, isz)) + """ collect(collection) @@ -608,6 +686,8 @@ Return an `Array` of all items in a collection or iterator. For dictionaries, re [`HasShape`](@ref IteratorSize) trait, the result will have the same shape and number of dimensions as the argument. +Used by comprehensions to turn a generator into an `Array`. + # Examples ```jldoctest julia> collect(1:2:13) @@ -619,6 +699,13 @@ julia> collect(1:2:13) 9 11 13 + +julia> [x^2 for x in 1:8 if isodd(x)] +4-element Vector{Int64}: + 1 + 9 + 25 + 49 ``` """ collect(itr) = _collect(1:1 #= Array =#, itr, IteratorEltype(itr), IteratorSize(itr)) @@ -628,10 +715,10 @@ collect(A::AbstractArray) = _collect_indices(axes(A), A) collect_similar(cont, itr) = _collect(cont, itr, IteratorEltype(itr), IteratorSize(itr)) _collect(cont, itr, ::HasEltype, isz::Union{HasLength,HasShape}) = - copyto!(_similar_for(cont, eltype(itr), itr, isz), itr) + copyto!(_similar_for(cont, eltype(itr), itr, isz, _similar_shape(itr, isz)), itr) function _collect(cont, itr, ::HasEltype, isz::SizeUnknown) - a = _similar_for(cont, eltype(itr), itr, isz) + a = _similar_for(cont, eltype(itr), itr, isz, nothing) for x in itr push!(a,x) end @@ -646,6 +733,20 @@ function _collect_indices(indsA, A) copyto!(B, CartesianIndices(axes(B)), A, CartesianIndices(indsA)) end +# NOTE: this function is not meant to be called, only inferred, for the +# purpose of bounding the types of values generated by an iterator. +function _iterator_upper_bound(itr) + x = iterate(itr) + while x !== nothing + val = getfield(x, 1) + if inferencebarrier(nothing) + return val + end + x = iterate(itr, getfield(x, 2)) + end + throw(nothing) +end + # define this as a macro so that the call to Core.Compiler # gets inlined into the caller before recursion detection # gets a chance to see it, so that recursive calls to the caller @@ -655,10 +756,11 @@ if isdefined(Core, :Compiler) I = esc(itr) return quote if $I isa Generator && ($I).f isa Type - ($I).f + T = ($I).f else - Core.Compiler.return_type(first, Tuple{typeof($I)}) + T = Core.Compiler.return_type(_iterator_upper_bound, Tuple{typeof($I)}) end + promote_typejoin_union(T) end end else @@ -666,7 +768,7 @@ else I = esc(itr) return quote if $I isa Generator && ($I).f isa Type - ($I).f + promote_typejoin_union($I.f) else Any end @@ -674,34 +776,44 @@ else end end -_array_for(::Type{T}, itr, ::HasLength) where {T} = Vector{T}(undef, Int(length(itr)::Integer)) -_array_for(::Type{T}, itr, ::HasShape{N}) where {T,N} = similar(Array{T,N}, axes(itr)) - function collect(itr::Generator) isz = IteratorSize(itr.iter) et = @default_eltype(itr) if isa(isz, SizeUnknown) return grow_to!(Vector{et}(), itr) else + shp = _similar_shape(itr, isz) y = iterate(itr) if y === nothing - return _array_for(et, itr.iter, isz) + return _array_for(et, isz, shp) end v1, st = y - collect_to_with_first!(_array_for(typeof(v1), itr.iter, isz), v1, itr, st) + dest = _array_for(typeof(v1), isz, shp) + # The typeassert gives inference a helping hand on the element type and dimensionality + # (work-around for #28382) + et′ = et <: Type ? Type : et + RT = dest isa AbstractArray ? AbstractArray{<:et′, ndims(dest)} : Any + collect_to_with_first!(dest, v1, itr, st)::RT end end _collect(c, itr, ::EltypeUnknown, isz::SizeUnknown) = - grow_to!(_similar_for(c, @default_eltype(itr), itr, isz), itr) + grow_to!(_similar_for(c, @default_eltype(itr), itr, isz, nothing), itr) function _collect(c, itr, ::EltypeUnknown, isz::Union{HasLength,HasShape}) + et = @default_eltype(itr) + shp = _similar_shape(itr, isz) y = iterate(itr) if y === nothing - return _similar_for(c, @default_eltype(itr), itr, isz) + return _similar_for(c, et, itr, isz, shp) end v1, st = y - collect_to_with_first!(_similar_for(c, typeof(v1), itr, isz), v1, itr, st) + dest = _similar_for(c, typeof(v1), itr, isz, shp) + # The typeassert gives inference a helping hand on the element type and dimensionality + # (work-around for #28382) + et′ = et <: Type ? Type : et + RT = dest isa AbstractArray ? AbstractArray{<:et′, ndims(dest)} : Any + collect_to_with_first!(dest, v1, itr, st)::RT end function collect_to_with_first!(dest::AbstractArray, v1, itr, st) @@ -716,7 +828,7 @@ function collect_to_with_first!(dest, v1, itr, st) end function setindex_widen_up_to(dest::AbstractArray{T}, el, i) where T - @_inline_meta + @inline new = similar(dest, promote_typejoin(T, typeof(el))) f = first(LinearIndices(dest)) copyto!(new, first(LinearIndices(new)), dest, f, i-f) @@ -732,8 +844,8 @@ function collect_to!(dest::AbstractArray{T}, itr, offs, st) where T y = iterate(itr, st) y === nothing && break el, st = y - if el isa T || typeof(el) === T - @inbounds dest[i] = el::T + if el isa T + @inbounds dest[i] = el i += 1 else new = setindex_widen_up_to(dest, el, i) @@ -752,7 +864,7 @@ function grow_to!(dest, itr) end function push_widen(dest, el) - @_inline_meta + @inline new = sizehint!(empty(dest, promote_typejoin(eltype(dest), typeof(el))), length(dest)) if new isa AbstractSet # TODO: merge back these two branches when copy! is re-enabled for sets/vectors @@ -769,8 +881,8 @@ function grow_to!(dest, itr, st) y = iterate(itr, st) while y !== nothing el, st = y - if el isa T || typeof(el) === T - push!(dest, el::T) + if el isa T + push!(dest, el) else new = push_widen(dest, el) return grow_to!(new, itr, st) @@ -782,7 +894,7 @@ end ## Iteration ## -iterate(A::Array, i=1) = (@_inline_meta; (i % UInt) - 1 < length(A) ? (@inbounds A[i], i + 1) : nothing) +iterate(A::Array, i=1) = (@inline; (i % UInt) - 1 < length(A) ? (@inbounds A[i], i + 1) : nothing) ## Indexing: getindex ## @@ -792,6 +904,8 @@ iterate(A::Array, i=1) = (@_inline_meta; (i % UInt) - 1 < length(A) ? (@inbounds Retrieve the value(s) stored at the given key or index within a collection. The syntax `a[i,j,...]` is converted by the compiler to `getindex(a, i, j, ...)`. +See also [`get`](@ref), [`keys`](@ref), [`eachindex`](@ref). + # Examples ```jldoctest julia> A = Dict("a" => 1, "b" => 2) @@ -805,21 +919,21 @@ julia> getindex(A, "a") """ function getindex end -# This is more complicated than it needs to be in order to get Win64 through bootstrap -@eval getindex(A::Array, i1::Int) = arrayref($(Expr(:boundscheck)), A, i1) -@eval getindex(A::Array, i1::Int, i2::Int, I::Int...) = (@_inline_meta; arrayref($(Expr(:boundscheck)), A, i1, i2, I...)) - -# Faster contiguous indexing using copyto! for UnitRange and Colon -function getindex(A::Array, I::UnitRange{Int}) - @_inline_meta +# Faster contiguous indexing using copyto! for AbstractUnitRange and Colon +function getindex(A::Array, I::AbstractUnitRange{<:Integer}) + @inline @boundscheck checkbounds(A, I) lI = length(I) - X = similar(A, lI) + X = similar(A, axes(I)) if lI > 0 - unsafe_copyto!(X, 1, A, first(I), lI) + copyto!(X, firstindex(X), A, first(I), lI) end return X end + +# getindex for carrying out logical indexing for AbstractUnitRange{Bool} as Bool <: Integer +getindex(a::Array, r::AbstractUnitRange{Bool}) = getindex(a, to_index(r)) + function getindex(A::Array, c::Colon) lI = length(A) X = similar(A, lI) @@ -846,7 +960,7 @@ function setindex! end @eval setindex!(A::Array{T}, x, i1::Int) where {T} = arrayset($(Expr(:boundscheck)), A, convert(T,x)::T, i1) @eval setindex!(A::Array{T}, x, i1::Int, i2::Int, I::Int...) where {T} = - (@_inline_meta; arrayset($(Expr(:boundscheck)), A, convert(T,x)::T, i1, i2, I...)) + (@inline; arrayset($(Expr(:boundscheck)), A, convert(T,x)::T, i1, i2, I...)) # This is redundant with the abstract fallbacks but needed and helpful for bootstrap function setindex!(A::Array, X::AbstractArray, I::AbstractVector{Int}) @@ -865,8 +979,8 @@ function setindex!(A::Array, X::AbstractArray, I::AbstractVector{Int}) end # Faster contiguous setindex! with copyto! -function setindex!(A::Array{T}, X::Array{T}, I::UnitRange{Int}) where T - @_inline_meta +function setindex!(A::Array{T}, X::Array{T}, I::AbstractUnitRange{Int}) where T + @inline @boundscheck checkbounds(A, I) lI = length(I) @boundscheck setindex_shape_check(X, lI) @@ -876,7 +990,7 @@ function setindex!(A::Array{T}, X::Array{T}, I::UnitRange{Int}) where T return A end function setindex!(A::Array{T}, X::Array{T}, c::Colon) where T - @_inline_meta + @inline lI = length(A) @boundscheck setindex_shape_check(X, lI) if lI > 0 @@ -926,6 +1040,10 @@ julia> push!([1, 2, 3], 4, 5, 6) If `collection` is ordered, use [`append!`](@ref) to add all the elements of another collection to it. The result of the preceding example is equivalent to `append!([1, 2, 3], [4, 5, 6])`. For `AbstractSet` objects, [`union!`](@ref) can be used instead. + +See [`sizehint!`](@ref) for notes about the performance model. + +See also [`pushfirst!`](@ref). """ function push! end @@ -933,7 +1051,7 @@ function push!(a::Array{T,1}, item) where T # convert first so we don't grow the array if the assignment won't work itemT = convert(T, item) _growend!(a, 1) - a[end] = itemT + @inbounds a[end] = itemT return a end @@ -944,19 +1062,23 @@ function push!(a::Array{Any,1}, @nospecialize item) end """ - append!(collection, collection2) -> collection. + append!(collection, collections...) -> collection. -For an ordered container `collection`, add the elements of `collection2` to the end of it. +For an ordered container `collection`, add the elements of each `collections` +to the end of it. + +!!! compat "Julia 1.6" + Specifying multiple collections to be appended requires at least Julia 1.6. # Examples ```jldoctest -julia> append!([1],[2,3]) +julia> append!([1], [2, 3]) 3-element Vector{Int64}: 1 2 3 -julia> append!([1, 2, 3], [4, 5, 6]) +julia> append!([1, 2, 3], [4, 5], [6]) 6-element Vector{Int64}: 1 2 @@ -969,6 +1091,11 @@ julia> append!([1, 2, 3], [4, 5, 6]) Use [`push!`](@ref) to add individual items to `collection` which are not already themselves in another collection. The result of the preceding example is equivalent to `push!([1, 2, 3], 4, 5, 6)`. + +See [`sizehint!`](@ref) for notes about the performance model. + +See also [`vcat`](@ref) for vectors, [`union!`](@ref) for sets, +and [`prepend!`](@ref) and [`pushfirst!`](@ref) for the opposite order. """ function append!(a::Vector, items::AbstractVector) itemindices = eachindex(items) @@ -981,6 +1108,8 @@ end append!(a::AbstractVector, iter) = _append!(a, IteratorSize(iter), iter) push!(a::AbstractVector, iter...) = append!(a, iter) +append!(a::AbstractVector, iter...) = foldl(append!, iter, init=a) + function _append!(a, ::Union{HasLength,HasShape}, iter) n = length(a) i = lastindex(a) @@ -999,17 +1128,32 @@ function _append!(a, ::IteratorSize, iter) end """ - prepend!(a::Vector, items) -> collection + prepend!(a::Vector, collections...) -> collection -Insert the elements of `items` to the beginning of `a`. +Insert the elements of each `collections` to the beginning of `a`. + +When `collections` specifies multiple collections, order is maintained: +elements of `collections[1]` will appear leftmost in `a`, and so on. + +!!! compat "Julia 1.6" + Specifying multiple collections to be prepended requires at least Julia 1.6. # Examples ```jldoctest -julia> prepend!([3],[1,2]) +julia> prepend!([3], [1, 2]) 3-element Vector{Int64}: 1 2 3 + +julia> prepend!([6], [1, 2], [3, 4, 5]) +6-element Vector{Int64}: + 1 + 2 + 3 + 4 + 5 + 6 ``` """ function prepend! end @@ -1029,6 +1173,8 @@ end prepend!(a::Vector, iter) = _prepend!(a, IteratorSize(iter), iter) pushfirst!(a::Vector, iter...) = prepend!(a, iter) +prepend!(a::AbstractVector, iter...) = foldr((v, a) -> prepend!(a, v), iter, init=a) + function _prepend!(a, ::Union{HasLength,HasShape}, iter) require_one_based_indexing(a) n = length(iter) @@ -1093,9 +1239,22 @@ function resize!(a::Vector, nl::Integer) end """ - sizehint!(s, n) + sizehint!(s, n) -> s Suggest that collection `s` reserve capacity for at least `n` elements. This can improve performance. + +# Notes on the performance model + +For types that support `sizehint!`, + +1. `push!` and `append!` methods generally may (but are not required to) preallocate extra + storage. For types implemented in `Base`, they typically do, using a heuristic optimized for + a general use case. + +2. `sizehint!` may control this preallocation. Again, it typically does this for types in + `Base`. + +3. `empty!` is nearly costless (and O(1)) for types that support this kind of preallocation. """ function sizehint! end @@ -1108,7 +1267,10 @@ end pop!(collection) -> item Remove an item in `collection` and return it. If `collection` is an -ordered container, the last item is returned. +ordered container, the last item is returned; for unordered containers, +an arbitrary element is returned. + +See also: [`popfirst!`](@ref), [`popat!`](@ref), [`delete!`](@ref), [`deleteat!`](@ref), [`splice!`](@ref), and [`push!`](@ref). # Examples ```jldoctest @@ -1158,7 +1320,8 @@ Remove the item at the given `i` and return it. Subsequent items are shifted to fill the resulting gap. When `i` is not a valid index for `a`, return `default`, or throw an error if `default` is not specified. -See also [`deleteat!`](@ref) and [`splice!`](@ref). + +See also: [`pop!`](@ref), [`popfirst!`](@ref), [`deleteat!`](@ref), [`splice!`](@ref). !!! compat "Julia 1.5" This function is available as of Julia 1.5. @@ -1203,6 +1366,8 @@ end Insert one or more `items` at the beginning of `collection`. +This function is called `unshift` in many other programming languages. + # Examples ```jldoctest julia> pushfirst!([1, 2, 3, 4], 5, 6) @@ -1227,6 +1392,10 @@ end Remove the first `item` from `collection`. +This function is called `shift` in many other programming languages. + +See also: [`pop!`](@ref), [`popat!`](@ref), [`delete!`](@ref). + # Examples ```jldoctest julia> A = [1, 2, 3, 4, 5, 6] @@ -1265,16 +1434,19 @@ end Insert an `item` into `a` at the given `index`. `index` is the index of `item` in the resulting `a`. +See also: [`push!`](@ref), [`replace`](@ref), [`popat!`](@ref), [`splice!`](@ref). + # Examples ```jldoctest -julia> insert!([6, 5, 4, 2, 1], 4, 3) -6-element Vector{Int64}: - 6 - 5 - 4 - 3 - 2 +julia> insert!(Any[1:6;], 3, "here") +7-element Vector{Any}: 1 + 2 + "here" + 3 + 4 + 5 + 6 ``` """ function insert!(a::Array{T,1}, i::Integer, item) where T @@ -1292,6 +1464,8 @@ end Remove the item at the given `i` and return the modified `a`. Subsequent items are shifted to fill the resulting gap. +See also: [`delete!`](@ref), [`popat!`](@ref), [`splice!`](@ref). + # Examples ```jldoctest julia> deleteat!([6, 5, 4, 3, 2, 1], 2) @@ -1303,14 +1477,24 @@ julia> deleteat!([6, 5, 4, 3, 2, 1], 2) 1 ``` """ -deleteat!(a::Vector, i::Integer) = (_deleteat!(a, i, 1); a) - -function deleteat!(a::Vector, r::UnitRange{<:Integer}) - n = length(a) - isempty(r) || _deleteat!(a, first(r), length(r)) +function deleteat!(a::Vector, i::Integer) + i isa Bool && depwarn("passing Bool as an index is deprecated", :deleteat!) + _deleteat!(a, i, 1) return a end +function deleteat!(a::Vector, r::AbstractUnitRange{<:Integer}) + if eltype(r) === Bool + return invoke(deleteat!, Tuple{Vector, AbstractVector{Bool}}, a, r) + else + n = length(a) + f = first(r) + f isa Bool && depwarn("passing Bool as an index is deprecated", :deleteat!) + isempty(r) || _deleteat!(a, f, length(r)) + return a + end +end + """ deleteat!(a::Vector, inds) @@ -1345,6 +1529,23 @@ deleteat!(a::Vector, inds::AbstractVector) = _deleteat!(a, to_indices(a, (inds,) struct Nowhere; end push!(::Nowhere, _) = nothing +_growend!(::Nowhere, _) = nothing + +@inline function _push_deleted!(dltd, a::Vector, ind) + if @inbounds isassigned(a, ind) + push!(dltd, @inbounds a[ind]) + else + _growend!(dltd, 1) + end +end + +@inline function _copy_item!(a::Vector, p, q) + if @inbounds isassigned(a, q) + @inbounds a[p] = a[q] + else + _unsetindex!(a, p) + end +end function _deleteat!(a::Vector, inds, dltd=Nowhere()) n = length(a) @@ -1352,7 +1553,7 @@ function _deleteat!(a::Vector, inds, dltd=Nowhere()) y === nothing && return a (p, s) = y checkbounds(a, p) - push!(dltd, @inbounds a[p]) + _push_deleted!(dltd, a, p) q = p+1 while true y = iterate(inds, s) @@ -1366,14 +1567,14 @@ function _deleteat!(a::Vector, inds, dltd=Nowhere()) end end while q < i - @inbounds a[p] = a[q] + _copy_item!(a, p, q) p += 1; q += 1 end - push!(dltd, @inbounds a[i]) + _push_deleted!(dltd, a, i) q = i+1 end while q <= n - @inbounds a[p] = a[q] + _copy_item!(a, p, q) p += 1; q += 1 end _deleteend!(a, n-p+1) @@ -1386,7 +1587,7 @@ function deleteat!(a::Vector, inds::AbstractVector{Bool}) length(inds) == n || throw(BoundsError(a, inds)) p = 1 for (q, i) in enumerate(inds) - @inbounds a[p] = a[q] + _copy_item!(a, p, q) p += !i end _deleteend!(a, n-p+1) @@ -1403,6 +1604,8 @@ Subsequent items are shifted left to fill the resulting gap. If specified, replacement values from an ordered collection will be spliced in place of the removed item. +See also: [`replace`](@ref), [`delete!`](@ref), [`deleteat!`](@ref), [`pop!`](@ref), [`popat!`](@ref). + # Examples ```jldoctest julia> A = [6, 5, 4, 3, 2, 1]; splice!(A, 5) @@ -1469,7 +1672,7 @@ Remove items at specified indices, and return a collection containing the removed items. Subsequent items are shifted left to fill the resulting gaps. If specified, replacement values from an ordered collection will be spliced in -place of the removed items; in this case, `indices` must be a `UnitRange`. +place of the removed items; in this case, `indices` must be a `AbstractUnitRange`. To insert `replacement` before an index `n` without removing any items, use `splice!(collection, n:n-1, replacement)`. @@ -1477,6 +1680,9 @@ To insert `replacement` before an index `n` without removing any items, use !!! compat "Julia 1.5" Prior to Julia 1.5, `indices` must always be a `UnitRange`. +!!! compat "Julia 1.8" + Prior to Julia 1.8, `indices` must be a `UnitRange` if splicing in replacement values. + # Examples ```jldoctest julia> A = [-1, -2, -3, 5, 4, 3, -1]; splice!(A, 4:3, 2) @@ -1494,7 +1700,7 @@ julia> A -1 ``` """ -function splice!(a::Vector, r::UnitRange{<:Integer}, ins=_default_splice) +function splice!(a::Vector, r::AbstractUnitRange{<:Integer}, ins=_default_splice) v = a[r] m = length(ins) if m == 0 @@ -1552,7 +1758,7 @@ end reverse(v [, start=1 [, stop=length(v) ]] ) Return a copy of `v` reversed from start to stop. See also [`Iterators.reverse`](@ref) -for reverse-order iteration without making a copy. +for reverse-order iteration without making a copy, and in-place [`reverse!`](@ref). # Examples ```jldoctest @@ -1696,7 +1902,7 @@ function vcat(arrays::Vector{T}...) where T return arr end -_cat(n::Integer, x::Integer...) = reshape([x...], (ntuple(x->1, n-1)..., length(x))) +_cat(n::Integer, x::Integer...) = reshape([x...], (ntuple(Returns(1), n-1)..., length(x))) ## find ## @@ -1732,18 +1938,7 @@ julia> findnext(A, CartesianIndex(1, 1)) CartesianIndex(2, 1) ``` """ -function findnext(A, start) - l = last(keys(A)) - i = oftype(l, start) - i > l && return nothing - while true - A[i] && return i - i == l && break - # nextind(A, l) can throw/overflow - i = nextind(A, i) - end - return nothing -end +findnext(A, start) = findnext(identity, A, start) """ findfirst(A) @@ -1755,6 +1950,8 @@ To search for other kinds of values, pass a predicate as the first argument. Indices or keys are of the same type as those returned by [`keys(A)`](@ref) and [`pairs(A)`](@ref). +See also: [`findall`](@ref), [`findnext`](@ref), [`findlast`](@ref), [`searchsortedfirst`](@ref). + # Examples ```jldoctest julia> A = [false, false, true, false] @@ -1778,14 +1975,7 @@ julia> findfirst(A) CartesianIndex(2, 1) ``` """ -function findfirst(A) - for (i, a) in pairs(A) - if a - return i - end - end - return nothing -end +findfirst(A) = findfirst(identity, A) # Needed for bootstrap, and allows defining only an optimized findnext method findfirst(A::AbstractArray) = findnext(A, first(keys(A))) @@ -1877,7 +2067,7 @@ findfirst(p::Union{Fix2{typeof(isequal),Int},Fix2{typeof(==),Int}}, r::OneTo{Int 1 <= p.x <= r.stop ? p.x : nothing findfirst(p::Union{Fix2{typeof(isequal),T},Fix2{typeof(==),T}}, r::AbstractUnitRange) where {T<:Integer} = - first(r) <= p.x <= last(r) ? 1+Int(p.x - first(r)) : nothing + first(r) <= p.x <= last(r) ? firstindex(r) + Int(p.x - first(r)) : nothing function findfirst(p::Union{Fix2{typeof(isequal),T},Fix2{typeof(==),T}}, r::StepRange{T,S}) where {T,S} isempty(r) && return nothing @@ -1896,6 +2086,8 @@ or `nothing` if not found. Indices are of the same type as those returned by [`keys(A)`](@ref) and [`pairs(A)`](@ref). +See also: [`findnext`](@ref), [`findfirst`](@ref), [`findall`](@ref). + # Examples ```jldoctest julia> A = [false, false, true, true] @@ -1919,18 +2111,7 @@ julia> findprev(A, CartesianIndex(2, 1)) CartesianIndex(2, 1) ``` """ -function findprev(A, start) - f = first(keys(A)) - i = oftype(f, start) - i < f && return nothing - while true - A[i] && return i - i == f && break - # prevind(A, f) can throw/underflow - i = prevind(A, i) - end - return nothing -end +findprev(A, start) = findprev(identity, A, start) """ findlast(A) @@ -1941,6 +2122,8 @@ Return `nothing` if there is no `true` value in `A`. Indices or keys are of the same type as those returned by [`keys(A)`](@ref) and [`pairs(A)`](@ref). +See also: [`findfirst`](@ref), [`findprev`](@ref), [`findall`](@ref). + # Examples ```jldoctest julia> A = [true, false, true, false] @@ -1966,14 +2149,7 @@ julia> findlast(A) CartesianIndex(2, 1) ``` """ -function findlast(A) - for (i, a) in Iterators.reverse(pairs(A)) - if a - return i - end - end - return nothing -end +findlast(A) = findlast(identity, A) # Needed for bootstrap, and allows defining only an optimized findprev method findlast(A::AbstractArray) = findprev(A, last(keys(A))) @@ -2119,6 +2295,10 @@ julia> findall(x -> x >= 0, d) """ findall(testf::Function, A) = collect(first(p) for p in pairs(A) if testf(last(p))) +# Broadcasting is much faster for small testf, and computing +# integer indices from logical index using findall has a negligible cost +findall(testf::Function, A::AbstractArray) = findall(testf.(A)) + """ findall(A) @@ -2129,6 +2309,8 @@ To search for other kinds of values, pass a predicate as the first argument. Indices or keys are of the same type as those returned by [`keys(A)`](@ref) and [`pairs(A)`](@ref). +See also: [`findfirst`](@ref), [`searchsorted`](@ref). + # Examples ```jldoctest julia> A = [true, false, false, true] @@ -2160,6 +2342,7 @@ Int64[] function findall(A) collect(first(p) for p in pairs(A) if last(p)) end + # Allocating result upfront is faster (possible only when collection can be iterated twice) function findall(A::AbstractArray{Bool}) n = count(A) @@ -2178,140 +2361,6 @@ findall(x::Bool) = x ? [1] : Vector{Int}() findall(testf::Function, x::Number) = testf(x) ? [1] : Vector{Int}() findall(p::Fix2{typeof(in)}, x::Number) = x in p.x ? [1] : Vector{Int}() -""" - findmax(itr) -> (x, index) - -Return the maximum element of the collection `itr` and its index. If there are multiple -maximal elements, then the first one will be returned. -If any data element is `NaN`, this element is returned. -The result is in line with `max`. - -The collection must not be empty. - -# Examples -```jldoctest -julia> findmax([8,0.1,-9,pi]) -(8.0, 1) - -julia> findmax([1,7,7,6]) -(7, 2) - -julia> findmax([1,7,7,NaN]) -(NaN, 4) -``` -""" -findmax(a) = _findmax(a, :) - -function _findmax(a, ::Colon) - p = pairs(a) - y = iterate(p) - if y === nothing - throw(ArgumentError("collection must be non-empty")) - end - (mi, m), s = y - i = mi - while true - y = iterate(p, s) - y === nothing && break - m != m && break - (i, ai), s = y - if ai != ai || isless(m, ai) - m = ai - mi = i - end - end - return (m, mi) -end - -""" - findmin(itr) -> (x, index) - -Return the minimum element of the collection `itr` and its index. If there are multiple -minimal elements, then the first one will be returned. -If any data element is `NaN`, this element is returned. -The result is in line with `min`. - -The collection must not be empty. - -# Examples -```jldoctest -julia> findmin([8,0.1,-9,pi]) -(-9.0, 3) - -julia> findmin([7,1,1,6]) -(1, 2) - -julia> findmin([7,1,1,NaN]) -(NaN, 4) -``` -""" -findmin(a) = _findmin(a, :) - -function _findmin(a, ::Colon) - p = pairs(a) - y = iterate(p) - if y === nothing - throw(ArgumentError("collection must be non-empty")) - end - (mi, m), s = y - i = mi - while true - y = iterate(p, s) - y === nothing && break - m != m && break - (i, ai), s = y - if ai != ai || isless(ai, m) - m = ai - mi = i - end - end - return (m, mi) -end - -""" - argmax(itr) -> Integer - -Return the index of the maximum element in a collection. If there are multiple maximal -elements, then the first one will be returned. - -The collection must not be empty. - -# Examples -```jldoctest -julia> argmax([8,0.1,-9,pi]) -1 - -julia> argmax([1,7,7,6]) -2 - -julia> argmax([1,7,7,NaN]) -4 -``` -""" -argmax(a) = findmax(a)[2] - -""" - argmin(itr) -> Integer - -Return the index of the minimum element in a collection. If there are multiple minimal -elements, then the first one will be returned. - -The collection must not be empty. - -# Examples -```jldoctest -julia> argmin([8,0.1,-9,pi]) -3 - -julia> argmin([7,1,1,6]) -2 - -julia> argmin([7,1,1,NaN]) -4 -``` -""" -argmin(a) = findmin(a)[2] - # similar to Matlab's ismember """ indexin(a, b) @@ -2320,6 +2369,8 @@ Return an array containing the first index in `b` for each value in `a` that is a member of `b`. The output array contains `nothing` wherever `a` is not a member of `b`. +See also: [`sortperm`](@ref), [`findfirst`](@ref). + # Examples ```jldoctest julia> a = ['a', 'b', 'c', 'b', 'd', 'a']; @@ -2416,7 +2467,8 @@ function findall(pred::Fix2{typeof(in),<:Union{Array{<:Real},Real}}, x::Array{<: end # issorted fails for some element types so the method above has to be restricted # to element with isless/< defined. -findall(pred::Fix2{typeof(in)}, x::Union{AbstractArray, Tuple}) = _findin(x, pred.x) +findall(pred::Fix2{typeof(in)}, x::AbstractArray) = _findin(x, pred.x) +findall(pred::Fix2{typeof(in)}, x::Tuple) = _findin(x, pred.x) # Copying subregions function indcopy(sz::Dims, I::Vector) @@ -2452,6 +2504,8 @@ The function `f` is passed one argument. !!! compat "Julia 1.4" Support for `a` as a tuple requires at least Julia 1.4. +See also: [`filter!`](@ref), [`Iterators.filter`](@ref). + # Examples ```jldoctest julia> a = 1:10 @@ -2528,6 +2582,56 @@ function filter!(f, a::AbstractVector) return a end +""" + keepat!(a::Vector, inds) + keepat!(a::BitVector, inds) + +Remove the items at all the indices which are not given by `inds`, +and return the modified `a`. +Items which are kept are shifted to fill the resulting gaps. + +`inds` must be an iterator of sorted and unique integer indices. +See also [`deleteat!`](@ref). + +!!! compat "Julia 1.7" + This function is available as of Julia 1.7. + +# Examples +```jldoctest +julia> keepat!([6, 5, 4, 3, 2, 1], 1:2:5) +3-element Vector{Int64}: + 6 + 4 + 2 +``` +""" +keepat!(a::Vector, inds) = _keepat!(a, inds) + +""" + keepat!(a::Vector, m::AbstractVector{Bool}) + keepat!(a::BitVector, m::AbstractVector{Bool}) + +The in-place version of logical indexing `a = a[m]`. That is, `keepat!(a, m)` on +vectors of equal length `a` and `m` will remove all elements from `a` for which +`m` at the corresponding index is `false`. + +# Examples +```jldoctest +julia> a = [:a, :b, :c]; + +julia> keepat!(a, [true, false, true]) +2-element Vector{Symbol}: + :a + :c + +julia> a +2-element Vector{Symbol}: + :a + :c +``` +""" +keepat!(a::Vector, m::AbstractVector{Bool}) = _keepat!(a, m) + # set-like operators for vectors # These are moderately efficient, preserve order, and remove dupes. @@ -2561,19 +2665,27 @@ function _shrink!(shrinker!, v::AbstractVector, itrs) seen = Set{eltype(v)}() filter!(_grow_filter!(seen), v) shrinker!(seen, itrs...) - filter!(_in(seen), v) + filter!(in(seen), v) end intersect!(v::AbstractVector, itrs...) = _shrink!(intersect!, v, itrs) setdiff!( v::AbstractVector, itrs...) = _shrink!(setdiff!, v, itrs) -vectorfilter(f, v::AbstractVector) = filter(f, v) # TODO: do we want this special case? -vectorfilter(f, v) = [x for x in v if f(x)] +vectorfilter(T::Type, f, v) = T[x for x in v if f(x)] function _shrink(shrinker!, itr, itrs) - keep = shrinker!(Set(itr), itrs...) - vectorfilter(_shrink_filter!(keep), itr) + T = promote_eltype(itr, itrs...) + keep = shrinker!(Set{T}(itr), itrs...) + vectorfilter(T, _shrink_filter!(keep), itr) end intersect(itr, itrs...) = _shrink(intersect!, itr, itrs) setdiff( itr, itrs...) = _shrink(setdiff!, itr, itrs) + +function intersect(v::AbstractVector, r::AbstractRange) + T = promote_eltype(v, r) + common = Iterators.filter(in(r), v) + seen = Set{T}(common) + return vectorfilter(T, _shrink_filter!(seen), common) +end +intersect(r::AbstractRange, v::AbstractVector) = intersect(v, r) diff --git a/base/arraymath.jl b/base/arraymath.jl index e75e98bf9dd62c..62dc3772e49381 100644 --- a/base/arraymath.jl +++ b/base/arraymath.jl @@ -1,36 +1,5 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -## Unary operators ## - -""" - conj!(A) - -Transform an array to its complex conjugate in-place. - -See also [`conj`](@ref). - -# Examples -```jldoctest -julia> A = [1+im 2-im; 2+2im 3+im] -2×2 Matrix{Complex{Int64}}: - 1+1im 2-1im - 2+2im 3+1im - -julia> conj!(A); - -julia> A -2×2 Matrix{Complex{Int64}}: - 1-1im 2+1im - 2-2im 3-1im -``` -""" -conj!(A::AbstractArray{<:Number}) = (@inbounds broadcast!(conj, A, A); A) - -for f in (:-, :conj, :real, :imag) - @eval ($f)(A::AbstractArray) = broadcast_preserving_zero_d($f, A) -end - - ## Binary arithmetic operators ## for f in (:+, :-) diff --git a/base/arrayshow.jl b/base/arrayshow.jl index 1cb002db24f8f1..0d480b64bb32d4 100644 --- a/base/arrayshow.jl +++ b/base/arrayshow.jl @@ -57,15 +57,16 @@ Parameter `sep::Integer` is number of spaces to put between elements. Alignment is reported as a vector of (left,right) tuples, one for each column going across the screen. """ -function alignment(io::IO, X::AbstractVecOrMat, - rows::AbstractVector, cols::AbstractVector, - cols_if_complete::Integer, cols_otherwise::Integer, sep::Integer) - a = Tuple{Int, Int}[] +function alignment(io::IO, @nospecialize(X::AbstractVecOrMat), + rows::AbstractVector{T}, cols::AbstractVector{V}, + cols_if_complete::Integer, cols_otherwise::Integer, sep::Integer, + #= `size(X) may not infer, set this in caller =# ncols::Integer=size(X, 2)) where {T,V} + a = Tuple{T, V}[] for j in cols # need to go down each column one at a time l = r = 0 for i in rows # plumb down and see what largest element sizes are if isassigned(X,i,j) - aij = alignment(io, X[i,j]) + aij = alignment(io, X[i,j])::Tuple{Int,Int} else aij = undef_ref_alignment end @@ -78,7 +79,7 @@ function alignment(io::IO, X::AbstractVecOrMat, break end end - if 1 < length(a) < length(axes(X,2)) + if 1 < length(a) < ncols while sum(map(sum,a)) + sep*length(a) >= cols_otherwise pop!(a) end @@ -94,8 +95,9 @@ is specified as string sep. `print_matrix_row` will also respect compact output for elements. """ function print_matrix_row(io::IO, - X::AbstractVecOrMat, A::Vector, - i::Integer, cols::AbstractVector, sep::AbstractString) + @nospecialize(X::AbstractVecOrMat), A::Vector, + i::Integer, cols::AbstractVector, sep::AbstractString, + #= `axes(X)` may not infer, set this in caller =# idxlast::Integer=last(axes(X, 2))) for (k, j) = enumerate(cols) k > length(A) && break if isassigned(X,Int(i),Int(j)) # isassigned accepts only `Int` indices @@ -114,7 +116,7 @@ function print_matrix_row(io::IO, sx = undef_ref_str end l = repeat(" ", A[k][1]-a[1]) # pad on left and right as needed - r = j == axes(X, 2)[end] ? "" : repeat(" ", A[k][2]-a[2]) + r = j == idxlast ? "" : repeat(" ", A[k][2]-a[2]) prettysx = replace_in_print_matrix(X,i,j,sx) print(io, l, prettysx, r) if k < length(A); print(io, sep); end @@ -166,7 +168,12 @@ function print_matrix(io::IO, X::AbstractVecOrMat, vdots::AbstractString = "\u22ee", ddots::AbstractString = " \u22f1 ", hmod::Integer = 5, vmod::Integer = 5) + _print_matrix(io, inferencebarrier(X), pre, sep, post, hdots, vdots, ddots, hmod, vmod, unitrange(axes(X,1)), unitrange(axes(X,2))) +end + +function _print_matrix(io, @nospecialize(X::AbstractVecOrMat), pre, sep, post, hdots, vdots, ddots, hmod, vmod, rowsA, colsA) hmod, vmod = Int(hmod)::Int, Int(vmod)::Int + ncols, idxlast = length(colsA), last(colsA) if !(get(io, :limit, false)::Bool) screenheight = screenwidth = typemax(Int) else @@ -178,7 +185,6 @@ function print_matrix(io::IO, X::AbstractVecOrMat, postsp = "" @assert textwidth(hdots) == textwidth(ddots) sepsize = length(sep)::Int - rowsA, colsA = UnitRange{Int}(axes(X,1)), UnitRange{Int}(axes(X,2)) m, n = length(rowsA), length(colsA) # To figure out alignments, only need to look at as many rows as could # fit down screen. If screen has at least as many rows as A, look at A. @@ -187,33 +193,37 @@ function print_matrix(io::IO, X::AbstractVecOrMat, halfheight = div(screenheight,2) if m > screenheight rowsA = [rowsA[(0:halfheight-1) .+ firstindex(rowsA)]; rowsA[(end-div(screenheight-1,2)+1):end]] + else + rowsA = [rowsA;] end # Similarly for columns, only necessary to get alignments for as many # columns as could conceivably fit across the screen maxpossiblecols = div(screenwidth, 1+sepsize) if n > maxpossiblecols colsA = [colsA[(0:maxpossiblecols-1) .+ firstindex(colsA)]; colsA[(end-maxpossiblecols+1):end]] + else + colsA = [colsA;] end - A = alignment(io, X, rowsA, colsA, screenwidth, screenwidth, sepsize) + A = alignment(io, X, rowsA, colsA, screenwidth, screenwidth, sepsize, ncols) # Nine-slicing is accomplished using print_matrix_row repeatedly if m <= screenheight # rows fit vertically on screen if n <= length(A) # rows and cols fit so just print whole matrix in one piece for i in rowsA print(io, i == first(rowsA) ? pre : presp) - print_matrix_row(io, X,A,i,colsA,sep) + print_matrix_row(io, X,A,i,colsA,sep,idxlast) print(io, i == last(rowsA) ? post : postsp) if i != last(rowsA); println(io); end end else # rows fit down screen but cols don't, so need horizontal ellipsis c = div(screenwidth-length(hdots)::Int+1,2)+1 # what goes to right of ellipsis - Ralign = reverse(alignment(io, X, rowsA, reverse(colsA), c, c, sepsize)) # alignments for right + Ralign = reverse(alignment(io, X, rowsA, reverse(colsA), c, c, sepsize, ncols)) # alignments for right c = screenwidth - sum(map(sum,Ralign)) - (length(Ralign)-1)*sepsize - length(hdots)::Int - Lalign = alignment(io, X, rowsA, colsA, c, c, sepsize) # alignments for left of ellipsis + Lalign = alignment(io, X, rowsA, colsA, c, c, sepsize, ncols) # alignments for left of ellipsis for i in rowsA print(io, i == first(rowsA) ? pre : presp) - print_matrix_row(io, X,Lalign,i,colsA[1:length(Lalign)],sep) + print_matrix_row(io, X,Lalign,i,colsA[1:length(Lalign)],sep,idxlast) print(io, (i - first(rowsA)) % hmod == 0 ? hdots : repeat(" ", length(hdots)::Int)) - print_matrix_row(io, X, Ralign, i, (n - length(Ralign)) .+ colsA, sep) + print_matrix_row(io, X, Ralign, i, (n - length(Ralign)) .+ colsA, sep, idxlast) print(io, i == last(rowsA) ? post : postsp) if i != last(rowsA); println(io); end end @@ -222,7 +232,7 @@ function print_matrix(io::IO, X::AbstractVecOrMat, if n <= length(A) # rows don't fit, cols do, so only vertical ellipsis for i in rowsA print(io, i == first(rowsA) ? pre : presp) - print_matrix_row(io, X,A,i,colsA,sep) + print_matrix_row(io, X,A,i,colsA,sep,idxlast) print(io, i == last(rowsA) ? post : postsp) if i != rowsA[end] || i == rowsA[halfheight]; println(io); end if i == rowsA[halfheight] @@ -233,15 +243,15 @@ function print_matrix(io::IO, X::AbstractVecOrMat, end else # neither rows nor cols fit, so use all 3 kinds of dots c = div(screenwidth-length(hdots)::Int+1,2)+1 - Ralign = reverse(alignment(io, X, rowsA, reverse(colsA), c, c, sepsize)) + Ralign = reverse(alignment(io, X, rowsA, reverse(colsA), c, c, sepsize, ncols)) c = screenwidth - sum(map(sum,Ralign)) - (length(Ralign)-1)*sepsize - length(hdots)::Int - Lalign = alignment(io, X, rowsA, colsA, c, c, sepsize) + Lalign = alignment(io, X, rowsA, colsA, c, c, sepsize, ncols) r = mod((length(Ralign)-n+1),vmod) # where to put dots on right half for i in rowsA print(io, i == first(rowsA) ? pre : presp) - print_matrix_row(io, X,Lalign,i,colsA[1:length(Lalign)],sep) + print_matrix_row(io, X,Lalign,i,colsA[1:length(Lalign)],sep,idxlast) print(io, (i - first(rowsA)) % hmod == 0 ? hdots : repeat(" ", length(hdots)::Int)) - print_matrix_row(io, X,Ralign,i,(n-length(Ralign)).+colsA,sep) + print_matrix_row(io, X,Ralign,i,(n-length(Ralign)).+colsA,sep,idxlast) print(io, i == last(rowsA) ? post : postsp) if i != rowsA[end] || i == rowsA[halfheight]; println(io); end if i == rowsA[halfheight] @@ -264,14 +274,21 @@ end # typeinfo agnostic # n-dimensional arrays -function show_nd(io::IO, a::AbstractArray, print_matrix::Function, label_slices::Bool) +show_nd(io::IO, a::AbstractArray, print_matrix::Function, show_full::Bool) = + _show_nd(io, inferencebarrier(a), print_matrix, show_full, map(unitrange, axes(a))) + +function _show_nd(io::IO, @nospecialize(a::AbstractArray), print_matrix::Function, show_full::Bool, axs::Tuple{Vararg{AbstractUnitRange}}) limit::Bool = get(io, :limit, false) if isempty(a) return end - tailinds = tail(tail(axes(a))) + tailinds = tail(tail(axs)) nd = ndims(a)-2 - for I in CartesianIndices(tailinds) + show_full || print(io, "[") + Is = CartesianIndices(tailinds) + lastidxs = first(Is).I + reached_last_d = false + for I in Is idxs = I.I if limit for i = 1:nd @@ -280,14 +297,15 @@ function show_nd(io::IO, a::AbstractArray, print_matrix::Function, label_slices: if length(ind) > 10 if ii == ind[firstindex(ind)+3] && all(d->idxs[d]==first(tailinds[d]),1:i-1) for j=i+1:nd - szj = length(axes(a, j+2)) + szj = length(axs[j+2]) indj = tailinds[j] if szj>10 && first(indj)+2 < idxs[j] <= last(indj)-3 @goto skip end end - #println(io, idxs) - print(io, "...\n\n") + print(io, ";"^(i+2)) + print(io, " \u2026 ") + show_full && print(io, "\n\n") @goto skip end if ind[firstindex(ind)+2] < ii <= ind[end-3] @@ -296,18 +314,40 @@ function show_nd(io::IO, a::AbstractArray, print_matrix::Function, label_slices: end end end - if label_slices - print(io, "[:, :, ") - for i = 1:(nd-1); print(io, "$(idxs[i]), "); end - println(io, idxs[end], "] =") + if show_full + _show_nd_label(io, a, idxs) + end + slice = view(a, axs[1], axs[2], idxs...) + if show_full + print_matrix(io, slice) + print(io, idxs == map(last,tailinds) ? "" : "\n\n") + else + idxdiff = lastidxs .- idxs .< 0 + if any(idxdiff) + lastchangeindex = 2 + findlast(idxdiff) + print(io, ";"^lastchangeindex) + lastchangeindex == ndims(a) && (reached_last_d = true) + print(io, " ") + end + print_matrix(io, slice) end - slice = view(a, axes(a,1), axes(a,2), idxs...) - print_matrix(io, slice) - print(io, idxs == map(last,tailinds) ? "" : "\n\n") @label skip + lastidxs = idxs + end + if !show_full + reached_last_d || print(io, ";"^(nd+2)) + print(io, "]") end end +function _show_nd_label(io::IO, a::AbstractArray, idxs) + print(io, "[:, :, ") + for i = 1:length(idxs)-1 + print(io, idxs[i], ", ") + end + println(io, idxs[end], "] =") +end + # print_array: main helper functions for show(io, text/plain, array) # typeinfo agnostic # Note that this is for showing the content inside the array, and for `MIME"text/plain". @@ -370,14 +410,18 @@ end `_show_nonempty(io, X::AbstractMatrix, prefix)` prints matrix X with opening and closing square brackets, preceded by `prefix`, supposed to encode the type of the elements. """ -function _show_nonempty(io::IO, X::AbstractMatrix, prefix::String) +_show_nonempty(io::IO, X::AbstractMatrix, prefix::String) = + _show_nonempty(io, inferencebarrier(X), prefix, false, axes(X)) + +function _show_nonempty(io::IO, @nospecialize(X::AbstractMatrix), prefix::String, drop_brackets::Bool, axs::Tuple{AbstractUnitRange,AbstractUnitRange}) @assert !isempty(X) limit = get(io, :limit, false)::Bool - indr, indc = axes(X,1), axes(X,2) + indr, indc = axs nr, nc = length(indr), length(indc) rdots, cdots = false, false - rr1, rr2 = UnitRange{Int}(indr), 1:0 - cr1, cr2 = UnitRange{Int}(indc), 1:0 + rr1, rr2 = unitrange(indr), 1:0 + cr1 = unitrange(indc) + cr2 = first(cr1) .+ (0:-1) if limit if nr > 4 rr1, rr2 = rr1[1:2], rr1[nr-1:nr] @@ -388,7 +432,7 @@ function _show_nonempty(io::IO, X::AbstractMatrix, prefix::String) cdots = true end end - print(io, prefix, "[") + drop_brackets || print(io, prefix, "[") for rr in (rr1, rr2) for i in rr for cr in (cr1, cr2) @@ -408,14 +452,18 @@ function _show_nonempty(io::IO, X::AbstractMatrix, prefix::String) end end end - last(rr) != nr && rdots && print(io, "\u2026 ; ") + last(rr) != last(indr) && rdots && print(io, "\u2026 ; ") + end + if !drop_brackets + nc > 1 || print(io, ";;") + print(io, "]") end - print(io, "]") + return nothing end _show_nonempty(io::IO, X::AbstractArray, prefix::String) = - show_nd(io, X, (io, slice) -> _show_nonempty(io, slice, prefix), false) + show_nd(io, X, (io, slice) -> _show_nonempty(io, inferencebarrier(slice), prefix, true, axes(slice)), false) # a specific call path is used to show vectors (show_vector) _show_nonempty(::IO, ::AbstractVector, ::String) = diff --git a/base/asyncevent.jl b/base/asyncevent.jl index 234552e635e2c4..d3938bd66c8425 100644 --- a/base/asyncevent.jl +++ b/base/asyncevent.jl @@ -10,12 +10,14 @@ Create a async condition that wakes up tasks waiting for it when notified from C by a call to `uv_async_send`. Waiting tasks are woken with an error when the object is closed (by [`close`](@ref)). Use [`isopen`](@ref) to check whether it is still active. + +This provides an implicit acquire & release memory ordering between the sending and waiting threads. """ mutable struct AsyncCondition - handle::Ptr{Cvoid} + @atomic handle::Ptr{Cvoid} cond::ThreadSynchronizer - isopen::Bool - set::Bool + @atomic isopen::Bool + @atomic set::Bool function AsyncCondition() this = new(Libc.malloc(_sizeof_uv_async), ThreadSynchronizer(), true, false) @@ -43,10 +45,22 @@ the async condition object itself. """ function AsyncCondition(cb::Function) async = AsyncCondition() - @async while _trywait(async) + t = @task begin + unpreserve_handle(async) + while _trywait(async) cb(async) isopen(async) || return end + end + # here we are mimicking parts of _trywait, in coordination with task `t` + preserve_handle(async) + @lock async.cond begin + if async.set + schedule(t) + else + _wait2(async.cond, t) + end + end return async end @@ -57,22 +71,32 @@ end Create a timer that wakes up tasks waiting for it (by calling [`wait`](@ref) on the timer object). -Waiting tasks are woken after an initial delay of `delay` seconds, and then repeating with the given -`interval` in seconds. If `interval` is equal to `0`, the timer is only triggered once. When -the timer is closed (by [`close`](@ref)) waiting tasks are woken with an error. Use [`isopen`](@ref) -to check whether a timer is still active. +Waiting tasks are woken after an initial delay of at least `delay` seconds, and then repeating after +at least `interval` seconds again elapse. If `interval` is equal to `0`, the timer is only triggered +once. When the timer is closed (by [`close`](@ref)) waiting tasks are woken with an error. Use +[`isopen`](@ref) to check whether a timer is still active. + +!!! note + `interval` is subject to accumulating time skew. If you need precise events at a particular + absolute time, create a new timer at each expiration with the difference to the next time computed. + +!!! note + A `Timer` requires yield points to update its state. For instance, `isopen(t::Timer)` cannot be + used to timeout a non-yielding while loop. + """ mutable struct Timer - handle::Ptr{Cvoid} + @atomic handle::Ptr{Cvoid} cond::ThreadSynchronizer - isopen::Bool - set::Bool + @atomic isopen::Bool + @atomic set::Bool function Timer(timeout::Real; interval::Real = 0.0) timeout ≥ 0 || throw(ArgumentError("timer cannot have negative timeout of $timeout seconds")) interval ≥ 0 || throw(ArgumentError("timer cannot have negative repeat interval of $interval seconds")) - timeout = UInt64(round(timeout * 1000)) + 1 - interval = UInt64(round(interval * 1000)) + # libuv has a tendency to timeout 1 ms early, so we need +1 on the timeout (in milliseconds), unless it is zero + timeoutms = ceil(UInt64, timeout * 1000) + !iszero(timeout) + intervalms = ceil(UInt64, interval * 1000) loop = eventloop() this = new(Libc.malloc(_sizeof_uv_timer), ThreadSynchronizer(), true, false) @@ -84,7 +108,7 @@ mutable struct Timer ccall(:uv_update_time, Cvoid, (Ptr{Cvoid},), loop) err = ccall(:uv_timer_start, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, UInt64, UInt64), this, @cfunction(uv_timercb, Cvoid, (Ptr{Cvoid},)), - timeout, interval) + timeoutms, intervalms) @assert err == 0 iolock_end() return this @@ -96,7 +120,11 @@ unsafe_convert(::Type{Ptr{Cvoid}}, async::AsyncCondition) = async.handle function _trywait(t::Union{Timer, AsyncCondition}) set = t.set - if !set + if set + # full barrier now for AsyncCondition + t isa Timer || Core.Intrinsics.atomic_fence(:acquire_release) + else + t.isopen || return false t.handle == C_NULL && return false iolock_begin() set = t.set @@ -105,14 +133,12 @@ function _trywait(t::Union{Timer, AsyncCondition}) lock(t.cond) try set = t.set - if !set - if t.handle != C_NULL - iolock_end() - set = wait(t.cond) - unlock(t.cond) - iolock_begin() - lock(t.cond) - end + if !set && t.isopen && t.handle != C_NULL + iolock_end() + set = wait(t.cond) + unlock(t.cond) + iolock_begin() + lock(t.cond) end finally unlock(t.cond) @@ -121,7 +147,7 @@ function _trywait(t::Union{Timer, AsyncCondition}) end iolock_end() end - t.set = false + @atomic :monotonic t.set = false return set end @@ -131,12 +157,12 @@ function wait(t::Union{Timer, AsyncCondition}) end -isopen(t::Union{Timer, AsyncCondition}) = t.isopen +isopen(t::Union{Timer, AsyncCondition}) = t.isopen && t.handle != C_NULL function close(t::Union{Timer, AsyncCondition}) iolock_begin() - if t.handle != C_NULL && isopen(t) - t.isopen = false + if isopen(t) + @atomic :monotonic t.isopen = false ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), t) end iolock_end() @@ -148,12 +174,12 @@ function uvfinalize(t::Union{Timer, AsyncCondition}) lock(t.cond) try if t.handle != C_NULL - disassociate_julia_struct(t.handle) # not going to call the usual close hooks + disassociate_julia_struct(t.handle) # not going to call the usual close hooks anymore if t.isopen - t.isopen = false - ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), t) + @atomic :monotonic t.isopen = false + ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), t.handle) end - t.handle = C_NULL + @atomic :monotonic t.handle = C_NULL notify(t.cond, false) end finally @@ -166,9 +192,9 @@ end function _uv_hook_close(t::Union{Timer, AsyncCondition}) lock(t.cond) try - t.isopen = false - t.handle = C_NULL - notify(t.cond, t.set) + @atomic :monotonic t.isopen = false + Libc.free(@atomicswap :monotonic t.handle = C_NULL) + notify(t.cond, false) finally unlock(t.cond) end @@ -177,9 +203,9 @@ end function uv_asynccb(handle::Ptr{Cvoid}) async = @handle_as handle AsyncCondition - lock(async.cond) + lock(async.cond) # acquire barrier try - async.set = true + @atomic :release async.set = true notify(async.cond, true) finally unlock(async.cond) @@ -191,7 +217,7 @@ function uv_timercb(handle::Ptr{Cvoid}) t = @handle_as handle Timer lock(t.cond) try - t.set = true + @atomic :monotonic t.set = true if ccall(:uv_timer_get_repeat, UInt64, (Ptr{Cvoid},), t) == 0 # timer is stopped now close(t) @@ -219,18 +245,18 @@ end """ Timer(callback::Function, delay; interval = 0) -Create a timer that wakes up tasks waiting for it (by calling [`wait`](@ref) on the timer object) and -calls the function `callback`. +Create a timer that runs the function `callback` at each timer expiration. -Waiting tasks are woken and the function `callback` is called after an initial delay of `delay` seconds, -and then repeating with the given `interval` in seconds. If `interval` is equal to `0`, the timer -is only triggered once. The function `callback` is called with a single argument, the timer itself. -When the timer is closed (by [`close`](@ref)) waiting tasks are woken with an error. Use [`isopen`](@ref) -to check whether a timer is still active. +Waiting tasks are woken and the function `callback` is called after an initial delay of `delay` +seconds, and then repeating with the given `interval` in seconds. If `interval` is equal to `0`, the +callback is only run once. The function `callback` is called with a single argument, the timer +itself. Stop a timer by calling `close`. The `cb` may still be run one final time, if the timer has +already expired. # Examples -Here the first number is printed after a delay of two seconds, then the following numbers are printed quickly. +Here the first number is printed after a delay of two seconds, then the following numbers are +printed quickly. ```julia-repl julia> begin @@ -248,53 +274,56 @@ julia> begin """ function Timer(cb::Function, timeout::Real; interval::Real=0.0) timer = Timer(timeout, interval=interval) - @async while _trywait(timer) - cb(timer) + t = @task begin + unpreserve_handle(timer) + while _trywait(timer) + try + cb(timer) + catch err + write(stderr, "Error in Timer:\n") + showerror(stderr, err, catch_backtrace()) + return + end isopen(timer) || return end + end + # here we are mimicking parts of _trywait, in coordination with task `t` + preserve_handle(timer) + @lock timer.cond begin + if timer.set + schedule(t) + else + _wait2(timer.cond, t) + end + end return timer end """ - timedwait(callback::Function, timeout::Real; pollint::Real=0.1) + timedwait(testcb, timeout::Real; pollint::Real=0.1) -Waits until `callback` returns `true` or `timeout` seconds have passed, whichever is earlier. -`callback` is polled every `pollint` seconds. The minimum value for `timeout` and `pollint` -is `0.001`, that is, 1 millisecond. +Waits until `testcb()` returns `true` or `timeout` seconds have passed, whichever is earlier. +The test function is polled every `pollint` seconds. The minimum value for `pollint` is 0.001 seconds, +that is, 1 millisecond. Returns :ok or :timed_out """ -function timedwait(testcb::Function, timeout::Real; pollint::Real=0.1) +function timedwait(testcb, timeout::Real; pollint::Real=0.1) pollint >= 1e-3 || throw(ArgumentError("pollint must be ≥ 1 millisecond")) start = time_ns() ns_timeout = 1e9 * timeout - done = Channel(1) - function timercb(aw) - try - if testcb() - put!(done, (:ok, nothing)) - elseif (time_ns() - start) > ns_timeout - put!(done, (:timed_out, nothing)) - end - catch e - put!(done, (:error, CapturedException(e, catch_backtrace()))) - finally - isready(done) && close(aw) - end - nothing - end - - try - testcb() && return :ok - catch e - throw(CapturedException(e, catch_backtrace())) - end - t = Timer(timercb, pollint, interval = pollint) - ret, e = fetch(done) - close(t) + testcb() && return :ok - ret === :error && throw(e) - - return ret + t = Timer(pollint, interval=pollint) + while _trywait(t) # stop if we ever get closed + if testcb() + close(t) + return :ok + elseif (time_ns() - start) > ns_timeout + close(t) + break + end + end + return :timed_out end diff --git a/base/asyncmap.jl b/base/asyncmap.jl index 976ce6c7b85ca2..0b3678f6b4b9ba 100644 --- a/base/asyncmap.jl +++ b/base/asyncmap.jl @@ -15,7 +15,7 @@ up to 100 tasks will be used for concurrent mapping. `ntasks` can also be specified as a zero-arg function. In this case, the number of tasks to run in parallel is checked before processing every element and a new -task started if the value of `ntasks_func` is less than the current number +task started if the value of `ntasks_func` is greater than the current number of tasks. If `batch_size` is specified, the collection is processed in batch mode. `f` must @@ -236,7 +236,7 @@ function start_worker_task!(worker_tasks, exec_func, chnl, batch_size=nothing) end catch e close(chnl) - retval = e + retval = capture_exception(e, catch_backtrace()) end retval end @@ -305,20 +305,7 @@ end function iterate(itr::AsyncCollector) itr.ntasks = verify_ntasks(itr.enumerator, itr.ntasks) itr.batch_size = verify_batch_size(itr.batch_size) - if itr.batch_size !== nothing - exec_func = batch -> begin - # extract indices from the input tuple - batch_idxs = map(x->x[1], batch) - - # and the args tuple.... - batched_args = map(x->x[2], batch) - results = f(batched_args) - foreach(x -> (itr.results[batch_idxs[x[1]]] = x[2]), enumerate(results)) - end - else - exec_func = (i,args) -> (itr.results[i]=itr.f(args...)) - end chnl, worker_tasks = setup_chnl_and_tasks((i,args) -> (itr.results[i]=itr.f(args...)), itr.ntasks, itr.batch_size) return iterate(itr, AsyncCollectorState(chnl, worker_tasks)) end diff --git a/base/atomics.jl b/base/atomics.jl index 1a980eb6561ec1..e6d62c3fc807b2 100644 --- a/base/atomics.jl +++ b/base/atomics.jl @@ -335,7 +335,7 @@ const llvmtypes = IdDict{Any,String}( Int32 => "i32", UInt32 => "i32", Int64 => "i64", UInt64 => "i64", Int128 => "i128", UInt128 => "i128", - Float16 => "i16", # half + Float16 => "half", Float32 => "float", Float64 => "double", ) @@ -356,13 +356,13 @@ for typ in atomictypes rt = "$lt, $lt*" irt = "$ilt, $ilt*" @eval getindex(x::Atomic{$typ}) = - llvmcall($""" + GC.@preserve x llvmcall($""" %ptr = inttoptr i$WORD_SIZE %0 to $lt* %rv = load atomic $rt %ptr acquire, align $(gc_alignment(typ)) ret $lt %rv """, $typ, Tuple{Ptr{$typ}}, unsafe_convert(Ptr{$typ}, x)) @eval setindex!(x::Atomic{$typ}, v::$typ) = - llvmcall($""" + GC.@preserve x llvmcall($""" %ptr = inttoptr i$WORD_SIZE %0 to $lt* store atomic $lt %1, $lt* %ptr release, align $(gc_alignment(typ)) ret void @@ -371,7 +371,7 @@ for typ in atomictypes # Note: atomic_cas! succeeded (i.e. it stored "new") if and only if the result is "cmp" if typ <: Integer @eval atomic_cas!(x::Atomic{$typ}, cmp::$typ, new::$typ) = - llvmcall($""" + GC.@preserve x llvmcall($""" %ptr = inttoptr i$WORD_SIZE %0 to $lt* %rs = cmpxchg $lt* %ptr, $lt %1, $lt %2 acq_rel acquire %rv = extractvalue { $lt, i1 } %rs, 0 @@ -380,7 +380,7 @@ for typ in atomictypes unsafe_convert(Ptr{$typ}, x), cmp, new) else @eval atomic_cas!(x::Atomic{$typ}, cmp::$typ, new::$typ) = - llvmcall($""" + GC.@preserve x llvmcall($""" %iptr = inttoptr i$WORD_SIZE %0 to $ilt* %icmp = bitcast $lt %1 to $ilt %inew = bitcast $lt %2 to $ilt @@ -403,7 +403,7 @@ for typ in atomictypes if rmwop in arithmetic_ops && !(typ <: ArithmeticTypes) continue end if typ <: Integer @eval $fn(x::Atomic{$typ}, v::$typ) = - llvmcall($""" + GC.@preserve x llvmcall($""" %ptr = inttoptr i$WORD_SIZE %0 to $lt* %rv = atomicrmw $rmw $lt* %ptr, $lt %1 acq_rel ret $lt %rv @@ -411,7 +411,7 @@ for typ in atomictypes else rmwop === :xchg || continue @eval $fn(x::Atomic{$typ}, v::$typ) = - llvmcall($""" + GC.@preserve x llvmcall($""" %iptr = inttoptr i$WORD_SIZE %0 to $ilt* %ival = bitcast $lt %1 to $ilt %irv = atomicrmw $rmw $ilt* %iptr, $ilt %ival acq_rel diff --git a/base/baseext.jl b/base/baseext.jl index 75ef96caa94be3..8ebd599312453e 100644 --- a/base/baseext.jl +++ b/base/baseext.jl @@ -2,6 +2,17 @@ # extensions to Core types to add features in Base +""" + VecElement{T} + +A wrapper type that holds a single value of type `T`. When used in the context of an +`NTuple{N, VecElement{T}} where {T, N}` object, it provides a hint to the runtime +system to align that struct to be more amenable to vectorization optimization +opportunities. In `ccall`, such an NTuple in the type signature will also use the +vector register ABI, rather than the usual struct ABI. +""" +VecElement + # hook up VecElement constructor to Base.convert VecElement{T}(arg) where {T} = VecElement{T}(convert(T, arg)) convert(::Type{T}, arg::T) where {T<:VecElement} = arg diff --git a/base/binaryplatforms.jl b/base/binaryplatforms.jl index a14b147108fce2..e2dda00bf58e7b 100644 --- a/base/binaryplatforms.jl +++ b/base/binaryplatforms.jl @@ -1,78 +1,16 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + module BinaryPlatforms export AbstractPlatform, Platform, HostPlatform, platform_dlext, tags, arch, os, - os_version, libc, compiler_abi, libgfortran_version, libstdcxx_version, + os_version, libc, libgfortran_version, libstdcxx_version, cxxstring_abi, parse_dl_name_version, detect_libgfortran_version, detect_libstdcxx_version, detect_cxxstring_abi, call_abi, wordsize, triplet, select_platform, platforms_match, platform_name import .Libc.Libdl ### Submodule with information about CPU features -module CPUID - -export cpu_isa - -""" - ISA(features::Set{UInt32}) - -A structure which represents the Instruction Set Architecture (ISA) of a -computer. It holds the `Set` of features of the CPU. - -The numerical values of the features are automatically generated from the C -source code of Julia and stored in the `features_h.jl` Julia file. -""" -struct ISA - features::Set{UInt32} -end - -Base.:<=(a::ISA, b::ISA) = a.features <= b.features -Base.:<(a::ISA, b::ISA) = a.features < b.features -Base.isless(a::ISA, b::ISA) = a < b - -include("features_h.jl") - -# Keep in sync with `arch_march_isa_mapping`. -const ISAs_by_family = Dict( - "x86_64" => ( - # Source: https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html. - # Implicit in all sets, because always required: mmx, sse, sse2 - "x86_64" => ISA(Set{UInt32}()), - "core2" => ISA(Set((JL_X86_sse3, JL_X86_ssse3))), - "nehalem" => ISA(Set((JL_X86_sse3, JL_X86_ssse3, JL_X86_sse41, JL_X86_sse42, JL_X86_popcnt))), - "sandybridge" => ISA(Set((JL_X86_sse3, JL_X86_ssse3, JL_X86_sse41, JL_X86_sse42, JL_X86_popcnt, JL_X86_avx, JL_X86_aes, JL_X86_pclmul))), - "haswell" => ISA(Set((JL_X86_movbe, JL_X86_sse3, JL_X86_ssse3, JL_X86_sse41, JL_X86_sse42, JL_X86_popcnt, JL_X86_avx, JL_X86_avx2, JL_X86_aes, JL_X86_pclmul, JL_X86_fsgsbase, JL_X86_rdrnd, JL_X86_fma, JL_X86_bmi, JL_X86_bmi2, JL_X86_f16c))), - "skylake" => ISA(Set((JL_X86_movbe, JL_X86_sse3, JL_X86_ssse3, JL_X86_sse41, JL_X86_sse42, JL_X86_popcnt, JL_X86_avx, JL_X86_avx2, JL_X86_aes, JL_X86_pclmul, JL_X86_fsgsbase, JL_X86_rdrnd, JL_X86_fma, JL_X86_bmi, JL_X86_bmi2, JL_X86_f16c, JL_X86_rdseed, JL_X86_adx, JL_X86_prfchw, JL_X86_clflushopt, JL_X86_xsavec, JL_X86_xsaves))), - "skylake_avx512" => ISA(Set((JL_X86_movbe, JL_X86_sse3, JL_X86_ssse3, JL_X86_sse41, JL_X86_sse42, JL_X86_popcnt, JL_X86_pku, JL_X86_avx, JL_X86_avx2, JL_X86_aes, JL_X86_pclmul, JL_X86_fsgsbase, JL_X86_rdrnd, JL_X86_fma, JL_X86_bmi, JL_X86_bmi2, JL_X86_f16c, JL_X86_rdseed, JL_X86_adx, JL_X86_prfchw, JL_X86_clflushopt, JL_X86_xsavec, JL_X86_xsaves, JL_X86_avx512f, JL_X86_clwb, JL_X86_avx512vl, JL_X86_avx512bw, JL_X86_avx512dq, JL_X86_avx512cd))), - ), - "arm" => ( - "armv7l" => ISA(Set{UInt32}()), - "armv7l_neon" => ISA(Set((JL_AArch32_neon,))), - "armv7l_neon_vfp4" => ISA(Set((JL_AArch32_neon, JL_AArch32_vfp4))), - ), - "aarch64" => ( - # Implicit in all sets, because always required: fp, asimd - "armv8.0_a" => ISA(Set{UInt32}()), - "armv8.1_a" => ISA(Set((JL_AArch64_lse, JL_AArch64_crc, JL_AArch64_rdm))), - "armv8.2_a_crypto" => ISA(Set((JL_AArch64_lse, JL_AArch64_crc, JL_AArch64_rdm, JL_AArch64_aes, JL_AArch64_sha2))), - "armv8.4_a_crypto_sve" => ISA(Set((JL_AArch64_lse, JL_AArch64_crc, JL_AArch64_rdm, JL_AArch64_fp16fml, JL_AArch64_dotprod, JL_AArch64_aes, JL_AArch64_sha2, JL_AArch64_dotprod, JL_AArch64_sve))), - ), -) - -test_cpu_feature(feature::UInt32) = ccall(:jl_test_cpu_feature, Bool, (UInt32,), feature) -cpu_family() = String(Sys.ARCH) - -""" - cpu_isa() - -Return the [`ISA`](@ref) (instruction set architecture) of the current CPU. -""" -function cpu_isa() - all_features = last(last(get(ISAs_by_family, cpu_family(), "" => [ISA(Set{UInt32}())]))).features - return ISA(Set{UInt32}(feat for feat in all_features if test_cpu_feature(feat))) -end - -end # module CPUID - +include("cpuid.jl") using .CPUID # This exists to ease compatibility with old-style Platform objects @@ -102,29 +40,21 @@ struct Platform <: AbstractPlatform # The "compare strategy" allows selective overriding on how a tag is compared compare_strategies::Dict{String,Function} - function Platform(arch::String, os::String; + # Passing `tags` as a `Dict` avoids the need to infer different NamedTuple specializations + function Platform(arch::String, os::String, _tags::Dict{String}; validate_strict::Bool = false, - compare_strategies::Dict{String,<:Function} = Dict{String,Function}(), - kwargs...) + compare_strategies::Dict{String,<:Function} = Dict{String,Function}()) # A wee bit of normalization os = lowercase(os) - arch = lowercase(arch) - if arch ∈ ("amd64",) - arch = "x86_64" - elseif arch ∈ ("i386", "i586") - arch = "i686" - elseif arch ∈ ("arm",) - arch = "armv7l" - elseif arch ∈ ("ppc64le",) - arch = "powerpc64le" - end + arch = CPUID.normalize_arch(arch) tags = Dict{String,String}( "arch" => arch, "os" => os, ) - for (tag, value) in kwargs - tag = lowercase(string(tag)) + for (tag, value) in _tags + value = value::Union{String,VersionNumber,Nothing} + tag = lowercase(tag) if tag ∈ ("arch", "os") throw(ArgumentError("Cannot double-pass key $(tag)")) end @@ -139,17 +69,14 @@ struct Platform <: AbstractPlatform # doesn't parse nicely into a VersionNumber to persist, but if `validate_strict` is # set to `true`, it will cause an error later on. if tag ∈ ("libgfortran_version", "libstdcxx_version", "os_version") - normver(x::VersionNumber) = string(x) - function normver(str::AbstractString) - v = tryparse(VersionNumber, str) - if v === nothing - # If this couldn't be parsed as a VersionNumber, return the original. - return str + if isa(value, VersionNumber) + value = string(value) + elseif isa(value, String) + v = tryparse(VersionNumber, value) + if isa(v, VersionNumber) + value = string(v) end - # Otherwise, return the `string(VersionNumber(str))` version. - return normver(v) end - value = normver(value) end # Use `add_tag!()` to add the tag to our collection of tags @@ -184,6 +111,19 @@ struct Platform <: AbstractPlatform end end +# Keyword interface (to avoid inference of specialized NamedTuple methods, use the Dict interface for `tags`) +function Platform(arch::String, os::String; + validate_strict::Bool = false, + compare_strategies::Dict{String,<:Function} = Dict{String,Function}(), + kwargs...) + tags = Dict{String,Any}(String(tag)::String=>tagvalue(value) for (tag, value) in kwargs) + return Platform(arch, os, tags; validate_strict, compare_strategies) +end + +tagvalue(v::Union{String,VersionNumber,Nothing}) = v +tagvalue(v::Symbol) = String(v) +tagvalue(v::AbstractString) = convert(String, v)::String + # Simple tag insertion that performs a little bit of validation function add_tag!(tags::Dict{String,String}, tag::String, value::String) # I know we said only alphanumeric and dots, but let's be generous so that we can expand @@ -215,6 +155,20 @@ function Base.setindex!(p::AbstractPlatform, v::String, k::String) return p end +# Hash definition to ensure that it's stable +function Base.hash(p::Platform, h::UInt) + h += 0x506c6174666f726d % UInt + h = hash(p.tags, h) + h = hash(p.compare_strategies, h) + return h +end + +# Simple equality definition; for compatibility testing, use `platforms_match()` +function Base.:(==)(a::Platform, b::Platform) + return a.tags == b.tags && a.compare_strategies == b.compare_strategies +end + + # Allow us to easily serialize Platform objects function Base.repr(p::Platform; context=nothing) str = string( @@ -228,21 +182,21 @@ function Base.repr(p::Platform; context=nothing) ) end -# Simple equality definition; for compatibility testing, use `platforms_match()` -Base.:(==)(a::AbstractPlatform, b::AbstractPlatform) = tags(a) == tags(b) - -const ARCHITECTURE_FLAGS = Dict( - "x86_64" => ["x86_64", "avx", "avx2", "avx512"], - "i686" => ["prescott"], - "armv7l" => ["armv7l", "neon", "vfp4"], - "armv6l" => ["generic"], - "aarch64" => ["armv8", "thunderx2", "carmel"], - "powerpc64le" => ["generic"], -) +# Make showing the platform a bit more palatable +function Base.show(io::IO, p::Platform) + str = string(platform_name(p), " ", arch(p)) + # Add on all the other tags not covered by os/arch: + other_tags = sort(collect(filter(kv -> kv[1] ∉ ("os", "arch"), tags(p)))) + if !isempty(other_tags) + str = string(str, " {", join([string(k, "=", v) for (k, v) in other_tags], ", "), "}") + end + print(io, str) +end + function validate_tags(tags::Dict) throw_invalid_key(k) = throw(ArgumentError("Key \"$(k)\" cannot have value \"$(tags[k])\"")) # Validate `arch` - if tags["arch"] ∉ keys(ARCHITECTURE_FLAGS) + if tags["arch"] ∉ ("x86_64", "i686", "armv7l", "armv6l", "aarch64", "powerpc64le") throw_invalid_key("arch") end # Validate `os` @@ -292,24 +246,14 @@ function validate_tags(tags::Dict) throw_version_number("libgfortran_version") end - # Validate `libstdcxx_version` is a parsable `VersionNumber` - if "libstdcxx_version" in keys(tags) && tryparse(VersionNumber, tags["libstdcxx_version"]) === nothing - throw_version_number("libstdcxx_version") - end - # Validate `cxxstring_abi` is one of the two valid options: if "cxxstring_abi" in keys(tags) && tags["cxxstring_abi"] ∉ ("cxx03", "cxx11") throw_invalid_key("cxxstring_abi") end - # Validate `march` is one of our recognized microarchitectures for the architecture we're advertising - if "march" in keys(tags) && tags["march"] ∉ ARCHITECTURE_FLAGS[tags["arch"]] - throw(ArgumentError("\"march\" cannot have value \"$(tags["march"])\" for arch $(tags["arch"])")) - end - - # Validate `cuda` is a parsable `VersionNumber` - if "cuda" in keys(tags) && tryparse(VersionNumber, tags["cuda"]) === nothing - throw_version_number("cuda") + # Validate `libstdcxx_version` is a parsable `VersionNumber` + if "libstdcxx_version" in keys(tags) && tryparse(VersionNumber, tags["libstdcxx_version"]) === nothing + throw_version_number("libstdcxx_version") end end @@ -497,7 +441,7 @@ function VNorNothing(d::Dict, key) if v === nothing return nothing end - return VersionNumber(v) + return VersionNumber(v)::VersionNumber end """ @@ -568,7 +512,7 @@ julia> triplet(Platform("armv7l", "Linux"; libgfortran_version="3")) """ function triplet(p::AbstractPlatform) str = string( - arch(p), + arch(p)::Union{Symbol,String}, os_str(p), libc_str(p), call_abi_str(p), @@ -578,16 +522,16 @@ function triplet(p::AbstractPlatform) if libgfortran_version(p) !== nothing str = string(str, "-libgfortran", libgfortran_version(p).major) end - if libstdcxx_version(p) !== nothing - str = string(str, "-libstdcxx", libstdcxx_version(p).patch) - end if cxxstring_abi(p) !== nothing str = string(str, "-", cxxstring_abi(p)) end + if libstdcxx_version(p) !== nothing + str = string(str, "-libstdcxx", libstdcxx_version(p).patch) + end # Tack on all extra tags for (tag, val) in tags(p) - if tag ∈ ("os", "arch", "libc", "call_abi", "libgfortran_version", "libstdcxx_version", "cxxstring_abi") + if tag ∈ ("os", "arch", "libc", "call_abi", "libgfortran_version", "libstdcxx_version", "cxxstring_abi", "os_version") continue end str = string(str, "-", tag, "+", val) @@ -621,15 +565,19 @@ end # Helper functions for Linux and FreeBSD libc/abi mishmashes function libc_str(p::AbstractPlatform) - if libc(p) === nothing + lc = libc(p) + if lc === nothing return "" - elseif libc(p) === "glibc" + elseif lc === "glibc" return "-gnu" else - return string("-", libc(p)) + return string("-", lc) end end -call_abi_str(p::AbstractPlatform) = (call_abi(p) === nothing) ? "" : call_abi(p) +function call_abi_str(p::AbstractPlatform) + cabi = call_abi(p) + cabi === nothing ? "" : string(cabi::Union{Symbol,String}) +end Sys.isapple(p::AbstractPlatform) = os(p) == "macos" Sys.islinux(p::AbstractPlatform) = os(p) == "linux" @@ -646,28 +594,40 @@ const arch_mapping = Dict( "powerpc64le" => "p(ower)?pc64le", ) # Keep this in sync with `CPUID.ISAs_by_family` +# These are the CPUID side of the microarchitectures targeted by GCC flags in BinaryBuilder.jl const arch_march_isa_mapping = let function get_set(arch, name) all = CPUID.ISAs_by_family[arch] return all[findfirst(x -> x.first == name, all)].second end Dict( - "x86_64" => Dict{String,CPUID.ISA}( + "i686" => [ + "pentium4" => get_set("i686", "pentium4"), + "prescott" => get_set("i686", "prescott"), + ], + "x86_64" => [ "x86_64" => get_set("x86_64", "x86_64"), "avx" => get_set("x86_64", "sandybridge"), "avx2" => get_set("x86_64", "haswell"), "avx512" => get_set("x86_64", "skylake_avx512"), - ), - "armv7l" => Dict{String,CPUID.ISA}( - "armv7l" => get_set("arm", "armv7l"), - "neon" => get_set("arm", "armv7l_neon"), - "vfp4" => get_set("arm", "armv7l_neon_vfp4"), - ), - "aarch64" => Dict{String,CPUID.ISA}( - "armv8" => get_set("aarch64", "armv8.0_a"), - "thunderx2" => get_set("aarch64", "armv8.1_a"), - "carmel" => get_set("aarch64", "armv8.2_a_crypto"), - ), + ], + "armv6l" => [ + "arm1176jzfs" => get_set("armv6l", "arm1176jzfs"), + ], + "armv7l" => [ + "armv7l" => get_set("armv7l", "armv7l"), + "neonvfpv4" => get_set("armv7l", "armv7l+neon+vfpv4"), + ], + "aarch64" => [ + "armv8_0" => get_set("aarch64", "armv8.0-a"), + "armv8_1" => get_set("aarch64", "armv8.1-a"), + "armv8_2_crypto" => get_set("aarch64", "armv8.2-a+crypto"), + "a64fx" => get_set("aarch64", "a64fx"), + "apple_m1" => get_set("aarch64", "apple_m1"), + ], + "powerpc64le" => [ + "power8" => get_set("powerpc64le", "power8"), + ] ) end const os_mapping = Dict( @@ -692,23 +652,22 @@ const libgfortran_version_mapping = Dict( "libgfortran4" => "(-libgfortran4)|(-gcc7)", "libgfortran5" => "(-libgfortran5)|(-gcc8)", ) -const libstdcxx_version_mapping = Dict{String,String}( - "libstdcxx_nothing" => "", - # This is sadly easier than parsing out the digit directly - ("libstdcxx$(idx)" => "-libstdcxx$(idx)" for idx in 18:26)..., -) const cxxstring_abi_mapping = Dict( "cxxstring_nothing" => "", "cxx03" => "-cxx03", "cxx11" => "-cxx11", ) +const libstdcxx_version_mapping = Dict{String,String}( + "libstdcxx_nothing" => "", + "libstdcxx" => "-libstdcxx\\d+", +) """ parse(::Type{Platform}, triplet::AbstractString) Parses a string platform triplet back into a `Platform` object. """ -function Base.parse(::Type{Platform}, triplet::AbstractString; validate_strict::Bool = false) +function Base.parse(::Type{Platform}, triplet::String; validate_strict::Bool = false) # Helper function to collapse dictionary of mappings down into a regex of # named capture groups joined by "|" operators c(mapping) = string("(",join(["(?<$k>$v)" for (k, v) in mapping], "|"), ")") @@ -723,8 +682,8 @@ function Base.parse(::Type{Platform}, triplet::AbstractString; validate_strict:: c(call_abi_mapping), # Next, optional things, like libgfortran/libstdcxx/cxxstring abi c(libgfortran_version_mapping), - c(libstdcxx_version_mapping), c(cxxstring_abi_mapping), + c(libstdcxx_version_mapping), # Finally, the catch-all for extended tags "(?(?:-[^-]+\\+[^-]+)*)?", "\$", @@ -745,7 +704,7 @@ function Base.parse(::Type{Platform}, triplet::AbstractString; validate_strict:: if startswith(k, "libgfortran") return VersionNumber(parse(Int,k[12:end])) elseif startswith(k, "libstdcxx") - return VersionNumber(3, 4, parse(Int,k[10:end])) + return VersionNumber(3, 4, parse(Int,m[k][11:end])) else return k end @@ -754,21 +713,22 @@ function Base.parse(::Type{Platform}, triplet::AbstractString; validate_strict:: end # Extract the information we're interested in: + tags = Dict{String,Any}() arch = get_field(m, arch_mapping) os = get_field(m, os_mapping) - libc = get_field(m, libc_mapping) - call_abi = get_field(m, call_abi_mapping) - libgfortran_version = get_field(m, libgfortran_version_mapping) - libstdcxx_version = get_field(m, libstdcxx_version_mapping) - cxxstring_abi = get_field(m, cxxstring_abi_mapping) + tags["libc"] = get_field(m, libc_mapping) + tags["call_abi"] = get_field(m, call_abi_mapping) + tags["libgfortran_version"] = get_field(m, libgfortran_version_mapping) + tags["libstdcxx_version"] = get_field(m, libstdcxx_version_mapping) + tags["cxxstring_abi"] = get_field(m, cxxstring_abi_mapping) function split_tags(tagstr) - tag_fields = filter(!isempty, split(tagstr, "-")) + tag_fields = split(tagstr, "-"; keepempty=false) if isempty(tag_fields) return Pair{String,String}[] end - return map(v -> Symbol(v[1]) => v[2], split.(tag_fields, "+")) + return map(v -> String(v[1]) => String(v[2]), split.(tag_fields, "+")) end - tags = split_tags(m["tags"]) + merge!(tags, Dict(split_tags(m["tags"]))) # Special parsing of os version number, if any exists function extract_os_version(os_name, pattern) @@ -785,21 +745,14 @@ function Base.parse(::Type{Platform}, triplet::AbstractString; validate_strict:: if os == "freebsd" os_version = extract_os_version("freebsd", r".*freebsd([\d.]+)") end + tags["os_version"] = os_version - return Platform( - arch, os; - validate_strict, - libc, - call_abi, - libgfortran_version, - libstdcxx_version, - cxxstring_abi, - os_version, - tags..., - ) + return Platform(arch, os, tags; validate_strict) end throw(ArgumentError("Platform `$(triplet)` is not an officially supported platform")) end +Base.parse(::Type{Platform}, triplet::AbstractString; kwargs...) = + parse(Platform, convert(String, triplet)::String; kwargs...) function Base.tryparse(::Type{Platform}, triplet::AbstractString) try @@ -850,7 +803,7 @@ function parse_dl_name_version(path::String, os::String) dlregex = r"^(.*?)((?:\.[\d]+)*)\.dylib$" else # On Linux and FreeBSD, libraries look like `libnettle.so.6.3.0` - dlregex = r"^(.*?).so((?:\.[\d]+)*)$" + dlregex = r"^(.*?)\.so((?:\.[\d]+)*)$" end m = match(dlregex, basename(path)) @@ -904,12 +857,13 @@ function detect_libgfortran_version() end """ - detect_libstdcxx_version() + detect_libstdcxx_version(max_minor_version::Int=30) Inspects the currently running Julia process to find out what version of libstdc++ -it is linked against (if any). +it is linked against (if any). `max_minor_version` is the latest version in the +3.4 series of GLIBCXX where the search is performed. """ -function detect_libstdcxx_version() +function detect_libstdcxx_version(max_minor_version::Int=30) libstdcxx_paths = filter(x -> occursin("libstdc++", x), Libdl.dllist()) if isempty(libstdcxx_paths) # This can happen if we were built by clang, so we don't link against @@ -919,7 +873,9 @@ function detect_libstdcxx_version() # Brute-force our way through GLIBCXX_* symbols to discover which version we're linked against hdl = Libdl.dlopen(first(libstdcxx_paths)) - for minor_version in 26:-1:18 + # Try all GLIBCXX versions down to GCC v4.8: + # https://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html + for minor_version in max_minor_version:-1:18 if Libdl.dlsym(hdl, "GLIBCXX_3.4.$(minor_version)"; throw_error=false) !== nothing Libdl.dlclose(hdl) return VersionNumber("3.4.$(minor_version)") @@ -947,7 +903,7 @@ function detect_cxxstring_abi() end function open_libllvm(f::Function) - for lib_name in ("libLLVM", "LLVM", "libLLVMSupport") + for lib_name in ("libLLVM-13jl", "libLLVM", "LLVM", "libLLVMSupport") hdl = Libdl.dlopen_e(lib_name) if hdl != C_NULL try @@ -982,30 +938,36 @@ detect compiler ABI values such as `libgfortran_version`, `libstdcxx_version` an we have much of that built. """ function host_triplet() - str = Sys.MACHINE - libgfortran_version = detect_libgfortran_version() - if libgfortran_version !== nothing - str = string(str, "-libgfortran", libgfortran_version.major) + str = Base.BUILD_TRIPLET + + if !occursin("-libgfortran", str) + libgfortran_version = detect_libgfortran_version() + if libgfortran_version !== nothing + str = string(str, "-libgfortran", libgfortran_version.major) + end end - libstdcxx_version = detect_libstdcxx_version() - if libstdcxx_version !== nothing - str = string(str, "-libstdcxx", libstdcxx_version.patch) + if !occursin("-cxx", str) + cxxstring_abi = detect_cxxstring_abi() + if cxxstring_abi !== nothing + str = string(str, "-", cxxstring_abi) + end end - cxxstring_abi = detect_cxxstring_abi() - if cxxstring_abi !== nothing - str = string(str, "-", cxxstring_abi) + if !occursin("-libstdcxx", str) + libstdcxx_version = detect_libstdcxx_version() + if libstdcxx_version !== nothing + str = string(str, "-libstdcxx", libstdcxx_version.patch) + end end # Add on julia_version extended tag - str = string(str, "-julia_version+", VersionNumber(VERSION.major, VERSION.minor, VERSION.patch)) - + if !occursin("-julia_version+", str) + str = string(str, "-julia_version+", VersionNumber(VERSION.major, VERSION.minor, VERSION.patch)) + end return str end -# Cache the host platform value, and return it if someone asks for just `HostPlatform()`. -default_host_platform = HostPlatform(parse(Platform, host_triplet())) """ HostPlatform() @@ -1015,7 +977,7 @@ relevant comparison strategies set to host platform mode. This is equivalent to HostPlatform(parse(Platform, Base.BinaryPlatforms.host_triplet())) """ function HostPlatform() - return default_host_platform::Platform + return HostPlatform(parse(Platform, host_triplet()))::Platform end """ @@ -1039,7 +1001,7 @@ only available in macOS `v"10.11"` and later, or an artifact can state that it r a libstdc++ that is at least `v"3.4.22"`, etc... """ function platforms_match(a::AbstractPlatform, b::AbstractPlatform) - for k in union(keys(tags(a)), keys(tags(b))) + for k in union(keys(tags(a)::Dict{String,String}), keys(tags(b)::Dict{String,String})) ak = get(tags(a), k, nothing) bk = get(tags(b), k, nothing) @@ -1114,4 +1076,9 @@ function select_platform(download_info::Dict, platform::AbstractPlatform = HostP return download_info[p] end +# precompiles to reduce latency (see https://github.com/JuliaLang/julia/pull/43990#issuecomment-1025692379) +Dict{Platform,String}()[HostPlatform()] = "" +Platform("x86_64", "linux", Dict{String,Any}(); validate_strict=true) +Platform("x86_64", "linux", Dict{String,String}(); validate_strict=false) # called this way from Artifacts.unpack_platform + end # module diff --git a/base/bitarray.jl b/base/bitarray.jl index 51eff74a3f50f1..4494218172bf19 100644 --- a/base/bitarray.jl +++ b/base/bitarray.jl @@ -605,7 +605,7 @@ gen_bitarrayN(::Type{BitVector}, itsz, itr) = gen_bitarra gen_bitarrayN(::Type{BitVector}, itsz::HasShape{1}, itr) = gen_bitarray(itsz, itr) gen_bitarrayN(::Type{BitArray{N}}, itsz::HasShape{N}, itr) where N = gen_bitarray(itsz, itr) # The first of these is just for ambiguity resolution -gen_bitarrayN(::Type{BitVector}, itsz::HasShape{N}, itr) where N = throw(DimensionMismatch("cannot create a $T from a $N-dimensional iterator")) +gen_bitarrayN(::Type{BitVector}, itsz::HasShape{N}, itr) where N = throw(DimensionMismatch("cannot create a BitVector from a $N-dimensional iterator")) gen_bitarrayN(@nospecialize(T::Type), itsz::HasShape{N}, itr) where N = throw(DimensionMismatch("cannot create a $T from a $N-dimensional iterator")) gen_bitarrayN(@nospecialize(T::Type), itsz, itr) = throw(DimensionMismatch("cannot create a $T from a generic iterator")) @@ -703,7 +703,7 @@ end indexoffset(i) = first(i)-1 indexoffset(::Colon) = 0 -@propagate_inbounds function setindex!(B::BitArray, X::AbstractArray, J0::Union{Colon,UnitRange{Int}}) +@propagate_inbounds function setindex!(B::BitArray, X::AbstractArray, J0::Union{Colon,AbstractUnitRange{Int}}) _setindex!(IndexStyle(B), B, X, to_indices(B, (J0,))[1]) end @@ -947,6 +947,7 @@ function _deleteat!(B::BitVector, i::Int) end function deleteat!(B::BitVector, i::Integer) + i isa Bool && depwarn("passing Bool as an index is deprecated", :deleteat!) i = Int(i) n = length(B) 1 <= i <= n || throw(BoundsError(B, i)) @@ -954,7 +955,7 @@ function deleteat!(B::BitVector, i::Integer) return _deleteat!(B, i) end -function deleteat!(B::BitVector, r::UnitRange{Int}) +function deleteat!(B::BitVector, r::AbstractUnitRange{Int}) n = length(B) i_f = first(r) i_l = last(r) @@ -987,25 +988,27 @@ function deleteat!(B::BitVector, inds) (p, s) = y checkbounds(B, p) + p isa Bool && throw(ArgumentError("invalid index $p of type Bool")) q = p+1 new_l -= 1 y = iterate(inds, s) while y !== nothing (i, s) = y if !(q <= i <= n) + i isa Bool && throw(ArgumentError("invalid index $i of type Bool")) i < q && throw(ArgumentError("indices must be unique and sorted")) throw(BoundsError(B, i)) end new_l -= 1 if i > q - copy_chunks!(Bc, p, Bc, Int(q), Int(i-q)) + copy_chunks!(Bc, Int(p), Bc, Int(q), Int(i-q)) p += i-q end q = i+1 y = iterate(inds, s) end - q <= n && copy_chunks!(Bc, p, Bc, Int(q), Int(n-q+1)) + q <= n && copy_chunks!(Bc, Int(p), Bc, Int(q), Int(n-q+1)) delta_k = num_bit_chunks(new_l) - length(Bc) delta_k < 0 && _deleteend!(Bc, -delta_k) @@ -1019,7 +1022,55 @@ function deleteat!(B::BitVector, inds) return B end +function deleteat!(B::BitVector, inds::AbstractVector{Bool}) + length(inds) == length(B) || throw(BoundsError(B, inds)) + + n = new_l = length(B) + y = findfirst(inds) + y === nothing && return B + + Bc = B.chunks + + p = y + s = y + 1 + checkbounds(B, p) + q = p + 1 + new_l -= 1 + y = findnext(inds, s) + while y !== nothing + i = y + s = y + 1 + new_l -= 1 + if i > q + copy_chunks!(Bc, Int(p), Bc, Int(q), Int(i-q)) + p += i - q + end + q = i + 1 + y = findnext(inds, s) + end + + q <= n && copy_chunks!(Bc, Int(p), Bc, Int(q), Int(n - q + 1)) + + delta_k = num_bit_chunks(new_l) - length(Bc) + delta_k < 0 && _deleteend!(Bc, -delta_k) + + B.len = new_l + + if new_l > 0 + Bc[end] &= _msk_end(new_l) + end + + return B +end + +keepat!(B::BitVector, inds) = _keepat!(B, inds) +keepat!(B::BitVector, inds::AbstractVector{Bool}) = _keepat!(B, inds) + function splice!(B::BitVector, i::Integer) + # TODO: after deprecation remove the four lines below + # as v = B[i] is enough to do both bounds checking + # and Bool check then just pass Int(i) to _deleteat! + i isa Bool && depwarn("passing Bool as an index is deprecated", :splice!) i = Int(i) n = length(B) 1 <= i <= n || throw(BoundsError(B, i)) @@ -1031,9 +1082,11 @@ end const _default_bit_splice = BitVector() -function splice!(B::BitVector, r::Union{UnitRange{Int}, Integer}, ins::AbstractArray = _default_bit_splice) - _splice_int!(B, isa(r, UnitRange{Int}) ? r : Int(r), ins) +function splice!(B::BitVector, r::Union{AbstractUnitRange{Int}, Integer}, ins::AbstractArray = _default_bit_splice) + r isa Bool && depwarn("passing Bool as an index is deprecated", :splice!) + _splice_int!(B, isa(r, AbstractUnitRange{Int}) ? r : Int(r), ins) end + function _splice_int!(B::BitVector, r, ins) n = length(B) i_f, i_l = first(r), last(r) @@ -1073,7 +1126,7 @@ function _splice_int!(B::BitVector, r, ins) return v end -function splice!(B::BitVector, r::Union{UnitRange{Int}, Integer}, ins) +function splice!(B::BitVector, r::Union{AbstractUnitRange{Int}, Integer}, ins) Bins = BitVector(undef, length(ins)) i = 1 for x in ins @@ -1386,15 +1439,15 @@ circshift!(B::BitVector, i::Integer) = circshift!(B, B, i) ## count & find ## -function bitcount(Bc::Vector{UInt64}) - n = 0 +function bitcount(Bc::Vector{UInt64}; init::T=0) where {T} + n::T = init @inbounds for i = 1:length(Bc) - n += count_ones(Bc[i]) + n = (n + count_ones(Bc[i])) % T end return n end -count(B::BitArray) = bitcount(B.chunks) +_count(::typeof(identity), B::BitArray, ::Colon, init) = bitcount(B.chunks; init) function unsafe_bitfindnext(Bc::Vector{UInt64}, start::Int) chunk_start = _div64(start-1)+1 @@ -1707,6 +1760,8 @@ map!(::typeof(identity), dest::BitArray, A::BitArray) = copyto!(dest, A) for (T, f) in ((:(Union{typeof(&), typeof(*), typeof(min)}), :(&)), (:(Union{typeof(|), typeof(max)}), :(|)), (:(Union{typeof(xor), typeof(!=)}), :xor), + (:(typeof(nand)), :nand), + (:(typeof(nor)), :nor), (:(Union{typeof(>=), typeof(^)}), :((p, q) -> p | ~q)), (:(typeof(<=)), :((p, q) -> ~p | q)), (:(typeof(==)), :((p, q) -> ~xor(p, q))), @@ -1858,3 +1913,10 @@ function read!(s::IO, B::BitArray) end sizeof(B::BitArray) = sizeof(B.chunks) + +function _split_rest(a::Union{Vector, BitVector}, n::Int) + _check_length_split_rest(length(a), n) + last_n = a[end-n+1:end] + resize!(a, length(a) - n) + return a, last_n +end diff --git a/base/bitset.jl b/base/bitset.jl index 7b94e2e4745f44..0abd9d4b782d29 100644 --- a/base/bitset.jl +++ b/base/bitset.jl @@ -11,7 +11,7 @@ const NO_OFFSET = Int === Int64 ? -one(Int) << 60 : -one(Int) << 29 # a small optimization in the in(x, ::BitSet) method mutable struct BitSet <: AbstractSet{Int} - bits::Vector{UInt64} + const bits::Vector{UInt64} # 1st stored Int equals 64*offset offset::Int diff --git a/base/bool.jl b/base/bool.jl index 92a27543d2fbc1..7648df3e0250ea 100644 --- a/base/bool.jl +++ b/base/bool.jl @@ -14,6 +14,8 @@ typemax(::Type{Bool}) = true Boolean not. Implements [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic), returning [`missing`](@ref) if `x` is `missing`. +See also [`~`](@ref) for bitwise not. + # Examples ```jldoctest julia> !true @@ -70,6 +72,74 @@ julia> [true; true; false] .⊻ [true; false; false] """ xor(x::Bool, y::Bool) = (x != y) +""" + nand(x, y) + ⊼(x, y) + +Bitwise nand (not and) of `x` and `y`. Implements +[three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic), +returning [`missing`](@ref) if one of the arguments is `missing`. + +The infix operation `a ⊼ b` is a synonym for `nand(a,b)`, and +`⊼` can be typed by tab-completing `\\nand` or `\\barwedge` in the Julia REPL. + +# Examples +```jldoctest +julia> nand(true, false) +true + +julia> nand(true, true) +false + +julia> nand(true, missing) +missing + +julia> false ⊼ false +true + +julia> [true; true; false] .⊼ [true; false; false] +3-element BitVector: + 0 + 1 + 1 +``` +""" +nand(x...) = ~(&)(x...) + +""" + nor(x, y) + ⊽(x, y) + +Bitwise nor (not or) of `x` and `y`. Implements +[three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic), +returning [`missing`](@ref) if one of the arguments is `missing`. + +The infix operation `a ⊽ b` is a synonym for `nor(a,b)`, and +`⊽` can be typed by tab-completing `\\nor` or `\\barvee` in the Julia REPL. + +# Examples +```jldoctest +julia> nor(true, false) +false + +julia> nor(true, true) +false + +julia> nor(true, missing) +false + +julia> false ⊽ false +true + +julia> [true; true; false] .⊽ [true; false; false] +3-element BitVector: + 0 + 0 + 1 +``` +""" +nor(x...) = ~(|)(x...) + >>(x::Bool, c::UInt) = Int(x) >> c <<(x::Bool, c::UInt) = Int(x) << c >>>(x::Bool, c::UInt) = Int(x) >>> c diff --git a/base/boot.jl b/base/boot.jl index e653a82399ba52..bb7fcfd0719edc 100644 --- a/base/boot.jl +++ b/base/boot.jl @@ -8,7 +8,7 @@ #abstract type Vararg{T} end #mutable struct Symbol -# #opaque +## opaque #end #mutable struct TypeName @@ -53,28 +53,43 @@ #abstract type DenseArray{T,N} <: AbstractArray{T,N} end #mutable struct Array{T,N} <: DenseArray{T,N} +## opaque #end #mutable struct Module -# name::Symbol +## opaque +#end + +#mutable struct SimpleVector +## opaque +#end + +#mutable struct String +## opaque #end #mutable struct Method +#... #end #mutable struct MethodInstance +#... #end #mutable struct CodeInstance +#... #end #mutable struct CodeInfo +#... #end #mutable struct TypeMapLevel +#... #end #mutable struct TypeMapEntry +#... #end #abstract type Ref{T} end @@ -96,8 +111,8 @@ # module::Module # method::Symbol # file::Symbol -# line::Int -# inlined_at::Int +# line::Int32 +# inlined_at::Int32 #end #struct GotoNode @@ -156,7 +171,7 @@ export # key types Any, DataType, Vararg, NTuple, Tuple, Type, UnionAll, TypeVar, Union, Nothing, Cvoid, - AbstractArray, DenseArray, NamedTuple, + AbstractArray, DenseArray, NamedTuple, Pair, # special objects Function, Method, Module, Symbol, Task, Array, UndefInitializer, undef, WeakRef, VecElement, @@ -172,12 +187,15 @@ export InterruptException, InexactError, OutOfMemoryError, ReadOnlyMemoryError, OverflowError, StackOverflowError, SegmentationFault, UndefRefError, UndefVarError, TypeError, ArgumentError, MethodError, AssertionError, LoadError, InitError, - UndefKeywordError, + UndefKeywordError, ConcurrencyViolationError, # AST representation Expr, QuoteNode, LineNumberNode, GlobalRef, # object model functions - fieldtype, getfield, setfield!, nfields, throw, tuple, ===, isdefined, eval, ifelse, - # sizeof # not exported, to avoid conflicting with Base.sizeof + fieldtype, getfield, setfield!, swapfield!, modifyfield!, replacefield!, + nfields, throw, tuple, ===, isdefined, eval, + # access to globals + getglobal, setglobal!, + # ifelse, sizeof # not exported, to avoid conflicting with Base # type reflection <:, typeof, isa, typeassert, # method reflection @@ -185,7 +203,7 @@ export # constants nothing, Main -const getproperty = getfield +const getproperty = getfield # TODO: use `getglobal` for modules instead const setproperty! = setfield! abstract type Number end @@ -206,7 +224,7 @@ primitive type Char <: AbstractChar 32 end primitive type Int8 <: Signed 8 end #primitive type UInt8 <: Unsigned 8 end primitive type Int16 <: Signed 16 end -primitive type UInt16 <: Unsigned 16 end +#primitive type UInt16 <: Unsigned 16 end #primitive type Int32 <: Signed 32 end #primitive type UInt32 <: Unsigned 32 end #primitive type Int64 <: Signed 64 end @@ -227,14 +245,23 @@ ccall(:jl_toplevel_eval_in, Any, (Any, Any), (f::typeof(Typeof))(x) = ($(_expr(:meta,:nospecialize,:x)); isa(x,Type) ? Type{x} : typeof(x)) end) -# let the compiler assume that calling Union{} as a constructor does not need -# to be considered ever (which comes up often as Type{<:T}) -Union{}(a...) = throw(MethodError(Union{}, a)) macro nospecialize(x) _expr(:meta, :nospecialize, x) end +TypeVar(n::Symbol) = _typevar(n, Union{}, Any) +TypeVar(n::Symbol, @nospecialize(ub)) = _typevar(n, Union{}, ub) +TypeVar(n::Symbol, @nospecialize(lb), @nospecialize(ub)) = _typevar(n, lb, ub) + +UnionAll(v::TypeVar, @nospecialize(t)) = ccall(:jl_type_unionall, Any, (Any, Any), v, t) + +const Vararg = ccall(:jl_toplevel_eval_in, Any, (Any, Any), Core, _expr(:new, TypeofVararg)) + +# let the compiler assume that calling Union{} as a constructor does not need +# to be considered ever (which comes up often as Type{<:T}) +Union{}(a...) = throw(MethodError(Union{}, a)) + Expr(@nospecialize args...) = _expr(args...) abstract type Exception end @@ -242,20 +269,15 @@ struct ErrorException <: Exception msg::AbstractString end -macro _inline_meta() - Expr(:meta, :inline) -end - -macro _noinline_meta() - Expr(:meta, :noinline) -end +macro inline() Expr(:meta, :inline) end +macro noinline() Expr(:meta, :noinline) end struct BoundsError <: Exception a::Any i::Any BoundsError() = new() - BoundsError(@nospecialize(a)) = (@_noinline_meta; new(a)) - BoundsError(@nospecialize(a), i) = (@_noinline_meta; new(a,i)) + BoundsError(@nospecialize(a)) = (@noinline; new(a)) + BoundsError(@nospecialize(a), i) = (@noinline; new(a,i)) end struct DivideError <: Exception end struct OutOfMemoryError <: Exception end @@ -266,12 +288,15 @@ struct UndefRefError <: Exception end struct UndefVarError <: Exception var::Symbol end +struct ConcurrencyViolationError <: Exception + msg::AbstractString +end struct InterruptException <: Exception end struct DomainError <: Exception val msg::AbstractString - DomainError(@nospecialize(val)) = (@_noinline_meta; new(val, "")) - DomainError(@nospecialize(val), @nospecialize(msg)) = (@_noinline_meta; new(val, msg)) + DomainError(@nospecialize(val)) = (@noinline; new(val, "")) + DomainError(@nospecialize(val), @nospecialize(msg)) = (@noinline; new(val, msg)) end struct TypeError <: Exception # `func` is the name of the builtin function that encountered a type error, @@ -292,7 +317,7 @@ struct InexactError <: Exception func::Symbol T # Type val - InexactError(f::Symbol, @nospecialize(T), @nospecialize(val)) = (@_noinline_meta; new(f, T, val)) + InexactError(f::Symbol, @nospecialize(T), @nospecialize(val)) = (@noinline; new(f, T, val)) end struct OverflowError <: Exception msg::AbstractString @@ -363,12 +388,6 @@ mutable struct WeakRef (Ptr{Cvoid}, Any), getptls(), v) end -TypeVar(n::Symbol) = _typevar(n, Union{}, Any) -TypeVar(n::Symbol, @nospecialize(ub)) = _typevar(n, Union{}, ub) -TypeVar(n::Symbol, @nospecialize(lb), @nospecialize(ub)) = _typevar(n, lb, ub) - -UnionAll(v::TypeVar, @nospecialize(t)) = ccall(:jl_type_unionall, Any, (Any, Any), v, t) - Tuple{}() = () struct VecElement{T} @@ -377,39 +396,49 @@ struct VecElement{T} end VecElement(arg::T) where {T} = VecElement{T}(arg) -_new(typ::Symbol, argty::Symbol) = eval(Core, :($typ(@nospecialize n::$argty) = $(Expr(:new, typ, :n)))) -_new(:GotoNode, :Int) -_new(:NewvarNode, :SlotNumber) -_new(:QuoteNode, :Any) -_new(:SSAValue, :Int) -_new(:Argument, :Int) -_new(:ReturnNode, :Any) -eval(Core, :(ReturnNode() = $(Expr(:new, :ReturnNode)))) # unassigned val indicates unreachable -eval(Core, :(GotoIfNot(@nospecialize(cond), dest::Int) = $(Expr(:new, :GotoIfNot, :cond, :dest)))) -eval(Core, :(LineNumberNode(l::Int) = $(Expr(:new, :LineNumberNode, :l, nothing)))) -eval(Core, :(LineNumberNode(l::Int, @nospecialize(f)) = $(Expr(:new, :LineNumberNode, :l, :f)))) -LineNumberNode(l::Int, f::String) = LineNumberNode(l, Symbol(f)) -eval(Core, :(GlobalRef(m::Module, s::Symbol) = $(Expr(:new, :GlobalRef, :m, :s)))) -eval(Core, :(SlotNumber(n::Int) = $(Expr(:new, :SlotNumber, :n)))) -eval(Core, :(TypedSlot(n::Int, @nospecialize(t)) = $(Expr(:new, :TypedSlot, :n, :t)))) -eval(Core, :(PhiNode(edges::Array{Int32, 1}, values::Array{Any, 1}) = $(Expr(:new, :PhiNode, :edges, :values)))) -eval(Core, :(PiNode(val, typ) = $(Expr(:new, :PiNode, :val, :typ)))) -eval(Core, :(PhiCNode(values::Array{Any, 1}) = $(Expr(:new, :PhiCNode, :values)))) -eval(Core, :(UpsilonNode(val) = $(Expr(:new, :UpsilonNode, :val)))) -eval(Core, :(UpsilonNode() = $(Expr(:new, :UpsilonNode)))) -eval(Core, :(LineInfoNode(mod::Module, @nospecialize(method), file::Symbol, line::Int, inlined_at::Int) = - $(Expr(:new, :LineInfoNode, :mod, :method, :file, :line, :inlined_at)))) -eval(Core, :(CodeInstance(mi::MethodInstance, @nospecialize(rettype), @nospecialize(inferred_const), - @nospecialize(inferred), const_flags::Int32, - min_world::UInt, max_world::UInt) = - ccall(:jl_new_codeinst, Ref{CodeInstance}, (Any, Any, Any, Any, Int32, UInt, UInt), - mi, rettype, inferred_const, inferred, const_flags, min_world, max_world))) -eval(Core, :(Const(@nospecialize(v)) = $(Expr(:new, :Const, :v)))) -eval(Core, :(PartialStruct(@nospecialize(typ), fields::Array{Any, 1}) = $(Expr(:new, :PartialStruct, :typ, :fields)))) -eval(Core, :(MethodMatch(@nospecialize(spec_types), sparams::SimpleVector, method::Method, fully_covers::Bool) = - $(Expr(:new, :MethodMatch, :spec_types, :sparams, :method, :fully_covers)))) - -Module(name::Symbol=:anonymous, std_imports::Bool=true) = ccall(:jl_f_new_module, Ref{Module}, (Any, Bool), name, std_imports) +eval(Core, quote + GotoNode(label::Int) = $(Expr(:new, :GotoNode, :label)) + NewvarNode(slot::SlotNumber) = $(Expr(:new, :NewvarNode, :slot)) + QuoteNode(@nospecialize value) = $(Expr(:new, :QuoteNode, :value)) + SSAValue(id::Int) = $(Expr(:new, :SSAValue, :id)) + Argument(n::Int) = $(Expr(:new, :Argument, :n)) + ReturnNode(@nospecialize val) = $(Expr(:new, :ReturnNode, :val)) + ReturnNode() = $(Expr(:new, :ReturnNode)) # unassigned val indicates unreachable + GotoIfNot(@nospecialize(cond), dest::Int) = $(Expr(:new, :GotoIfNot, :cond, :dest)) + LineNumberNode(l::Int) = $(Expr(:new, :LineNumberNode, :l, nothing)) + function LineNumberNode(l::Int, @nospecialize(f)) + isa(f, String) && (f = Symbol(f)) + return $(Expr(:new, :LineNumberNode, :l, :f)) + end + LineInfoNode(mod::Module, @nospecialize(method), file::Symbol, line::Int32, inlined_at::Int32) = + $(Expr(:new, :LineInfoNode, :mod, :method, :file, :line, :inlined_at)) + GlobalRef(m::Module, s::Symbol) = $(Expr(:new, :GlobalRef, :m, :s)) + SlotNumber(n::Int) = $(Expr(:new, :SlotNumber, :n)) + TypedSlot(n::Int, @nospecialize(t)) = $(Expr(:new, :TypedSlot, :n, :t)) + PhiNode(edges::Array{Int32, 1}, values::Array{Any, 1}) = $(Expr(:new, :PhiNode, :edges, :values)) + PiNode(@nospecialize(val), @nospecialize(typ)) = $(Expr(:new, :PiNode, :val, :typ)) + PhiCNode(values::Array{Any, 1}) = $(Expr(:new, :PhiCNode, :values)) + UpsilonNode(@nospecialize(val)) = $(Expr(:new, :UpsilonNode, :val)) + UpsilonNode() = $(Expr(:new, :UpsilonNode)) + function CodeInstance( + mi::MethodInstance, @nospecialize(rettype), @nospecialize(inferred_const), + @nospecialize(inferred), const_flags::Int32, min_world::UInt, max_world::UInt, + ipo_effects::UInt32, effects::UInt32, @nospecialize(argescapes#=::Union{Nothing,Vector{ArgEscapeInfo}}=#), + relocatability::UInt8) + return ccall(:jl_new_codeinst, Ref{CodeInstance}, + (Any, Any, Any, Any, Int32, UInt, UInt, UInt32, UInt32, Any, UInt8), + mi, rettype, inferred_const, inferred, const_flags, min_world, max_world, + ipo_effects, effects, argescapes, + relocatability) + end + Const(@nospecialize(v)) = $(Expr(:new, :Const, :v)) + PartialStruct(@nospecialize(typ), fields::Array{Any, 1}) = $(Expr(:new, :PartialStruct, :typ, :fields)) + PartialOpaque(@nospecialize(typ), @nospecialize(env), parent::MethodInstance, source::Method) = $(Expr(:new, :PartialOpaque, :typ, :env, :parent, :source)) + InterConditional(slot::Int, @nospecialize(vtype), @nospecialize(elsetype)) = $(Expr(:new, :InterConditional, :slot, :vtype, :elsetype)) + MethodMatch(@nospecialize(spec_types), sparams::SimpleVector, method::Method, fully_covers::Bool) = $(Expr(:new, :MethodMatch, :spec_types, :sparams, :method, :fully_covers)) +end) + +Module(name::Symbol=:anonymous, std_imports::Bool=true, default_names::Bool=true) = ccall(:jl_f_new_module, Ref{Module}, (Any, Bool, Bool), name, std_imports, default_names) function _Task(@nospecialize(f), reserved_stack::Int, completion_future) return ccall(:jl_new_task, Ref{Task}, (Any, Any, Int), f, completion_future, reserved_stack) @@ -425,7 +454,6 @@ unsafe_convert(::Type{T}, x::T) where {T} = x const NTuple{N,T} = Tuple{Vararg{T,N}} - ## primitive Array constructors struct UndefInitializer end const undef = UndefInitializer() @@ -564,10 +592,11 @@ function (g::GeneratedFunctionStub)(@nospecialize args...) Expr(:meta, :push_loc, g.file, Symbol("@generated body")), Expr(:return, body), Expr(:meta, :pop_loc)))) - if g.spnames === nothing + spnames = g.spnames + if spnames === nothing return lam else - return Expr(Symbol("with-static-parameters"), lam, g.spnames...) + return Expr(Symbol("with-static-parameters"), lam, spnames...) end end @@ -584,26 +613,26 @@ eval(Core, :(NamedTuple{names,T}(args::T) where {names, T <: Tuple} = import .Intrinsics: eq_int, trunc_int, lshr_int, sub_int, shl_int, bitcast, sext_int, zext_int, and_int -throw_inexacterror(f::Symbol, ::Type{T}, val) where {T} = (@_noinline_meta; throw(InexactError(f, T, val))) +throw_inexacterror(f::Symbol, ::Type{T}, val) where {T} = (@noinline; throw(InexactError(f, T, val))) function is_top_bit_set(x) - @_inline_meta + @inline eq_int(trunc_int(UInt8, lshr_int(x, sub_int(shl_int(sizeof(x), 3), 1))), trunc_int(UInt8, 1)) end function is_top_bit_set(x::Union{Int8,UInt8}) - @_inline_meta + @inline eq_int(lshr_int(x, 7), trunc_int(typeof(x), 1)) end function check_top_bit(::Type{To}, x) where {To} - @_inline_meta + @inline is_top_bit_set(x) && throw_inexacterror(:check_top_bit, To, x) x end function checked_trunc_sint(::Type{To}, x::From) where {To,From} - @_inline_meta + @inline y = trunc_int(To, x) back = sext_int(From, y) eq_int(x, back) || throw_inexacterror(:trunc, To, x) @@ -611,7 +640,7 @@ function checked_trunc_sint(::Type{To}, x::From) where {To,From} end function checked_trunc_uint(::Type{To}, x::From) where {To,From} - @_inline_meta + @inline y = trunc_int(To, x) back = zext_int(From, y) eq_int(x, back) || throw_inexacterror(:trunc, To, x) @@ -764,15 +793,15 @@ Unsigned(x::Int64) = UInt64(x) Signed(x::UInt128) = Int128(x) Unsigned(x::Int128) = UInt128(x) -Signed(x::Union{Float32, Float64, Bool}) = Int(x) -Unsigned(x::Union{Float32, Float64, Bool}) = UInt(x) +Signed(x::Union{Float16, Float32, Float64, Bool}) = Int(x) +Unsigned(x::Union{Float16, Float32, Float64, Bool}) = UInt(x) Integer(x::Integer) = x -Integer(x::Union{Float32, Float64}) = Int(x) +Integer(x::Union{Float16, Float32, Float64}) = Int(x) # Binding for the julia parser, called as # -# Core._parse(text, filename, offset, options) +# Core._parse(text, filename, lineno, offset, options) # # Parse Julia code from the buffer `text`, starting at `offset` and attributing # it to `filename`. `text` may be a `String` or `svec(ptr::Ptr{UInt8}, @@ -785,4 +814,19 @@ Integer(x::Union{Float32, Float64}) = Int(x) # The internal jl_parse which will call into Core._parse if not `nothing`. _parse = nothing +# support for deprecated uses of internal _apply function +_apply(x...) = Core._apply_iterate(Main.Base.iterate, x...) + +struct Pair{A, B} + first::A + second::B + # if we didn't inline this, it's probably because the callsite was actually dynamic + # to avoid potentially compiling many copies of this, we mark the arguments with `@nospecialize` + # but also mark the whole function with `@inline` to ensure we will inline it whenever possible + # (even if `convert(::Type{A}, a::A)` for some reason was expensive) + Pair(a, b) = new{typeof(a), typeof(b)}(a, b) + Pair{A, B}(a::A, b::B) where {A, B} = new(a, b) + Pair{Any, Any}(@nospecialize(a::Any), @nospecialize(b::Any)) = new(a, b) +end + ccall(:jl_set_istopmod, Cvoid, (Any, Bool), Core, true) diff --git a/base/broadcast.jl b/base/broadcast.jl index 12bddaf531e274..7c32e6893268f0 100644 --- a/base/broadcast.jl +++ b/base/broadcast.jl @@ -8,10 +8,10 @@ Module containing the broadcasting implementation. module Broadcast using .Base.Cartesian -using .Base: Indices, OneTo, tail, to_shape, isoperator, promote_typejoin, - _msk_end, unsafe_bitgetindex, bitcache_chunks, bitcache_size, dumpbitcache, unalias +using .Base: Indices, OneTo, tail, to_shape, isoperator, promote_typejoin, promote_typejoin_union, + _msk_end, unsafe_bitgetindex, bitcache_chunks, bitcache_size, dumpbitcache, unalias, negate import .Base: copy, copyto!, axes -export broadcast, broadcast!, BroadcastStyle, broadcast_axes, broadcastable, dotview, @__dot__, broadcast_preserving_zero_d +export broadcast, broadcast!, BroadcastStyle, broadcast_axes, broadcastable, dotview, @__dot__, BroadcastFunction ## Computing the result's axes: deprecated name const broadcast_axes = axes @@ -137,7 +137,7 @@ BroadcastStyle(a::AbstractArrayStyle, ::Style{Tuple}) = a BroadcastStyle(::A, ::A) where A<:ArrayStyle = A() BroadcastStyle(::ArrayStyle, ::ArrayStyle) = Unknown() BroadcastStyle(::A, ::A) where A<:AbstractArrayStyle = A() -Base.@pure function BroadcastStyle(a::A, b::B) where {A<:AbstractArrayStyle{M},B<:AbstractArrayStyle{N}} where {M,N} +function BroadcastStyle(a::A, b::B) where {A<:AbstractArrayStyle{M},B<:AbstractArrayStyle{N}} where {M,N} if Base.typename(A) === Base.typename(B) return A(Val(max(M, N))) end @@ -179,6 +179,21 @@ function Broadcasted{Style}(f::F, args::Args, axes=nothing) where {Style, F, Arg Broadcasted{Style, typeof(axes), Core.Typeof(f), Args}(f, args, axes) end +struct AndAnd end +const andand = AndAnd() +broadcasted(::AndAnd, a, b) = broadcasted((a, b) -> a && b, a, b) +function broadcasted(::AndAnd, a, bc::Broadcasted) + bcf = flatten(bc) + broadcasted((a, args...) -> a && bcf.f(args...), a, bcf.args...) +end +struct OrOr end +const oror = OrOr() +broadcasted(::OrOr, a, b) = broadcasted((a, b) -> a || b, a, b) +function broadcasted(::OrOr, a, bc::Broadcasted) + bcf = flatten(bc) + broadcasted((a, args...) -> a || bcf.f(args...), a, bcf.args...) +end + Base.convert(::Type{Broadcasted{NewStyle}}, bc::Broadcasted{Style,Axes,F,Args}) where {NewStyle,Style,Axes,F,Args} = Broadcasted{NewStyle,Axes,F,Args}(bc.f, bc.args, bc.axes) @@ -515,7 +530,12 @@ axistype(a, b) = UnitRange{Int}(a) check_broadcast_shape(shp) = nothing check_broadcast_shape(shp, ::Tuple{}) = nothing check_broadcast_shape(::Tuple{}, ::Tuple{}) = nothing -check_broadcast_shape(::Tuple{}, Ashp::Tuple) = throw(DimensionMismatch("cannot broadcast array to have fewer dimensions")) +function check_broadcast_shape(::Tuple{}, Ashp::Tuple) + if any(ax -> length(ax) != 1, Ashp) + throw(DimensionMismatch("cannot broadcast array to have fewer non-singleton dimensions")) + end + nothing +end function check_broadcast_shape(shp, Ashp::Tuple) _bcsm(shp[1], Ashp[1]) || throw(DimensionMismatch("array could not be broadcast to match destination")) check_broadcast_shape(tail(shp), tail(Ashp)) @@ -546,18 +566,20 @@ an `Int`. """ Base.@propagate_inbounds newindex(arg, I::CartesianIndex) = CartesianIndex(_newindex(axes(arg), I.I)) Base.@propagate_inbounds newindex(arg, I::Integer) = CartesianIndex(_newindex(axes(arg), (I,))) -Base.@propagate_inbounds _newindex(ax::Tuple, I::Tuple) = (ifelse(Base.unsafe_length(ax[1])==1, ax[1][1], I[1]), _newindex(tail(ax), tail(I))...) +Base.@propagate_inbounds _newindex(ax::Tuple, I::Tuple) = (ifelse(length(ax[1]) == 1, ax[1][1], I[1]), _newindex(tail(ax), tail(I))...) Base.@propagate_inbounds _newindex(ax::Tuple{}, I::Tuple) = () Base.@propagate_inbounds _newindex(ax::Tuple, I::Tuple{}) = (ax[1][1], _newindex(tail(ax), ())...) Base.@propagate_inbounds _newindex(ax::Tuple{}, I::Tuple{}) = () # If dot-broadcasting were already defined, this would be `ifelse.(keep, I, Idefault)`. @inline newindex(I::CartesianIndex, keep, Idefault) = CartesianIndex(_newindex(I.I, keep, Idefault)) -@inline newindex(i::Integer, keep::Tuple{Bool}, idefault) = ifelse(keep[1], i, idefault[1]) +@inline newindex(i::Integer, keep::Tuple, idefault) = ifelse(keep[1], i, idefault[1]) @inline newindex(i::Integer, keep::Tuple{}, idefault) = CartesianIndex(()) @inline _newindex(I, keep, Idefault) = (ifelse(keep[1], I[1], Idefault[1]), _newindex(tail(I), tail(keep), tail(Idefault))...) @inline _newindex(I, keep::Tuple{}, Idefault) = () # truncate if keep is shorter than I +@inline _newindex(I::Tuple{}, keep, Idefault) = () # or I is shorter +@inline _newindex(I::Tuple{}, keep::Tuple{}, Idefault) = () # or both # newindexer(A) generates `keep` and `Idefault` (for use by `newindex` above) # for a particular array `A`; `shapeindexer` does so for its axes. @@ -675,9 +697,9 @@ julia> Broadcast.broadcastable("hello") # Strings break convention of matching i Base.RefValue{String}("hello") ``` """ -broadcastable(x::Union{Symbol,AbstractString,Function,UndefInitializer,Nothing,RoundingMode,Missing,Val,Ptr,Regex,Pair}) = Ref(x) +broadcastable(x::Union{Symbol,AbstractString,Function,UndefInitializer,Nothing,RoundingMode,Missing,Val,Ptr,AbstractPattern,Pair,IO}) = Ref(x) broadcastable(::Type{T}) where {T} = Ref{Type{T}}(T) -broadcastable(x::Union{AbstractArray,Number,Ref,Tuple,Broadcasted}) = x +broadcastable(x::Union{AbstractArray,Number,AbstractChar,Ref,Tuple,Broadcasted}) = x # Default to collecting iterables — which will error for non-iterables broadcastable(x) = collect(x) broadcastable(::Union{AbstractDict, NamedTuple}) = throw(ArgumentError("broadcasting over dictionaries and `NamedTuple`s is reserved")) @@ -692,7 +714,8 @@ eltypes(t::Tuple{Any,Any}) = Tuple{_broadcast_getindex_eltype(t[1]), _broadcast_ eltypes(t::Tuple) = Tuple{_broadcast_getindex_eltype(t[1]), eltypes(tail(t)).types...} # Inferred eltype of result of broadcast(f, args...) -combine_eltypes(f, args::Tuple) = Base._return_type(f, eltypes(args)) +combine_eltypes(f, args::Tuple) = + promote_typejoin_union(Base._return_type(f, eltypes(args))) ## Broadcasting core @@ -877,7 +900,11 @@ const NonleafHandlingStyles = Union{DefaultArrayStyle,ArrayConflict} dest = similar(bc′, typeof(val)) @inbounds dest[I] = val # Now handle the remaining values - return copyto_nonleaf!(dest, bc′, iter, state, 1) + # The typeassert gives inference a helping hand on the element type and dimensionality + # (work-around for #28382) + ElType′ = ElType === Union{} ? Any : ElType <: Type ? Type : ElType + RT = dest isa AbstractArray ? AbstractArray{<:ElType′, ndims(dest)} : Any + return copyto_nonleaf!(dest, bc′, iter, state, 1)::RT end ## general `copyto!` methods @@ -914,8 +941,8 @@ broadcast_unalias(::Nothing, src) = src preprocess(dest, x) = extrude(broadcast_unalias(dest, x)) @inline preprocess_args(dest, args::Tuple) = (preprocess(dest, args[1]), preprocess_args(dest, tail(args))...) -preprocess_args(dest, args::Tuple{Any}) = (preprocess(dest, args[1]),) -preprocess_args(dest, args::Tuple{}) = () +@inline preprocess_args(dest, args::Tuple{Any}) = (preprocess(dest, args[1]),) +@inline preprocess_args(dest, args::Tuple{}) = () # Specialize this method if all you want to do is specialize on typeof(dest) @inline function copyto!(dest::AbstractArray, bc::Broadcasted{Nothing}) @@ -928,8 +955,10 @@ preprocess_args(dest, args::Tuple{}) = () end end bc′ = preprocess(dest, bc) - @simd for I in eachindex(bc′) - @inbounds dest[I] = bc′[I] + # Performance may vary depending on whether `@inbounds` is placed outside the + # for loop or not. (cf. https://github.com/JuliaLang/julia/issues/38086) + @inbounds @simd for I in eachindex(bc′) + dest[I] = bc′[I] end return dest end @@ -944,14 +973,14 @@ end destc = dest.chunks cind = 1 bc′ = preprocess(dest, bc) - for P in Iterators.partition(eachindex(bc′), bitcache_size) + @inbounds for P in Iterators.partition(eachindex(bc′), bitcache_size) ind = 1 @simd for I in P - @inbounds tmp[ind] = bc′[I] + tmp[ind] = bc′[I] ind += 1 end @simd for i in ind:bitcache_size - @inbounds tmp[i] = false + tmp[i] = false end dumpbitcache(destc, cind, tmp) cind += bitcache_chunks @@ -1024,7 +1053,7 @@ function copyto_nonleaf!(dest, bc::Broadcasted, iter, state, count) y === nothing && break I, state = y @inbounds val = bc[I] - if val isa T || typeof(val) === T + if val isa T @inbounds dest[I] = val else # This element type doesn't fit in dest. Allocate a new dest with wider eltype, @@ -1048,19 +1077,22 @@ end ## scalar-range broadcast operations ## # DefaultArrayStyle and \ are not available at the time of range.jl -broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r::OrdinalRange) = r -broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r::StepRangeLen) = r -broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r::LinRange) = r +broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r::AbstractRange) = r -broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::OrdinalRange) = range(-first(r), step=-step(r), length=length(r)) -broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::StepRangeLen) = StepRangeLen(-r.ref, -r.step, length(r), r.offset) +broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::AbstractRange) = range(-first(r), step=negate(step(r)), length=length(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::OrdinalRange) = range(-first(r), -last(r), step=negate(step(r))) +broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::StepRangeLen) = StepRangeLen(-r.ref, negate(r.step), length(r), r.offset) broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::LinRange) = LinRange(-r.start, -r.stop, length(r)) -broadcasted(::DefaultArrayStyle{1}, ::typeof(+), x::Real, r::AbstractUnitRange) = range(x + first(r), length=length(r)) -broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r::AbstractUnitRange, x::Real) = range(first(r) + x, length=length(r)) # For #18336 we need to prevent promotion of the step type: broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r::AbstractRange, x::Number) = range(first(r) + x, step=step(r), length=length(r)) broadcasted(::DefaultArrayStyle{1}, ::typeof(+), x::Number, r::AbstractRange) = range(x + first(r), step=step(r), length=length(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r::OrdinalRange, x::Integer) = range(first(r) + x, last(r) + x, step=step(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(+), x::Integer, r::OrdinalRange) = range(x + first(r), x + last(r), step=step(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r::AbstractUnitRange, x::Integer) = range(first(r) + x, last(r) + x) +broadcasted(::DefaultArrayStyle{1}, ::typeof(+), x::Integer, r::AbstractUnitRange) = range(x + first(r), x + last(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r::AbstractUnitRange, x::Real) = range(first(r) + x, length=length(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(+), x::Real, r::AbstractUnitRange) = range(x + first(r), length=length(r)) broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r::StepRangeLen{T}, x::Number) where T = StepRangeLen{typeof(T(r.ref)+x)}(r.ref + x, r.step, length(r), r.offset) broadcasted(::DefaultArrayStyle{1}, ::typeof(+), x::Number, r::StepRangeLen{T}) where T = @@ -1069,27 +1101,36 @@ broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r::LinRange, x::Number) = LinRa broadcasted(::DefaultArrayStyle{1}, ::typeof(+), x::Number, r::LinRange) = LinRange(x + r.start, x + r.stop, length(r)) broadcasted(::DefaultArrayStyle{1}, ::typeof(+), r1::AbstractRange, r2::AbstractRange) = r1 + r2 -broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::AbstractUnitRange, x::Number) = range(first(r)-x, length=length(r)) -broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::AbstractRange, x::Number) = range(first(r)-x, step=step(r), length=length(r)) -broadcasted(::DefaultArrayStyle{1}, ::typeof(-), x::Number, r::AbstractRange) = range(x-first(r), step=-step(r), length=length(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::AbstractRange, x::Number) = range(first(r) - x, step=step(r), length=length(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(-), x::Number, r::AbstractRange) = range(x - first(r), step=negate(step(r)), length=length(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::OrdinalRange, x::Integer) = range(first(r) - x, last(r) - x, step=step(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(-), x::Integer, r::OrdinalRange) = range(x - first(r), x - last(r), step=negate(step(r))) +broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::AbstractUnitRange, x::Integer) = range(first(r) - x, last(r) - x) +broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::AbstractUnitRange, x::Real) = range(first(r) - x, length=length(r)) broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::StepRangeLen{T}, x::Number) where T = StepRangeLen{typeof(T(r.ref)-x)}(r.ref - x, r.step, length(r), r.offset) broadcasted(::DefaultArrayStyle{1}, ::typeof(-), x::Number, r::StepRangeLen{T}) where T = - StepRangeLen{typeof(x-T(r.ref))}(x - r.ref, -r.step, length(r), r.offset) + StepRangeLen{typeof(x-T(r.ref))}(x - r.ref, negate(r.step), length(r), r.offset) broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::LinRange, x::Number) = LinRange(r.start - x, r.stop - x, length(r)) broadcasted(::DefaultArrayStyle{1}, ::typeof(-), x::Number, r::LinRange) = LinRange(x - r.start, x - r.stop, length(r)) broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r1::AbstractRange, r2::AbstractRange) = r1 - r2 -broadcasted(::DefaultArrayStyle{1}, ::typeof(*), x::Number, r::AbstractRange) = range(x*first(r), step=x*step(r), length=length(r)) +# at present Base.range_start_step_length(1,0,5) is an error, so for 0 .* (-2:2) we explicitly construct StepRangeLen: +broadcasted(::DefaultArrayStyle{1}, ::typeof(*), x::Number, r::AbstractRange) = StepRangeLen(x*first(r), x*step(r), length(r)) broadcasted(::DefaultArrayStyle{1}, ::typeof(*), x::Number, r::StepRangeLen{T}) where {T} = StepRangeLen{typeof(x*T(r.ref))}(x*r.ref, x*r.step, length(r), r.offset) broadcasted(::DefaultArrayStyle{1}, ::typeof(*), x::Number, r::LinRange) = LinRange(x * r.start, x * r.stop, r.len) -# separate in case of noncommutative multiplication -broadcasted(::DefaultArrayStyle{1}, ::typeof(*), r::AbstractRange, x::Number) = range(first(r)*x, step=step(r)*x, length=length(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(*), x::AbstractFloat, r::OrdinalRange) = + Base.range_start_step_length(x*first(r), x*step(r), length(r)) # 0.2 .* (-2:2) needs TwicePrecision +# separate in case of noncommutative multiplication: +broadcasted(::DefaultArrayStyle{1}, ::typeof(*), r::AbstractRange, x::Number) = StepRangeLen(first(r)*x, step(r)*x, length(r)) broadcasted(::DefaultArrayStyle{1}, ::typeof(*), r::StepRangeLen{T}, x::Number) where {T} = StepRangeLen{typeof(T(r.ref)*x)}(r.ref*x, r.step*x, length(r), r.offset) broadcasted(::DefaultArrayStyle{1}, ::typeof(*), r::LinRange, x::Number) = LinRange(r.start * x, r.stop * x, r.len) +broadcasted(::DefaultArrayStyle{1}, ::typeof(*), r::OrdinalRange, x::AbstractFloat) = + Base.range_start_step_length(first(r)*x, step(r)*x, length(r)) +#broadcasted(::DefaultArrayStyle{1}, ::typeof(/), r::AbstractRange, x::Number) = range(first(r)/x, last(r)/x, length=length(r)) broadcasted(::DefaultArrayStyle{1}, ::typeof(/), r::AbstractRange, x::Number) = range(first(r)/x, step=step(r)/x, length=length(r)) broadcasted(::DefaultArrayStyle{1}, ::typeof(/), r::StepRangeLen{T}, x::Number) where {T} = StepRangeLen{typeof(T(r.ref)/x)}(r.ref/x, r.step/x, length(r), r.offset) @@ -1112,7 +1153,7 @@ broadcasted(::typeof(+), j::CartesianIndex{N}, I::CartesianIndices{N}) where N = broadcasted(::typeof(-), I::CartesianIndices{N}, j::CartesianIndex{N}) where N = CartesianIndices(map((rng, offset)->rng .- offset, I.indices, Tuple(j))) function broadcasted(::typeof(-), j::CartesianIndex{N}, I::CartesianIndices{N}) where N - diffrange(offset, rng) = range(offset-last(rng), length=length(rng)) + diffrange(offset, rng) = range(offset-last(rng), length=length(rng), step=step(rng)) Iterators.reverse(CartesianIndices(map(diffrange, Tuple(j), I.indices))) end @@ -1196,18 +1237,12 @@ function __dot__(x::Expr) Meta.isexpr(x.args[1], :call) # function or macro definition Expr(x.head, x.args[1], dotargs[2]) elseif x.head === :(<:) || x.head === :(>:) - tmp = x.head === :(<:) ? :(.<:) : :(.>:) + tmp = x.head === :(<:) ? :.<: : :.>: Expr(:call, tmp, dotargs...) else - if x.head === :&& || x.head === :|| - error(""" - Using `&&` and `||` is disallowed in `@.` expressions. - Use `&` or `|` for elementwise logical operations. - """) - end - head = string(x.head) - if last(head) == '=' && first(head) != '.' - Expr(Symbol('.',head), dotargs...) + head = String(x.head)::String + if last(head) == '=' && first(head) != '.' || head == "&&" || head == "||" + Expr(Symbol('.', head), dotargs...) else Expr(x.head, dotargs...) end @@ -1242,7 +1277,13 @@ macro __dot__(x) esc(__dot__(x)) end -@inline broadcasted_kwsyntax(f, args...; kwargs...) = broadcasted((args...)->f(args...; kwargs...), args...) +@inline function broadcasted_kwsyntax(f, args...; kwargs...) + if isempty(kwargs) # some BroadcastStyles dispatch on `f`, so try to preserve its type + return broadcasted(f, args...) + else + return broadcasted((args...) -> f(args...; kwargs...), args...) + end +end @inline function broadcasted(f, args...) args′ = map(broadcastable, args) broadcasted(combine_styles(args′...), f, args′...) @@ -1264,4 +1305,44 @@ end end @inline broadcasted(::S, f, args...) where S<:BroadcastStyle = Broadcasted{S}(f, args) +""" + BroadcastFunction{F} <: Function + +Represents the "dotted" version of an operator, which broadcasts the operator over its +arguments, so `BroadcastFunction(op)` is functionally equivalent to `(x...) -> (op).(x...)`. + +Can be created by just passing an operator preceded by a dot to a higher-order function. + +# Examples +```jldoctest +julia> a = [[1 3; 2 4], [5 7; 6 8]]; + +julia> b = [[9 11; 10 12], [13 15; 14 16]]; + +julia> map(.*, a, b) +2-element Vector{Matrix{Int64}}: + [9 33; 20 48] + [65 105; 84 128] + +julia> Base.BroadcastFunction(+)(a, b) == a .+ b +true +``` + +!!! compat "Julia 1.6" + `BroadcastFunction` and the standalone `.op` syntax are available as of Julia 1.6. +""" +struct BroadcastFunction{F} <: Function + f::F +end + +@inline (op::BroadcastFunction)(x...; kwargs...) = op.f.(x...; kwargs...) + +function Base.show(io::IO, op::BroadcastFunction) + print(io, BroadcastFunction, '(') + show(io, op.f) + print(io, ')') + nothing +end +Base.show(io::IO, ::MIME"text/plain", op::BroadcastFunction) = show(io, op) + end # module diff --git a/base/c.jl b/base/c.jl index a26f41856dc8ff..7d168f2293c9c5 100644 --- a/base/c.jl +++ b/base/c.jl @@ -270,6 +270,21 @@ reasonably represented in the target encoding; it always succeeds for conversions between UTF-XX encodings, even for invalid Unicode data. Only conversion to/from UTF-8 is currently supported. + +# Examples +```jldoctest +julia> str = "αβγ" +"αβγ" + +julia> transcode(UInt16, str) +3-element Vector{UInt16}: + 0x03b1 + 0x03b2 + 0x03b3 + +julia> transcode(String, transcode(UInt16, str)) +"αβγ" +``` """ function transcode end @@ -533,6 +548,12 @@ function expand_ccallable(rt, def) error("expected method definition in @ccallable") end +""" + @ccallable(def) + +Make the annotated function be callable from C using its name. This can, for example, +be used to expose functionality as a C-API when creating a custom Julia sysimage. +""" macro ccallable(def) expand_ccallable(nothing, def) end @@ -712,3 +733,7 @@ name, if desired `"libglib-2.0".g_uri_escape_string(...` macro ccall(expr) return ccall_macro_lower(:ccall, ccall_macro_parse(expr)...) end + +macro ccall_effects(effects::UInt8, expr) + return ccall_macro_lower((:ccall, effects), ccall_macro_parse(expr)...) +end diff --git a/base/channels.jl b/base/channels.jl index e4c0d003866aa9..da7b1d24583ca2 100644 --- a/base/channels.jl +++ b/base/channels.jl @@ -33,10 +33,11 @@ mutable struct Channel{T} <: AbstractChannel{T} cond_take::Threads.Condition # waiting for data to become available cond_wait::Threads.Condition # waiting for data to become maybe available cond_put::Threads.Condition # waiting for a writeable slot - state::Symbol + @atomic state::Symbol excp::Union{Exception, Nothing} # exception to be thrown when state !== :open data::Vector{T} + @atomic n_avail_items::Int # Available items for taking, can be read without lock sz_max::Int # maximum size of channel function Channel{T}(sz::Integer = 0) where T @@ -46,7 +47,7 @@ mutable struct Channel{T} <: AbstractChannel{T} lock = ReentrantLock() cond_put, cond_take = Threads.Condition(lock), Threads.Condition(lock) cond_wait = (sz == 0 ? Threads.Condition(lock) : cond_take) # wait is distinct from take iff unbuffered - return new(cond_take, cond_wait, cond_put, :open, nothing, Vector{T}(), sz) + return new(cond_take, cond_wait, cond_put, :open, nothing, Vector{T}(), 0, sz) end end @@ -62,6 +63,7 @@ Channel(sz=0) = Channel{Any}(sz) Create a new task from `func`, bind it to a new channel of type `T` and size `size`, and schedule the task, all in a single call. +The channel is automatically closed when the task terminates. `func` must accept the bound channel as its only argument. @@ -121,7 +123,7 @@ julia> chnl = Channel{Char}(1, spawn=true) do ch put!(ch, c) end end -Channel{Char}(1) (1 item available) +Channel{Char}(1) (2 items available) julia> String(collect(chnl)) "hello world" @@ -166,6 +168,8 @@ isbuffered(c::Channel) = c.sz_max==0 ? false : true function check_channel_state(c::Channel) if !isopen(c) + # if the monotonic load succeed, now do an acquire fence + (@atomic :acquire c.state) === :open && concurrency_violation() excp = c.excp excp !== nothing && throw(excp) throw(closed_exception()) @@ -182,8 +186,8 @@ Close a channel. An exception (optionally given by `excp`), is thrown by: function close(c::Channel, excp::Exception=closed_exception()) lock(c) try - c.state = :closed c.excp = excp + @atomic :release c.state = :closed notify_error(c.cond_take, excp) notify_error(c.cond_wait, excp) notify_error(c.cond_put, excp) @@ -192,7 +196,7 @@ function close(c::Channel, excp::Exception=closed_exception()) end nothing end -isopen(c::Channel) = (c.state === :open) +isopen(c::Channel) = ((@atomic :monotonic c.state) === :open) """ bind(chnl::Channel, task::Task) @@ -283,7 +287,7 @@ function close_chnl_on_taskdone(t::Task, c::Channel) lock(c) try isopen(c) || return - if istaskfailed(t) && task_result(t) isa Exception + if istaskfailed(t) close(c, TaskFailedException(t)) return end @@ -295,9 +299,10 @@ function close_chnl_on_taskdone(t::Task, c::Channel) end struct InvalidStateException <: Exception - msg::AbstractString + msg::String state::Symbol end +showerror(io::IO, ex::InvalidStateException) = print(io, "InvalidStateException: ", ex.msg) """ put!(c::Channel, v) @@ -316,17 +321,36 @@ function put!(c::Channel{T}, v) where T return isbuffered(c) ? put_buffered(c, v) : put_unbuffered(c, v) end +# Atomically update channel n_avail, *assuming* we hold the channel lock. +function _increment_n_avail(c, inc) + # We hold the channel lock so it's safe to non-atomically read and + # increment c.n_avail_items + newlen = c.n_avail_items + inc + # Atomically store c.n_avail_items to prevent data races with other threads + # reading this outside the lock. + @atomic :monotonic c.n_avail_items = newlen +end + function put_buffered(c::Channel, v) lock(c) + did_buffer = false try + # Increment channel n_avail eagerly (before push!) to count data in the + # buffer as well as offers from tasks which are blocked in wait(). + _increment_n_avail(c, 1) while length(c.data) == c.sz_max check_channel_state(c) wait(c.cond_put) end + check_channel_state(c) push!(c.data, v) + did_buffer = true # notify all, since some of the waiters may be on a "fetch" call. notify(c.cond_take, nothing, true, false) finally + # Decrement the available items if this task had an exception before pushing the + # item to the buffer (e.g., during `wait(c.cond_put)`): + did_buffer || _increment_n_avail(c, -1) unlock(c) end return v @@ -335,14 +359,17 @@ end function put_unbuffered(c::Channel, v) lock(c) taker = try + _increment_n_avail(c, 1) while isempty(c.cond_take.waitq) check_channel_state(c) notify(c.cond_wait) wait(c.cond_put) end + check_channel_state(c) # unfair scheduled version of: notify(c.cond_take, v, false, false); yield() popfirst!(c.cond_take.waitq) finally + _increment_n_avail(c, -1) unlock(c) end schedule(taker, v) @@ -389,6 +416,7 @@ function take_buffered(c::Channel) wait(c.cond_take) end v = popfirst!(c.data) + _increment_n_avail(c, -1) notify(c.cond_put, nothing, false, false) # notify only one, since only one slot has become available for a put!. return v finally @@ -418,10 +446,14 @@ For unbuffered channels returns `true` if there are tasks waiting on a [`put!`](@ref). """ isready(c::Channel) = n_avail(c) > 0 -n_avail(c::Channel) = isbuffered(c) ? length(c.data) : length(c.cond_put.waitq) -isempty(c::Channel) = isbuffered(c) ? isempty(c.data) : isempty(c.cond_put.waitq) +isempty(c::Channel) = n_avail(c) == 0 +function n_avail(c::Channel) + # Lock-free equivalent to `length(c.data) + length(c.cond_put.waitq)` + @atomic :monotonic c.n_avail_items +end lock(c::Channel) = lock(c.cond_take) +lock(f, c::Channel) = lock(f, c.cond_take) unlock(c::Channel) = unlock(c.cond_take) trylock(c::Channel) = trylock(c.cond_take) @@ -454,7 +486,7 @@ function show(io::IO, ::MIME"text/plain", c::Channel) print(io, " (empty)") else s = n == 1 ? "" : "s" - print(io, " (", n_avail(c), " item$s available)") + print(io, " (", n, " item$s available)") end end end diff --git a/base/char.jl b/base/char.jl index 173c84711e551a..c8b1c28166bbf9 100644 --- a/base/char.jl +++ b/base/char.jl @@ -45,9 +45,10 @@ represents a valid Unicode character. """ Char -(::Type{T})(x::Number) where {T<:AbstractChar} = T(UInt32(x)) -AbstractChar(x::Number) = Char(x) -(::Type{T})(x::AbstractChar) where {T<:Union{Number,AbstractChar}} = T(codepoint(x)) +@constprop :aggressive (::Type{T})(x::Number) where {T<:AbstractChar} = T(UInt32(x)) +@constprop :aggressive AbstractChar(x::Number) = Char(x) +@constprop :aggressive (::Type{T})(x::AbstractChar) where {T<:Union{Number,AbstractChar}} = T(codepoint(x)) +@constprop :aggressive (::Type{T})(x::AbstractChar) where {T<:Union{Int32,Int64}} = codepoint(x) % T (::Type{T})(x::T) where {T<:AbstractChar} = x """ @@ -74,7 +75,7 @@ return a different-sized integer (e.g. `UInt8`). """ function codepoint end -codepoint(c::Char) = UInt32(c) +@constprop :aggressive codepoint(c::Char) = UInt32(c) struct InvalidCharError{T<:AbstractChar} <: Exception char::T @@ -82,11 +83,11 @@ end struct CodePointError{T<:Integer} <: Exception code::T end -@noinline invalid_char(c::AbstractChar) = throw(InvalidCharError(c)) -@noinline code_point_err(u::Integer) = throw(CodePointError(u)) +@noinline throw_invalid_char(c::AbstractChar) = throw(InvalidCharError(c)) +@noinline throw_code_point_err(u::Integer) = throw(CodePointError(u)) function ismalformed(c::Char) - u = reinterpret(UInt32, c) + u = bitcast(UInt32, c) l1 = leading_ones(u) << 3 t0 = trailing_zeros(u) & 56 (l1 == 8) | (l1 + t0 > 32) | @@ -96,7 +97,7 @@ end @inline is_overlong_enc(u::UInt32) = (u >> 24 == 0xc0) | (u >> 24 == 0xc1) | (u >> 21 == 0x0704) | (u >> 20 == 0x0f08) function isoverlong(c::Char) - u = reinterpret(UInt32, c) + u = bitcast(UInt32, c) is_overlong_enc(u) end @@ -107,8 +108,9 @@ end ismalformed(c::AbstractChar) -> Bool Return `true` if `c` represents malformed (non-Unicode) data according to the -encoding used by `c`. Defaults to `false` for non-`Char` types. See also -[`show_invalid`](@ref). +encoding used by `c`. Defaults to `false` for non-`Char` types. + +See also [`show_invalid`](@ref). """ ismalformed(c::AbstractChar) = false @@ -116,20 +118,21 @@ ismalformed(c::AbstractChar) = false isoverlong(c::AbstractChar) -> Bool Return `true` if `c` represents an overlong UTF-8 sequence. Defaults -to `false` for non-`Char` types. See also [`decode_overlong`](@ref) -and [`show_invalid`](@ref). +to `false` for non-`Char` types. + +See also [`decode_overlong`](@ref) and [`show_invalid`](@ref). """ isoverlong(c::AbstractChar) = false -function UInt32(c::Char) +@constprop :aggressive function UInt32(c::Char) # TODO: use optimized inline LLVM - u = reinterpret(UInt32, c) + u = bitcast(UInt32, c) u < 0x80000000 && return u >> 24 l1 = leading_ones(u) t0 = trailing_zeros(u) & 56 (l1 == 1) | (8l1 + t0 > 32) | ((((u & 0x00c0c0c0) ⊻ 0x00808080) >> t0 != 0) | is_overlong_enc(u)) && - invalid_char(c)::Union{} + throw_invalid_char(c) u &= 0xffffffff >> l1 u >>= t0 ((u & 0x0000007f) >> 0) | ((u & 0x00007f00) >> 2) | @@ -145,8 +148,8 @@ that support overlong encodings should implement `Base.decode_overlong`. """ function decode_overlong end -function decode_overlong(c::Char) - u = reinterpret(UInt32, c) +@constprop :aggressive function decode_overlong(c::Char) + u = bitcast(UInt32, c) l1 = leading_ones(u) t0 = trailing_zeros(u) & 56 u &= 0xffffffff >> l1 @@ -155,24 +158,26 @@ function decode_overlong(c::Char) ((u & 0x007f0000) >> 4) | ((u & 0x7f000000) >> 6) end -function Char(u::UInt32) - u < 0x80 && return reinterpret(Char, u << 24) - u < 0x00200000 || code_point_err(u)::Union{} +@constprop :aggressive function Char(u::UInt32) + u < 0x80 && return bitcast(Char, u << 24) + u < 0x00200000 || throw_code_point_err(u) c = ((u << 0) & 0x0000003f) | ((u << 2) & 0x00003f00) | ((u << 4) & 0x003f0000) | ((u << 6) & 0x3f000000) c = u < 0x00000800 ? (c << 16) | 0xc0800000 : u < 0x00010000 ? (c << 08) | 0xe0808000 : (c << 00) | 0xf0808080 - reinterpret(Char, c) + bitcast(Char, c) end -function (T::Union{Type{Int8},Type{UInt8}})(c::Char) - i = reinterpret(Int32, c) - i ≥ 0 ? ((i >>> 24) % T) : T(UInt32(c)) +@constprop :aggressive @noinline UInt32_cold(c::Char) = UInt32(c) +@constprop :aggressive function (T::Union{Type{Int8},Type{UInt8}})(c::Char) + i = bitcast(Int32, c) + i ≥ 0 ? ((i >>> 24) % T) : T(UInt32_cold(c)) end -function Char(b::Union{Int8,UInt8}) - 0 ≤ b ≤ 0x7f ? reinterpret(Char, (b % UInt32) << 24) : Char(UInt32(b)) +@constprop :aggressive @noinline Char_cold(b::UInt32) = Char(b) +@constprop :aggressive function Char(b::Union{Int8,UInt8}) + 0 ≤ b ≤ 0x7f ? bitcast(Char, (b % UInt32) << 24) : Char_cold(UInt32(b)) end convert(::Type{AbstractChar}, x::Number) = Char(x) # default to Char @@ -183,8 +188,8 @@ convert(::Type{T}, c::T) where {T<:AbstractChar} = c rem(x::AbstractChar, ::Type{T}) where {T<:Number} = rem(codepoint(x), T) -typemax(::Type{Char}) = reinterpret(Char, typemax(UInt32)) -typemin(::Type{Char}) = reinterpret(Char, typemin(UInt32)) +typemax(::Type{Char}) = bitcast(Char, typemax(UInt32)) +typemin(::Type{Char}) = bitcast(Char, typemin(UInt32)) size(c::AbstractChar) = () size(c::AbstractChar, d::Integer) = d < 1 ? throw(BoundsError()) : 1 @@ -205,12 +210,12 @@ iterate(c::AbstractChar, done=false) = done ? nothing : (c, true) isempty(c::AbstractChar) = false in(x::AbstractChar, y::AbstractChar) = x == y -==(x::Char, y::Char) = reinterpret(UInt32, x) == reinterpret(UInt32, y) -isless(x::Char, y::Char) = reinterpret(UInt32, x) < reinterpret(UInt32, y) +==(x::Char, y::Char) = bitcast(UInt32, x) == bitcast(UInt32, y) +isless(x::Char, y::Char) = bitcast(UInt32, x) < bitcast(UInt32, y) hash(x::Char, h::UInt) = - hash_uint64(((reinterpret(UInt32, x) + UInt64(0xd4d64234)) << 32) ⊻ UInt64(h)) + hash_uint64(((bitcast(UInt32, x) + UInt64(0xd4d64234)) << 32) ⊻ UInt64(h)) -first_utf8_byte(c::Char) = (reinterpret(UInt32, c) >> 24) % UInt8 +first_utf8_byte(c::Char) = (bitcast(UInt32, c) >> 24) % UInt8 # fallbacks: isless(x::AbstractChar, y::AbstractChar) = isless(Char(x), Char(y)) @@ -219,8 +224,26 @@ hash(x::AbstractChar, h::UInt) = hash(Char(x), h) widen(::Type{T}) where {T<:AbstractChar} = T @inline -(x::AbstractChar, y::AbstractChar) = Int(x) - Int(y) -@inline -(x::T, y::Integer) where {T<:AbstractChar} = T(Int32(x) - Int32(y)) -@inline +(x::T, y::Integer) where {T<:AbstractChar} = T(Int32(x) + Int32(y)) +@inline function -(x::T, y::Integer) where {T<:AbstractChar} + if x isa Char + u = Int32((bitcast(UInt32, x) >> 24) % Int8) + if u >= 0 # inline the runtime fast path + z = u - y + return 0 <= z < 0x80 ? bitcast(Char, (z % UInt32) << 24) : Char(UInt32(z)) + end + end + return T(Int32(x) - Int32(y)) +end +@inline function +(x::T, y::Integer) where {T<:AbstractChar} + if x isa Char + u = Int32((bitcast(UInt32, x) >> 24) % Int8) + if u >= 0 # inline the runtime fast path + z = u + y + return 0 <= z < 0x80 ? bitcast(Char, (z % UInt32) << 24) : Char(UInt32(z)) + end + end + return T(Int32(x) + Int32(y)) +end @inline +(x::Integer, y::AbstractChar) = y + x # `print` should output UTF-8 by default for all AbstractChar types. @@ -236,7 +259,7 @@ const hex_chars = UInt8['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', function show_invalid(io::IO, c::Char) write(io, 0x27) - u = reinterpret(UInt32, c) + u = bitcast(UInt32, c) while true a = hex_chars[((u >> 28) & 0xf) + 1] b = hex_chars[((u >> 24) & 0xf) + 1] diff --git a/base/checked.jl b/base/checked.jl index 840015861923fc..1f9e319f50fbd1 100644 --- a/base/checked.jl +++ b/base/checked.jl @@ -6,14 +6,14 @@ module Checked export checked_neg, checked_abs, checked_add, checked_sub, checked_mul, checked_div, checked_rem, checked_fld, checked_mod, checked_cld, - add_with_overflow, sub_with_overflow, mul_with_overflow + checked_length, add_with_overflow, sub_with_overflow, mul_with_overflow import Core.Intrinsics: checked_sadd_int, checked_ssub_int, checked_smul_int, checked_sdiv_int, checked_srem_int, checked_uadd_int, checked_usub_int, checked_umul_int, checked_udiv_int, checked_urem_int -import ..no_op_err, ..@_inline_meta, ..@_noinline_meta +import ..no_op_err, ..@inline, ..@noinline, ..checked_length # define promotion behavior for checked operations checked_add(x::Integer, y::Integer) = checked_add(promote(x,y)...) @@ -86,7 +86,7 @@ The overflow protection may impose a perceptible performance penalty. function checked_neg(x::T) where T<:Integer checked_sub(T(0), x) end -throw_overflowerr_negation(x) = (@_noinline_meta; +throw_overflowerr_negation(x) = (@noinline; throw(OverflowError(Base.invokelatest(string, "checked arithmetic: cannot compute -x for x = ", x, "::", typeof(x))))) if BrokenSignedInt != Union{} function checked_neg(x::BrokenSignedInt) @@ -115,9 +115,10 @@ function checked_abs end function checked_abs(x::SignedInt) r = ifelse(x<0, -x, x) - r<0 && throw(OverflowError(string("checked arithmetic: cannot compute |x| for x = ", x, "::", typeof(x)))) - r - end + r<0 || return r + msg = LazyString("checked arithmetic: cannot compute |x| for x = ", x, "::", typeof(x)) + throw(OverflowError(msg)) +end checked_abs(x::UnsignedInt) = x checked_abs(x::Bool) = x @@ -150,8 +151,8 @@ end end -throw_overflowerr_binaryop(op, x, y) = (@_noinline_meta; - throw(OverflowError(Base.invokelatest(string, x, " ", op, " ", y, " overflowed for type ", typeof(x))))) +throw_overflowerr_binaryop(op, x, y) = (@noinline; + throw(OverflowError(LazyString(x, " ", op, " ", y, " overflowed for type ", typeof(x))))) """ Base.checked_add(x, y) @@ -161,7 +162,7 @@ Calculates `x+y`, checking for overflow errors where applicable. The overflow protection may impose a perceptible performance penalty. """ function checked_add(x::T, y::T) where T<:Integer - @_inline_meta + @inline z, b = add_with_overflow(x, y) b && throw_overflowerr_binaryop(:+, x, y) z @@ -218,7 +219,7 @@ Calculates `x-y`, checking for overflow errors where applicable. The overflow protection may impose a perceptible performance penalty. """ function checked_sub(x::T, y::T) where T<:Integer - @_inline_meta + @inline z, b = sub_with_overflow(x, y) b && throw_overflowerr_binaryop(:-, x, y) z @@ -283,7 +284,7 @@ Calculates `x*y`, checking for overflow errors where applicable. The overflow protection may impose a perceptible performance penalty. """ function checked_mul(x::T, y::T) where T<:Integer - @_inline_meta + @inline z, b = mul_with_overflow(x, y) b && throw_overflowerr_binaryop(:*, x, y) z @@ -349,4 +350,12 @@ The overflow protection may impose a perceptible performance penalty. """ checked_cld(x::T, y::T) where {T<:Integer} = cld(x, y) # Base.cld already checks +""" + Base.checked_length(r) + +Calculates `length(r)`, but may check for overflow errors where applicable when +the result doesn't fit into `Union{Integer(eltype(r)),Int}`. +""" +checked_length(r) = length(r) # for most things, length doesn't error + end diff --git a/base/client.jl b/base/client.jl index 25fb605d729e3e..d335f378f24c1a 100644 --- a/base/client.jl +++ b/base/client.jl @@ -84,27 +84,33 @@ end function scrub_repl_backtrace(bt) if bt !== nothing && !(bt isa Vector{Any}) # ignore our sentinel value types - bt = stacktrace(bt) + bt = bt isa Vector{StackFrame} ? copy(bt) : stacktrace(bt) # remove REPL-related frames from interactive printing eval_ind = findlast(frame -> !frame.from_c && frame.func === :eval, bt) eval_ind === nothing || deleteat!(bt, eval_ind:length(bt)) end return bt end +scrub_repl_backtrace(stack::ExceptionStack) = + ExceptionStack(Any[(;x.exception, backtrace = scrub_repl_backtrace(x.backtrace)) for x in stack]) -function display_error(io::IO, er, bt) +istrivialerror(stack::ExceptionStack) = + length(stack) == 1 && length(stack[1].backtrace) ≤ 1 + # frame 1 = top level; assumes already went through scrub_repl_backtrace + +function display_error(io::IO, stack::ExceptionStack) printstyled(io, "ERROR: "; bold=true, color=Base.error_color()) - bt = scrub_repl_backtrace(bt) - showerror(IOContext(io, :limit => true), er, bt, backtrace = bt!==nothing) + show_exception_stack(IOContext(io, :limit => true), stack) println(io) end -function display_error(io::IO, stack::Vector) +display_error(stack::ExceptionStack) = display_error(stderr, stack) + +# these forms are depended on by packages outside Julia +function display_error(io::IO, er, bt) printstyled(io, "ERROR: "; bold=true, color=Base.error_color()) - bt = Any[ (x[1], scrub_repl_backtrace(x[2])) for x in stack ] - show_exception_stack(IOContext(io, :limit => true), bt) + showerror(IOContext(io, :limit => true), er, bt, backtrace = bt!==nothing) println(io) end -display_error(stack::Vector) = display_error(stderr, stack) display_error(er, bt=nothing) = display_error(stderr, er, bt) function eval_user_input(errio, @nospecialize(ast), show_value::Bool) @@ -117,13 +123,15 @@ function eval_user_input(errio, @nospecialize(ast), show_value::Bool) print(color_normal) end if lasterr !== nothing + lasterr = scrub_repl_backtrace(lasterr) + istrivialerror(lasterr) || setglobal!(Main, :err, lasterr) invokelatest(display_error, errio, lasterr) errcount = 0 lasterr = nothing else ast = Meta.lower(Main, ast) value = Core.eval(Main, ast) - ccall(:jl_set_global, Cvoid, (Any, Any, Any), Main, :ans, value) + setglobal!(Main, :ans, value) if !(value === nothing) && show_value if have_color print(answer_color()) @@ -134,7 +142,6 @@ function eval_user_input(errio, @nospecialize(ast), show_value::Bool) @error "Evaluation succeeded, but an error occurred while displaying the value" typeof(value) rethrow() end - println() end end break @@ -143,7 +150,8 @@ function eval_user_input(errio, @nospecialize(ast), show_value::Bool) @error "SYSTEM: display_error(errio, lasterr) caused an error" end errcount += 1 - lasterr = catch_stack() + lasterr = scrub_repl_backtrace(current_exceptions()) + setglobal!(Main, :err, lasterr) if errcount > 2 @error "It is likely that something important is broken, and Julia will not be able to continue normally" errcount break @@ -198,9 +206,6 @@ function incomplete_tag(ex::Expr) return :other end -# call include() on a file, ignoring if not found -include_ifexists(mod::Module, path::AbstractString) = isfile(path) && include(mod, path) - function exec_options(opts) if !isempty(ARGS) idxs = findall(x -> x == "--", ARGS) @@ -252,8 +257,18 @@ function exec_options(opts) invokelatest(Main.Distributed.process_opts, opts) end + interactiveinput = (repl || is_interactive::Bool) && isa(stdin, TTY) + is_interactive::Bool |= interactiveinput + # load ~/.julia/config/startup.jl file - startup && load_julia_startup() + if startup + try + load_julia_startup() + catch + invokelatest(display_error, scrub_repl_backtrace(current_exceptions())) + !(repl || is_interactive::Bool) && exit(1) + end + end # process cmds list for (cmd, arg) in cmds @@ -282,19 +297,20 @@ function exec_options(opts) exit_on_sigint(true) end try - include(Main, PROGRAM_FILE) + if PROGRAM_FILE == "-" + include_string(Main, read(stdin, String), "stdin") + else + include(Main, PROGRAM_FILE) + end catch - invokelatest(display_error, catch_stack()) + invokelatest(display_error, scrub_repl_backtrace(current_exceptions())) if !is_interactive::Bool exit(1) end end end - repl |= is_interactive::Bool - if repl - interactiveinput = isa(stdin, TTY) + if repl || is_interactive::Bool if interactiveinput - global is_interactive = true banner = (opts.banner != 0) # --banner!=no else banner = (opts.banner == 1) # --banner=yes @@ -304,17 +320,33 @@ function exec_options(opts) nothing end -function load_julia_startup() +function _global_julia_startup_file() # If the user built us with a specific Base.SYSCONFDIR, check that location first for a startup.jl file - # If it is not found, then continue on to the relative path based on Sys.BINDIR - BINDIR = Sys.BINDIR::String - SYSCONFDIR = Base.SYSCONFDIR::String - if !isempty(SYSCONFDIR) && isfile(joinpath(BINDIR, SYSCONFDIR, "julia", "startup.jl")) - include(Main, abspath(BINDIR, SYSCONFDIR, "julia", "startup.jl")) - else - include_ifexists(Main, abspath(BINDIR, "..", "etc", "julia", "startup.jl")) + # If it is not found, then continue on to the relative path based on Sys.BINDIR + BINDIR = Sys.BINDIR + SYSCONFDIR = Base.SYSCONFDIR + if !isempty(SYSCONFDIR) + p1 = abspath(BINDIR, SYSCONFDIR, "julia", "startup.jl") + isfile(p1) && return p1 + end + p2 = abspath(BINDIR, "..", "etc", "julia", "startup.jl") + isfile(p2) && return p2 + return nothing +end + +function _local_julia_startup_file() + if !isempty(DEPOT_PATH) + path = abspath(DEPOT_PATH[1], "config", "startup.jl") + isfile(path) && return path end - !isempty(DEPOT_PATH) && include_ifexists(Main, abspath(DEPOT_PATH[1], "config", "startup.jl")) + return nothing +end + +function load_julia_startup() + global_file = _global_julia_startup_file() + (global_file !== nothing) && include(Main, global_file) + local_file = _local_julia_startup_file() + (local_file !== nothing) && include(Main, local_file) return nothing end @@ -465,26 +497,27 @@ Returns the result of the last evaluated expression of the input file. During in a task-local include path is set to the directory containing the file. Nested calls to `include` will search relative to that path. This function is typically used to load source interactively, or to combine files in packages that are broken into multiple source files. +The argument `path` is normalized using [`normpath`](@ref) which will resolve +relative path tokens such as `..` and convert `/` to the appropriate path separator. The optional first argument `mapexpr` can be used to transform the included code before it is evaluated: for each parsed expression `expr` in `path`, the `include` function actually evaluates `mapexpr(expr)`. If it is omitted, `mapexpr` defaults to [`identity`](@ref). Use [`Base.include`](@ref) to evaluate a file into another module. + +!!! compat "Julia 1.5" + Julia 1.5 is required for passing the `mapexpr` argument. """ MainInclude.include function _start() empty!(ARGS) append!(ARGS, Core.ARGS) - if ccall(:jl_generating_output, Cint, ()) != 0 && JLOptions().incremental == 0 - # clear old invalid pointers - PCRE.__init__() - end try exec_options(JLOptions()) catch - invokelatest(display_error, catch_stack()) + invokelatest(display_error, scrub_repl_backtrace(current_exceptions())) exit(1) end if is_interactive && get(stdout, :color, false) diff --git a/base/cmd.jl b/base/cmd.jl index 210b77b11b1ca9..ecabb5c32b1d05 100644 --- a/base/cmd.jl +++ b/base/cmd.jl @@ -13,12 +13,14 @@ struct Cmd <: AbstractCmd flags::UInt32 # libuv process flags env::Union{Vector{String},Nothing} dir::String + cpus::Union{Nothing,Vector{UInt16}} Cmd(exec::Vector{String}) = - new(exec, false, 0x00, nothing, "") - Cmd(cmd::Cmd, ignorestatus, flags, env, dir) = + new(exec, false, 0x00, nothing, "", nothing) + Cmd(cmd::Cmd, ignorestatus, flags, env, dir, cpus = nothing) = new(cmd.exec, ignorestatus, flags, env, - dir === cmd.dir ? dir : cstr(dir)) + dir === cmd.dir ? dir : cstr(dir), cpus) function Cmd(cmd::Cmd; ignorestatus::Bool=cmd.ignorestatus, env=cmd.env, dir::AbstractString=cmd.dir, + cpus::Union{Nothing,Vector{UInt16}} = cmd.cpus, detach::Bool = 0 != cmd.flags & UV_PROCESS_DETACHED, windows_verbatim::Bool = 0 != cmd.flags & UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS, windows_hide::Bool = 0 != cmd.flags & UV_PROCESS_WINDOWS_HIDE) @@ -26,7 +28,7 @@ struct Cmd <: AbstractCmd windows_verbatim * UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS | windows_hide * UV_PROCESS_WINDOWS_HIDE new(cmd.exec, ignorestatus, flags, byteenv(env), - dir === cmd.dir ? dir : cstr(dir)) + dir === cmd.dir ? dir : cstr(dir), cpus) end end @@ -34,7 +36,8 @@ has_nondefault_cmd_flags(c::Cmd) = c.ignorestatus || c.flags != 0x00 || c.env !== nothing || - c.dir !== "" + c.dir !== "" || + c.cpus !== nothing """ Cmd(cmd::Cmd; ignorestatus, detach, windows_verbatim, windows_hide, env, dir) @@ -63,7 +66,7 @@ while changing the settings of the optional keyword arguments: array or tuple of `"var"=>val` pairs. In order to modify (rather than replace) the existing environment, initialize `env` with `copy(ENV)` and then set `env["var"]=val` as desired. To add to an environment block within a `Cmd` object without replacing all - elements, use `addenv()` which will return a `Cmd` object with the updated environment. + elements, use [`addenv()`](@ref) which will return a `Cmd` object with the updated environment. * `dir::AbstractString`: Specify a working directory for the command (instead of the current directory). @@ -103,13 +106,19 @@ shell_escape(cmd::Cmd; special::AbstractString="") = shell_escape(cmd.exec..., special=special) shell_escape_posixly(cmd::Cmd) = shell_escape_posixly(cmd.exec...) -shell_escape_winsomely(cmd::Cmd) = - shell_escape_winsomely(cmd.exec...) +shell_escape_csh(cmd::Cmd) = + shell_escape_csh(cmd.exec...) +escape_microsoft_c_args(cmd::Cmd) = + escape_microsoft_c_args(cmd.exec...) +escape_microsoft_c_args(io::IO, cmd::Cmd) = + escape_microsoft_c_args(io::IO, cmd.exec...) function show(io::IO, cmd::Cmd) print_env = cmd.env !== nothing print_dir = !isempty(cmd.dir) (print_env || print_dir) && print(io, "setenv(") + print_cpus = cmd.cpus !== nothing + print_cpus && print(io, "setcpuaffinity(") print(io, '`') join(io, map(cmd.exec) do arg replace(sprint(context=io) do io @@ -119,6 +128,11 @@ function show(io::IO, cmd::Cmd) end, '`' => "\\`") end, ' ') print(io, '`') + if print_cpus + print(io, ", ") + show(io, collect(Int, something(cmd.cpus))) + print(io, ")") + end print_env && (print(io, ","); show(io, cmd.env)) print_dir && (print(io, "; dir="); show(io, cmd.dir)) (print_dir || print_env) && print(io, ")") @@ -163,6 +177,7 @@ rawhandle(x::OS_HANDLE) = x if OS_HANDLE !== RawFD rawhandle(x::RawFD) = Libc._get_osfhandle(x) end +setup_stdio(stdio::Union{DevNull,OS_HANDLE,RawFD}, ::Bool) = (stdio, false) const Redirectable = Union{IO, FileRedirect, RawFD, OS_HANDLE} const StdIOSet = NTuple{3, Redirectable} @@ -228,48 +243,109 @@ byteenv(env::Union{AbstractVector{Pair{T,V}}, Tuple{Vararg{Pair{T,V}}}}) where { String[cstr(k*"="*string(v)) for (k,v) in env] """ - setenv(command::Cmd, env; dir="") + setenv(command::Cmd, env; dir) Set environment variables to use when running the given `command`. `env` is either a dictionary mapping strings to strings, an array of strings of the form `"var=val"`, or zero or more `"var"=>val` pair arguments. In order to modify (rather than replace) the existing environment, create `env` through `copy(ENV)` and then setting `env["var"]=val` -as desired, or use `addenv`. +as desired, or use [`addenv`](@ref). The `dir` keyword argument can be used to specify a working directory for the command. +`dir` defaults to the currently set `dir` for `command` (which is the current working +directory if not specified already). + +See also [`Cmd`](@ref), [`addenv`](@ref), [`ENV`](@ref), [`pwd`](@ref). """ -setenv(cmd::Cmd, env; dir="") = Cmd(cmd; env=byteenv(env), dir=dir) -setenv(cmd::Cmd, env::Pair{<:AbstractString}...; dir="") = +setenv(cmd::Cmd, env; dir=cmd.dir) = Cmd(cmd; env=byteenv(env), dir=dir) +setenv(cmd::Cmd, env::Pair{<:AbstractString}...; dir=cmd.dir) = setenv(cmd, env; dir=dir) -setenv(cmd::Cmd; dir="") = Cmd(cmd; dir=dir) +setenv(cmd::Cmd; dir=cmd.dir) = Cmd(cmd; dir=dir) + +# split environment entry string into before and after first `=` (key and value) +function splitenv(e::String) + i = findnext('=', e, 2) + if i === nothing + throw(ArgumentError("malformed environment entry")) + end + e[1:prevind(e, i)], e[nextind(e, i):end] +end """ - addenv(command::Cmd, env...) + addenv(command::Cmd, env...; inherit::Bool = true) + +Merge new environment mappings into the given [`Cmd`](@ref) object, returning a new `Cmd` object. +Duplicate keys are replaced. If `command` does not contain any environment values set already, +it inherits the current environment at time of `addenv()` call if `inherit` is `true`. +Keys with value `nothing` are deleted from the env. + +See also [`Cmd`](@ref), [`setenv`](@ref), [`ENV`](@ref). -Merge new environment mappings into the given `Cmd` object, returning a new `Cmd` object. -Duplicate keys are replaced. +!!! compat "Julia 1.6" + This function requires Julia 1.6 or later. """ -function addenv(cmd::Cmd, env::Dict) +function addenv(cmd::Cmd, env::Dict; inherit::Bool = true) new_env = Dict{String,String}() - if cmd.env !== nothing - for (k, v) in split.(cmd.env, "=") + if cmd.env === nothing + if inherit + merge!(new_env, ENV) + end + else + for (k, v) in splitenv.(cmd.env) new_env[string(k)::String] = string(v)::String end end for (k, v) in env - new_env[string(k)::String] = string(v)::String + if v === nothing + delete!(new_env, string(k)::String) + else + new_env[string(k)::String] = string(v)::String + end end return setenv(cmd, new_env) end -function addenv(cmd::Cmd, pairs::Pair{<:AbstractString}...) - return addenv(cmd, Dict(k => v for (k, v) in pairs)) +function addenv(cmd::Cmd, pairs::Pair{<:AbstractString}...; inherit::Bool = true) + return addenv(cmd, Dict(k => v for (k, v) in pairs); inherit) end -function addenv(cmd::Cmd, env::Vector{<:AbstractString}) - return addenv(cmd, Dict(k => v for (k, v) in split.(env, "="))) +function addenv(cmd::Cmd, env::Vector{<:AbstractString}; inherit::Bool = true) + return addenv(cmd, Dict(k => v for (k, v) in splitenv.(env)); inherit) end +""" + setcpuaffinity(original_command::Cmd, cpus) -> command::Cmd + +Set the CPU affinity of the `command` by a list of CPU IDs (1-based) `cpus`. Passing +`cpus = nothing` means to unset the CPU affinity if the `original_command` has any. + +This function is supported only in Linux and Windows. It is not supported in macOS because +libuv does not support affinity setting. + +!!! compat "Julia 1.8" + This function requires at least Julia 1.8. + +# Examples + +In Linux, the `taskset` command line program can be used to see how `setcpuaffinity` works. + +```julia +julia> run(setcpuaffinity(`sh -c 'taskset -p \$\$'`, [1, 2, 5])); +pid 2273's current affinity mask: 13 +``` + +Note that the mask value `13` reflects that the first, second, and the fifth bits (counting +from the least significant position) are turned on: + +```julia +julia> 0b010011 +0x13 +``` +""" +function setcpuaffinity end +setcpuaffinity(cmd::Cmd, ::Nothing) = Cmd(cmd; cpus = nothing) +setcpuaffinity(cmd::Cmd, cpus) = Cmd(cmd; cpus = collect(UInt16, cpus)) + (&)(left::AbstractCmd, right::AbstractCmd) = AndCmds(left, right) redir_out(src::AbstractCmd, dest::AbstractCmd) = OrCmds(src, dest) redir_err(src::AbstractCmd, dest::AbstractCmd) = ErrOrCmds(src, dest) diff --git a/base/combinatorics.jl b/base/combinatorics.jl index 9469452735da23..2dd69fbce4c42b 100644 --- a/base/combinatorics.jl +++ b/base/combinatorics.jl @@ -91,7 +91,7 @@ function isperm(P::Tuple) end end -isperm(P::Any16) = _isperm(P) +isperm(P::Any32) = _isperm(P) # swap columns i and j of a, in-place function swapcols!(a::AbstractMatrix, i, j) @@ -103,6 +103,18 @@ function swapcols!(a::AbstractMatrix, i, j) @inbounds a[k,i],a[k,j] = a[k,j],a[k,i] end end + +# swap rows i and j of a, in-place +function swaprows!(a::AbstractMatrix, i, j) + i == j && return + rows = axes(a,1) + @boundscheck i in rows || throw(BoundsError(a, (:,i))) + @boundscheck j in rows || throw(BoundsError(a, (:,j))) + for k in axes(a,2) + @inbounds a[i,k],a[j,k] = a[j,k],a[i,k] + end +end + # like permute!! applied to each row of a, in-place in a (overwriting p). function permutecols!!(a::AbstractMatrix, p::AbstractVector{<:Integer}) require_one_based_indexing(a, p) @@ -228,8 +240,15 @@ invpermute!(a, p::AbstractVector) = invpermute!!(a, copymutable(p)) Return the inverse permutation of `v`. If `B = A[v]`, then `A == B[invperm(v)]`. +See also [`sortperm`](@ref), [`invpermute!`](@ref), [`isperm`](@ref), [`permutedims`](@ref). + # Examples ```jldoctest +julia> p = (2, 3, 1); + +julia> invperm(p) +(3, 1, 2) + julia> v = [2; 4; 3; 1]; julia> invperm(v) @@ -286,7 +305,7 @@ function invperm(P::Tuple) end end -invperm(P::Any16) = Tuple(invperm(collect(P))) +invperm(P::Any32) = Tuple(invperm(collect(P))) #XXX This function should be moved to Combinatorics.jl but is currently used by Base.DSP. """ @@ -307,7 +326,7 @@ julia> 2^2 * 3^3 !!! compat "Julia 1.6" The method that accepts a tuple requires Julia 1.6 or later. """ -function nextprod(a::Union{Tuple{Vararg{<:Integer}},AbstractVector{<:Integer}}, x::Real) +function nextprod(a::Union{Tuple{Vararg{Integer}},AbstractVector{<:Integer}}, x::Real) if x > typemax(Int) throw(ArgumentError("unsafe for x > typemax(Int), got $x")) end diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index d4f92a3b8176f3..36ab6b81f47a06 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -4,160 +4,226 @@ # constants # ############# -const CoreNumType = Union{Int32, Int64, Float32, Float64} - const _REF_NAME = Ref.body.name ######### # logic # ######### -# see if the inference result might affect the final answer -call_result_unused(frame::InferenceState, pc::LineNum=frame.currpc) = - isexpr(frame.src.code[frame.currpc], :call) && isempty(frame.ssavalue_uses[pc]) +# See if the inference result of the current statement's result value might affect +# the final answer for the method (aside from optimization potential and exceptions). +# To do that, we need to check both for slot assignment and SSA usage. +call_result_unused(frame::InferenceState) = + isexpr(frame.src.code[frame.currpc], :call) && isempty(frame.ssavalue_uses[frame.currpc]) +function get_max_methods(mod::Module, interp::AbstractInterpreter) + max_methods = ccall(:jl_get_module_max_methods, Cint, (Any,), mod) % Int + max_methods < 0 ? InferenceParams(interp).MAX_METHODS : max_methods +end -function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), argtypes::Vector{Any}, @nospecialize(atype), sv::InferenceState, - max_methods::Int = InferenceParams(interp).MAX_METHODS) - if sv.params.unoptimize_throw_blocks && sv.currpc in sv.throw_blocks - return CallMeta(Any, false) +function get_max_methods(@nospecialize(f), mod::Module, interp::AbstractInterpreter) + if f !== nothing + fmm = typeof(f).name.max_methods + fmm !== UInt8(0) && return Int(fmm) end - valid_worlds = WorldRange() - atype_params = unwrap_unionall(atype).parameters - splitunions = 1 < countunionsplit(atype_params) <= InferenceParams(interp).MAX_UNION_SPLITTING - mts = Core.MethodTable[] - fullmatch = Bool[] - if splitunions - splitsigs = switchtupleunion(atype) - applicable = Any[] - infos = MethodMatchInfo[] - for sig_n in splitsigs - mt = ccall(:jl_method_table_for, Any, (Any,), sig_n) - if mt === nothing - add_remark!(interp, sv, "Could not identify method table for call") - return CallMeta(Any, false) - end - mt = mt::Core.MethodTable - matches = findall(sig_n, method_table(interp); limit=max_methods) - if matches === missing - add_remark!(interp, sv, "For one of the union split cases, too many methods matched") - return CallMeta(Any, false) - end - push!(infos, MethodMatchInfo(matches)) - append!(applicable, matches) - valid_worlds = intersect(valid_worlds, matches.valid_worlds) - thisfullmatch = _any(match->(match::MethodMatch).fully_covers, matches) - found = false - for (i, mt′) in enumerate(mts) - if mt′ === mt - fullmatch[i] &= thisfullmatch - found = true - break - end - end - if !found - push!(mts, mt) - push!(fullmatch, thisfullmatch) + return get_max_methods(mod, interp) +end + +const empty_bitset = BitSet() + +function should_infer_for_effects(sv::InferenceState) + sv.ipo_effects.terminates === ALWAYS_TRUE && + sv.ipo_effects.effect_free === ALWAYS_TRUE +end + +function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), + arginfo::ArgInfo, @nospecialize(atype), + sv::InferenceState, max_methods::Int) + if !should_infer_for_effects(sv) && + sv.params.unoptimize_throw_blocks && + is_stmt_throw_block(get_curr_ssaflag(sv)) + # Disable inference of calls in throw blocks, since we're unlikely to + # need their types. There is one exception however: If up until now, the + # function has not seen any side effects, we would like to make sure there + # aren't any in the throw block either to enable other optimizations. + add_remark!(interp, sv, "Skipped call in throw block") + nonoverlayed = false + if isoverlayed(method_table(interp)) && is_nonoverlayed(sv.ipo_effects) + # as we may want to concrete-evaluate this frame in cases when there are + # no overlayed calls, try an additional effort now to check if this call + # isn't overlayed rather than just handling it conservatively + matches = find_matching_methods(arginfo.argtypes, atype, method_table(interp), + InferenceParams(interp).MAX_UNION_SPLITTING, max_methods) + if !isa(matches, FailedMethodMatch) + nonoverlayed = matches.nonoverlayed end + else + nonoverlayed = true end - info = UnionSplitInfo(infos) - else - mt = ccall(:jl_method_table_for, Any, (Any,), atype) - if mt === nothing - add_remark!(interp, sv, "Could not identify method table for call") - return CallMeta(Any, false) - end - mt = mt::Core.MethodTable - matches = findall(atype, method_table(interp, sv); limit=max_methods) - if matches === missing - # this means too many methods matched - # (assume this will always be true, so we don't compute / update valid age in this case) - add_remark!(interp, sv, "Too many methods matched") - return CallMeta(Any, false) - end - push!(mts, mt) - push!(fullmatch, _any(match->(match::MethodMatch).fully_covers, matches)) - info = MethodMatchInfo(matches) - applicable = matches.matches - valid_worlds = matches.valid_worlds + # At this point we are guaranteed to end up throwing on this path, + # which is all that's required for :consistent-cy. Of course, we don't + # know anything else about this statement. + tristate_merge!(sv, Effects(; consistent=ALWAYS_TRUE, nonoverlayed)) + return CallMeta(Any, false) end + + argtypes = arginfo.argtypes + matches = find_matching_methods(argtypes, atype, method_table(interp), + InferenceParams(interp).MAX_UNION_SPLITTING, max_methods) + if isa(matches, FailedMethodMatch) + add_remark!(interp, sv, matches.reason) + tristate_merge!(sv, Effects()) + return CallMeta(Any, false) + end + + (; valid_worlds, applicable, info) = matches update_valid_age!(sv, valid_worlds) - applicable = applicable::Array{Any,1} napplicable = length(applicable) rettype = Bottom - edgecycle = false - edges = Any[] - nonbot = 0 # the index of the only non-Bottom inference result if > 0 - seen = 0 # number of signatures actually inferred - istoplevel = sv.linfo.def isa Module + edges = MethodInstance[] + conditionals = nothing # keeps refinement information of call argument types when the return type is boolean + seen = 0 # number of signatures actually inferred + any_const_result = false + const_results = Union{Nothing,ConstResult}[] multiple_matches = napplicable > 1 - - if f !== nothing && napplicable == 1 && is_method_pure(applicable[1]::MethodMatch) - val = pure_eval_call(f, argtypes) - if val !== false - # TODO: add some sort of edge(s) - return CallMeta(val, MethodResultPure()) - end + fargs = arginfo.fargs + all_effects = EFFECTS_TOTAL + if !matches.nonoverlayed + # currently we don't have a good way to execute the overlayed method definition, + # so we should give up pure/concrete eval when any of the matched methods is overlayed + f = nothing + all_effects = Effects(all_effects; nonoverlayed=false) end + # try pure-evaluation + val = pure_eval_call(interp, f, applicable, arginfo, sv) + val !== nothing && return CallMeta(val, MethodResultPure(info)) # TODO: add some sort of edge(s) + for i in 1:napplicable match = applicable[i]::MethodMatch method = match.method sig = match.spec_types - if istoplevel && !isdispatchtuple(sig) + if bail_out_toplevel_call(interp, sig, sv) # only infer concrete call sites in top-level expressions add_remark!(interp, sv, "Refusing to infer non-concrete call site in top-level expression") rettype = Any break end - sigtuple = unwrap_unionall(sig)::DataType - splitunions = false this_rt = Bottom - # TODO: splitunions = 1 < countunionsplit(sigtuple.parameters) * napplicable <= InferenceParams(interp).MAX_UNION_SPLITTING - # currently this triggers a bug in inference recursion detection + splitunions = false + # TODO: this used to trigger a bug in inference recursion detection, and is unmaintained now + # sigtuple = unwrap_unionall(sig)::DataType + # splitunions = 1 < unionsplitcost(sigtuple.parameters) * napplicable <= InferenceParams(interp).MAX_UNION_SPLITTING if splitunions splitsigs = switchtupleunion(sig) for sig_n in splitsigs - rt, edgecycle1, edge = abstract_call_method(interp, method, sig_n, svec(), multiple_matches, sv) - if edge !== nothing - push!(edges, edge) + result = abstract_call_method(interp, method, sig_n, svec(), multiple_matches, sv) + rt = result.rt + edge = result.edge + edge !== nothing && push!(edges, edge) + this_argtypes = isa(matches, MethodMatches) ? argtypes : matches.applicable_argtypes[i] + this_arginfo = ArgInfo(fargs, this_argtypes) + const_call_result = abstract_call_method_with_const_args(interp, result, + f, this_arginfo, match, sv) + effects = result.edge_effects + const_result = nothing + if const_call_result !== nothing + const_rt = const_call_result.rt + if const_rt ⊑ rt + rt = const_rt + (; effects, const_result) = const_call_result + end end - edgecycle |= edgecycle1::Bool + all_effects = tristate_merge(all_effects, effects) + push!(const_results, const_result) + any_const_result |= const_result !== nothing this_rt = tmerge(this_rt, rt) - this_rt === Any && break + if bail_out_call(interp, this_rt, sv) + break + end end + this_conditional = ignorelimited(this_rt) + this_rt = widenwrappedconditional(this_rt) else - this_rt, edgecycle1, edge = abstract_call_method(interp, method, sig, match.sparams, multiple_matches, sv) - edgecycle |= edgecycle1::Bool - if edge !== nothing - push!(edges, edge) + if infer_compilation_signature(interp) + # Also infer the compilation signature for this method, so it's available + # to the compiler in case it ends up needing it (which is likely). + csig = get_compileable_sig(method, sig, match.sparams) + if csig !== nothing && csig !== sig + # The result of this inference is not directly used, so temporarily empty + # the use set for the current SSA value. + saved_uses = sv.ssavalue_uses[sv.currpc] + sv.ssavalue_uses[sv.currpc] = empty_bitset + abstract_call_method(interp, method, csig, match.sparams, multiple_matches, sv) + sv.ssavalue_uses[sv.currpc] = saved_uses + end end - end - if this_rt !== Bottom - if nonbot === 0 - nonbot = i - else - nonbot = -1 + + result = abstract_call_method(interp, method, sig, match.sparams, multiple_matches, sv) + this_conditional = ignorelimited(result.rt) + this_rt = widenwrappedconditional(result.rt) + edge = result.edge + edge !== nothing && push!(edges, edge) + # try constant propagation with argtypes for this match + # this is in preparation for inlining, or improving the return result + this_argtypes = isa(matches, MethodMatches) ? argtypes : matches.applicable_argtypes[i] + this_arginfo = ArgInfo(fargs, this_argtypes) + const_call_result = abstract_call_method_with_const_args(interp, result, + f, this_arginfo, match, sv) + effects = result.edge_effects + const_result = nothing + if const_call_result !== nothing + this_const_conditional = ignorelimited(const_call_result.rt) + this_const_rt = widenwrappedconditional(const_call_result.rt) + # return type of const-prop' inference can be wider than that of non const-prop' inference + # e.g. in cases when there are cycles but cached result is still accurate + if this_const_rt ⊑ this_rt + this_conditional = this_const_conditional + this_rt = this_const_rt + (; effects, const_result) = const_call_result + end end + all_effects = tristate_merge(all_effects, effects) + push!(const_results, const_result) + any_const_result |= const_result !== nothing end + @assert !(this_conditional isa Conditional) "invalid lattice element returned from inter-procedural context" seen += 1 rettype = tmerge(rettype, this_rt) - rettype === Any && break - end - # try constant propagation if only 1 method is inferred to non-Bottom - # this is in preparation for inlining, or improving the return result - is_unused = call_result_unused(sv) - if nonbot > 0 && seen == napplicable && (!edgecycle || !is_unused) && isa(rettype, Type) && InferenceParams(interp).ipo_constant_propagation - # if there's a possibility we could constant-propagate a better result - # (hopefully without doing too much work), try to do that now - # TODO: it feels like this could be better integrated into abstract_call_method / typeinf_edge - const_rettype = abstract_call_method_with_const_args(interp, rettype, f, argtypes, applicable[nonbot]::MethodMatch, sv, edgecycle) - if const_rettype ⊑ rettype - # use the better result, if it's a refinement of rettype - rettype = const_rettype - end - end - if is_unused && !(rettype === Bottom) + if this_conditional !== Bottom && is_lattice_bool(rettype) && fargs !== nothing + if conditionals === nothing + conditionals = Any[Bottom for _ in 1:length(argtypes)], + Any[Bottom for _ in 1:length(argtypes)] + end + for i = 1:length(argtypes) + cnd = conditional_argtype(this_conditional, sig, argtypes, i) + conditionals[1][i] = tmerge(conditionals[1][i], cnd.vtype) + conditionals[2][i] = tmerge(conditionals[2][i], cnd.elsetype) + end + end + if bail_out_call(interp, rettype, sv) + break + end + end + + if any_const_result && seen == napplicable + @assert napplicable == nmatches(info) == length(const_results) + info = ConstCallInfo(info, const_results) + end + + if seen != napplicable + # there may be unanalyzed effects within unseen dispatch candidate, + # but we can still ignore nonoverlayed effect here since we already accounted for it + all_effects = tristate_merge(all_effects, EFFECTS_UNKNOWN) + elseif isa(matches, MethodMatches) ? (!matches.fullmatch || any_ambig(matches)) : + (!_all(b->b, matches.fullmatches) || any_ambig(matches)) + # Account for the fact that we may encounter a MethodError with a non-covered or ambiguous signature. + all_effects = Effects(all_effects; nothrow=TRISTATE_UNKNOWN) + end + + rettype = from_interprocedural!(rettype, sv, arginfo, conditionals) + + if call_result_unused(sv) && !(rettype === Bottom) add_remark!(interp, sv, "Call result type was widened because the return value is unused") # We're mainly only here because the optimizer might want this code, # but we ourselves locally don't typically care about it locally @@ -167,178 +233,278 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), # and avoid keeping track of a more complex result type. rettype = Any end - if !(rettype === Any) # adding a new method couldn't refine (widen) this type - for edge in edges - add_backedge!(edge::MethodInstance, sv) - end - for (thisfullmatch, mt) in zip(fullmatch, mts) - if !thisfullmatch - # also need an edge to the method table in case something gets - # added that did not intersect with any existing method - add_mt_backedge!(mt, atype, sv) - end + add_call_backedges!(interp, rettype, all_effects, edges, matches, atype, sv) + if !isempty(sv.pclimitations) # remove self, if present + delete!(sv.pclimitations, sv) + for caller in sv.callers_in_cycle + delete!(sv.pclimitations, caller) end end - #print("=> ", rettype, "\n") + tristate_merge!(sv, all_effects) return CallMeta(rettype, info) end +struct FailedMethodMatch + reason::String +end -function const_prop_profitable(@nospecialize(arg)) - # have new information from argtypes that wasn't available from the signature - if isa(arg, PartialStruct) - for b in arg.fields - isconstType(b) && return true - const_prop_profitable(b) && return true - end - elseif !isa(arg, Const) || (isa(arg.val, Symbol) || isa(arg.val, Type) || (!isa(arg.val, String) && !ismutable(arg.val))) - # don't consider mutable values or Strings useful constants - return true - end - return false +struct MethodMatches + applicable::Vector{Any} + info::MethodMatchInfo + valid_worlds::WorldRange + mt::Core.MethodTable + fullmatch::Bool + nonoverlayed::Bool end +any_ambig(info::MethodMatchInfo) = info.results.ambig +any_ambig(m::MethodMatches) = any_ambig(m.info) -# This is a heuristic to avoid trying to const prop through complicated functions -# where we would spend a lot of time, but are probably unliekly to get an improved -# result anyway. -function const_prop_heuristic(interp::AbstractInterpreter, method::Method, mi::MethodInstance) - # Peek at the inferred result for the function to determine if the optimizer - # was able to cut it down to something simple (inlineable in particular). - # If so, there's a good chance we might be able to const prop all the way - # through and learn something new. - code = get(code_cache(interp), mi, nothing) - declared_inline = isdefined(method, :source) && ccall(:jl_ir_flag_inlineable, Bool, (Any,), method.source) - cache_inlineable = declared_inline - if isdefined(code, :inferred) && !cache_inlineable - cache_inf = code.inferred - if !(cache_inf === nothing) - cache_src_inferred = ccall(:jl_ir_flag_inferred, Bool, (Any,), cache_inf) - cache_src_inlineable = ccall(:jl_ir_flag_inlineable, Bool, (Any,), cache_inf) - cache_inlineable = cache_src_inferred && cache_src_inlineable - end - end - if !cache_inlineable - return false - end - return true +struct UnionSplitMethodMatches + applicable::Vector{Any} + applicable_argtypes::Vector{Vector{Any}} + info::UnionSplitInfo + valid_worlds::WorldRange + mts::Vector{Core.MethodTable} + fullmatches::Vector{Bool} + nonoverlayed::Bool end +any_ambig(m::UnionSplitMethodMatches) = _any(any_ambig, m.info.matches) -function abstract_call_method_with_const_args(interp::AbstractInterpreter, @nospecialize(rettype), @nospecialize(f), argtypes::Vector{Any}, match::MethodMatch, sv::InferenceState, edgecycle::Bool) - method = match.method - nargs::Int = method.nargs - method.isva && (nargs -= 1) - length(argtypes) >= nargs || return Any - haveconst = false - allconst = true - # see if any or all of the arguments are constant and propagating constants may be worthwhile - for a in argtypes - a = widenconditional(a) - if allconst && !isa(a, Const) && !isconstType(a) && !isa(a, PartialStruct) - allconst = false +function find_matching_methods(argtypes::Vector{Any}, @nospecialize(atype), method_table::MethodTableView, + union_split::Int, max_methods::Int) + # NOTE this is valid as far as any "constant" lattice element doesn't represent `Union` type + if 1 < unionsplitcost(argtypes) <= union_split + split_argtypes = switchtupleunion(argtypes) + infos = MethodMatchInfo[] + applicable = Any[] + applicable_argtypes = Vector{Any}[] # arrays like `argtypes`, including constants, for each match + valid_worlds = WorldRange() + mts = Core.MethodTable[] + fullmatches = Bool[] + nonoverlayed = true + for i in 1:length(split_argtypes) + arg_n = split_argtypes[i]::Vector{Any} + sig_n = argtypes_to_type(arg_n) + mt = ccall(:jl_method_table_for, Any, (Any,), sig_n) + mt === nothing && return FailedMethodMatch("Could not identify method table for call") + mt = mt::Core.MethodTable + result = findall(sig_n, method_table; limit = max_methods) + if result === missing + return FailedMethodMatch("For one of the union split cases, too many methods matched") + end + matches, overlayed = result + nonoverlayed &= !overlayed + push!(infos, MethodMatchInfo(matches)) + for m in matches + push!(applicable, m) + push!(applicable_argtypes, arg_n) + end + valid_worlds = intersect(valid_worlds, matches.valid_worlds) + thisfullmatch = _any(match->(match::MethodMatch).fully_covers, matches) + found = false + for (i, mt′) in enumerate(mts) + if mt′ === mt + fullmatches[i] &= thisfullmatch + found = true + break + end + end + if !found + push!(mts, mt) + push!(fullmatches, thisfullmatch) + end end - if !haveconst && has_nontrivial_const_info(a) && const_prop_profitable(a) - haveconst = true + return UnionSplitMethodMatches(applicable, + applicable_argtypes, + UnionSplitInfo(infos), + valid_worlds, + mts, + fullmatches, + nonoverlayed) + else + mt = ccall(:jl_method_table_for, Any, (Any,), atype) + if mt === nothing + return FailedMethodMatch("Could not identify method table for call") end - if haveconst && !allconst - break + mt = mt::Core.MethodTable + result = findall(atype, method_table; limit = max_methods) + if result === missing + # this means too many methods matched + # (assume this will always be true, so we don't compute / update valid age in this case) + return FailedMethodMatch("Too many methods matched") + end + matches, overlayed = result + fullmatch = _any(match->(match::MethodMatch).fully_covers, matches) + return MethodMatches(matches.matches, + MethodMatchInfo(matches), + matches.valid_worlds, + mt, + fullmatch, + !overlayed) + end +end + +""" + from_interprocedural!(rt, sv::InferenceState, arginfo::ArgInfo, maybecondinfo) -> newrt + +Converts inter-procedural return type `rt` into a local lattice element `newrt`, +that is appropriate in the context of current local analysis frame `sv`, especially: +- unwraps `rt::LimitedAccuracy` and collects its limitations into the current frame `sv` +- converts boolean `rt` to new boolean `newrt` in a way `newrt` can propagate extra conditional + refinement information, e.g. translating `rt::InterConditional` into `newrt::Conditional` + that holds a type constraint information about a variable in `sv` + +This function _should_ be used wherever we propagate results returned from +`abstract_call_method` or `abstract_call_method_with_const_args`. + +When `maybecondinfo !== nothing`, this function also tries extra conditional argument type refinement. +In such cases `maybecondinfo` should be either of: +- `maybecondinfo::Tuple{Vector{Any},Vector{Any}}`: precomputed argument type refinement information +- method call signature tuple type +When we deal with multiple `MethodMatch`es, it's better to precompute `maybecondinfo` by +`tmerge`ing argument signature type of each method call. +""" +function from_interprocedural!(@nospecialize(rt), sv::InferenceState, arginfo::ArgInfo, @nospecialize(maybecondinfo)) + rt = collect_limitations!(rt, sv) + if is_lattice_bool(rt) + if maybecondinfo === nothing + rt = widenconditional(rt) + else + rt = from_interconditional(rt, sv, arginfo, maybecondinfo) end end - haveconst || improvable_via_constant_propagation(rettype) || return Any - if nargs > 1 - if istopfunction(f, :getindex) || istopfunction(f, :setindex!) - arrty = argtypes[2] - # don't propagate constant index into indexing of non-constant array - if arrty isa Type && arrty <: AbstractArray && !issingletontype(arrty) - return Any - elseif arrty ⊑ Array - return Any + @assert !(rt isa InterConditional) "invalid lattice element returned from inter-procedural context" + return rt +end + +function collect_limitations!(@nospecialize(typ), sv::InferenceState) + if isa(typ, LimitedAccuracy) + union!(sv.pclimitations, typ.causes) + return typ.typ + end + return typ +end + +function from_interconditional(@nospecialize(typ), sv::InferenceState, (; fargs, argtypes)::ArgInfo, @nospecialize(maybecondinfo)) + fargs === nothing && return widenconditional(typ) + slot = 0 + vtype = elsetype = Any + condval = maybe_extract_const_bool(typ) + for i in 1:length(fargs) + # find the first argument which supports refinement, + # and intersect all equivalent arguments with it + arg = ssa_def_slot(fargs[i], sv) + arg isa SlotNumber || continue # can't refine + old = argtypes[i] + old isa Type || continue # unlikely to refine + id = slot_id(arg) + if slot == 0 || id == slot + if isa(maybecondinfo, Tuple{Vector{Any},Vector{Any}}) + # if we have already computed argument refinement information, apply that now to get the result + new_vtype = maybecondinfo[1][i] + new_elsetype = maybecondinfo[2][i] + else + # otherwise compute it on the fly + cnd = conditional_argtype(typ, maybecondinfo, argtypes, i) + new_vtype = cnd.vtype + new_elsetype = cnd.elsetype end - elseif istopfunction(f, :iterate) - itrty = argtypes[2] - if itrty ⊑ Array - return Any + if condval === false + vtype = Bottom + elseif new_vtype ⊑ vtype + vtype = new_vtype + else + vtype = tmeet(vtype, widenconst(new_vtype)) + end + if condval === true + elsetype = Bottom + elseif new_elsetype ⊑ elsetype + elsetype = new_elsetype + else + elsetype = tmeet(elsetype, widenconst(new_elsetype)) + end + if (slot > 0 || condval !== false) && vtype ⋤ old + slot = id + elseif (slot > 0 || condval !== true) && elsetype ⋤ old + slot = id + else # reset: no new useful information for this slot + vtype = elsetype = Any + if slot > 0 + slot = 0 + end end end end - if !allconst && (istopfunction(f, :+) || istopfunction(f, :-) || istopfunction(f, :*) || - istopfunction(f, :(==)) || istopfunction(f, :!=) || - istopfunction(f, :<=) || istopfunction(f, :>=) || istopfunction(f, :<) || istopfunction(f, :>) || - istopfunction(f, :<<) || istopfunction(f, :>>)) - return Any + if vtype === Bottom && elsetype === Bottom + return Bottom # accidentally proved this call to be dead / throw ! + elseif slot > 0 + return Conditional(SlotNumber(slot), vtype, elsetype) # record a Conditional improvement to this slot end - force_inference = allconst || InferenceParams(interp).aggressive_constant_propagation - if istopfunction(f, :getproperty) || istopfunction(f, :setproperty!) - force_inference = true + return widenconditional(typ) +end + +function conditional_argtype(@nospecialize(rt), @nospecialize(sig), argtypes::Vector{Any}, i::Int) + if isa(rt, InterConditional) && rt.slot == i + return rt + else + vtype = elsetype = tmeet(argtypes[i], fieldtype(sig, i)) + condval = maybe_extract_const_bool(rt) + condval === true && (elsetype = Bottom) + condval === false && (vtype = Bottom) + return InterConditional(i, vtype, elsetype) end - mi = specialize_method(match, !force_inference) - mi === nothing && return Any - mi = mi::MethodInstance - # decide if it's likely to be worthwhile - if !force_inference && !const_prop_heuristic(interp, method, mi) - return Any +end + +function add_call_backedges!(interp::AbstractInterpreter, + @nospecialize(rettype), all_effects::Effects, + edges::Vector{MethodInstance}, matches::Union{MethodMatches,UnionSplitMethodMatches}, @nospecialize(atype), + sv::InferenceState) + # we don't need to add backedges when: + # - a new method couldn't refine (widen) this type and + # - the effects are known to not provide any useful IPO information + if rettype === Any + if !isoverlayed(method_table(interp)) + # we can ignore the `nonoverlayed` property if `interp` doesn't use + # overlayed method table at all since it will never be tainted anyway + all_effects = Effects(all_effects; nonoverlayed=false) + end + if all_effects === Effects() + return + end end - inf_cache = get_inference_cache(interp) - inf_result = cache_lookup(mi, argtypes, inf_cache) - if inf_result === nothing - if edgecycle - # if there might be a cycle, check to make sure we don't end up - # calling ourselves here. - infstate = sv - cyclei = 0 - while !(infstate === nothing) - if method === infstate.linfo.def && any(infstate.result.overridden_by_const) - return Any - end - if cyclei < length(infstate.callers_in_cycle) - cyclei += 1 - infstate = infstate.callers_in_cycle[cyclei] - else - cyclei = 0 - infstate = infstate.parent - end - end + for edge in edges + add_backedge!(edge, sv) + end + # also need an edge to the method table in case something gets + # added that did not intersect with any existing method + if isa(matches, MethodMatches) + matches.fullmatch || add_mt_backedge!(matches.mt, atype, sv) + else + for (thisfullmatch, mt) in zip(matches.fullmatches, matches.mts) + thisfullmatch || add_mt_backedge!(mt, atype, sv) end - inf_result = InferenceResult(mi, argtypes) - frame = InferenceState(inf_result, #=cache=#false, interp) - frame === nothing && return Any # this is probably a bad generated function (unsound), but just ignore it - frame.limited = true - frame.parent = sv - push!(inf_cache, inf_result) - typeinf(interp, frame) || return Any end - result = inf_result.result - # if constant inference hits a cycle, just bail out - isa(result, InferenceState) && return Any - add_backedge!(inf_result.linfo, sv) - return result end const RECURSION_UNUSED_MSG = "Bounded recursion detected with unused result. Annotated return type may be wider than true result." +const RECURSION_MSG = "Bounded recursion detected. Call was widened to force convergence." function abstract_call_method(interp::AbstractInterpreter, method::Method, @nospecialize(sig), sparams::SimpleVector, hardlimit::Bool, sv::InferenceState) if method.name === :depwarn && isdefined(Main, :Base) && method.module === Main.Base add_remark!(interp, sv, "Refusing to infer into `depwarn`") - return Any, false, nothing + return MethodCallResult(Any, false, false, nothing, Effects()) end topmost = nothing # Limit argument type tuple growth of functions: # look through the parents list to see if there's a call to the same method # and from the same method. # Returns the topmost occurrence of that repeated edge. - cyclei = 0 - infstate = sv edgecycle = false - # The `method_for_inference_heuristics` will expand the given method's generator if - # necessary in order to retrieve this field from the generated `CodeInfo`, if it exists. - # The other `CodeInfo`s we inspect will already have this field inflated, so we just - # access it directly instead (to avoid regeneration). - method2 = method_for_inference_heuristics(method, sig, sparams) # Union{Method, Nothing} - sv_method2 = sv.src.method_for_inference_limit_heuristics # limit only if user token match - sv_method2 isa Method || (sv_method2 = nothing) # Union{Method, Nothing} - while !(infstate === nothing) - infstate = infstate::InferenceState + edgelimited = false + + for infstate in InfStackUnwind(sv) if method === infstate.linfo.def - if infstate.linfo.specTypes == sig + if infstate.linfo.specTypes::Type == sig::Type # avoid widening when detecting self-recursion # TODO: merge call cycle and return right away if call_result_unused(sv) @@ -347,127 +513,578 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp # we have a self-cycle in the call-graph, but not in the inference graph (typically): # break this edge now (before we record it) by returning early # (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases) - return Any, true, nothing + return MethodCallResult(Any, true, true, nothing, Effects()) end topmost = nothing edgecycle = true break end - inf_method2 = infstate.src.method_for_inference_limit_heuristics # limit only if user token match - inf_method2 isa Method || (inf_method2 = nothing) # Union{Method, Nothing} - if topmost === nothing && method2 === inf_method2 - if hardlimit - topmost = infstate - edgecycle = true - else - # if this is a soft limit, - # also inspect the parent of this edge, - # to see if they are the same Method as sv - # in which case we'll need to ensure it is convergent - # otherwise, we don't - for parent in infstate.callers_in_cycle - # check in the cycle list first - # all items in here are mutual parents of all others - parent_method2 = parent.src.method_for_inference_limit_heuristics # limit only if user token match - parent_method2 isa Method || (parent_method2 = nothing) # Union{Method, Nothing} - if parent.linfo.def === sv.linfo.def && sv_method2 === parent_method2 - topmost = infstate - edgecycle = true - break - end - end - let parent = infstate.parent - # then check the parent link - if topmost === nothing && parent !== nothing - parent = parent::InferenceState - parent_method2 = parent.src.method_for_inference_limit_heuristics # limit only if user token match - parent_method2 isa Method || (parent_method2 = nothing) # Union{Method, Nothing} - if (parent.cached || parent.limited) && parent.linfo.def === sv.linfo.def && sv_method2 === parent_method2 - topmost = infstate - edgecycle = true - end - end - end + topmost === nothing || continue + if edge_matches_sv(infstate, method, sig, sparams, hardlimit, sv) + topmost = infstate + edgecycle = true + end + end + end + + if topmost !== nothing + sigtuple = unwrap_unionall(sig)::DataType + msig = unwrap_unionall(method.sig)::DataType + spec_len = length(msig.parameters) + 1 + ls = length(sigtuple.parameters) + + if method === sv.linfo.def + # Under direct self-recursion, permit much greater use of reducers. + # here we assume that complexity(specTypes) :>= complexity(sig) + comparison = sv.linfo.specTypes + l_comparison = length((unwrap_unionall(comparison)::DataType).parameters) + spec_len = max(spec_len, l_comparison) + else + comparison = method.sig + end + + if isdefined(method, :recursion_relation) + # We don't recquire the recursion_relation to be transitive, so + # apply a hard limit + hardlimit = true + end + + # see if the type is actually too big (relative to the caller), and limit it if required + newsig = limit_type_size(sig, comparison, hardlimit ? comparison : sv.linfo.specTypes, InferenceParams(interp).TUPLE_COMPLEXITY_LIMIT_DEPTH, spec_len) + + if newsig !== sig + # continue inference, but note that we've limited parameter complexity + # on this call (to ensure convergence), so that we don't cache this result + if call_result_unused(sv) + add_remark!(interp, sv, RECURSION_UNUSED_MSG) + # if we don't (typically) actually care about this result, + # don't bother trying to examine some complex abstract signature + # since it's very unlikely that we'll try to inline this, + # or want make an invoke edge to its calling convention return type. + # (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases) + return MethodCallResult(Any, true, true, nothing, Effects()) + end + add_remark!(interp, sv, RECURSION_MSG) + topmost = topmost::InferenceState + parentframe = topmost.parent + poison_callstack(sv, parentframe === nothing ? topmost : parentframe) + sig = newsig + sparams = svec() + edgelimited = true + end + end + + # if sig changed, may need to recompute the sparams environment + if isa(method.sig, UnionAll) && isempty(sparams) + recomputed = ccall(:jl_type_intersection_with_env, Any, (Any, Any), sig, method.sig)::SimpleVector + #@assert recomputed[1] !== Bottom + # We must not use `sig` here, since that may re-introduce structural complexity that + # our limiting heuristic sought to eliminate. The alternative would be to not increment depth over covariant contexts, + # but we prefer to permit inference of tuple-destructuring, so we don't do that right now + # For example, with a signature such as `Tuple{T, Ref{T}} where {T <: S}` + # we might want to limit this to `Tuple{S, Ref}`, while type-intersection can instead give us back the original type + # (which moves `S` back up to a lower comparison depth) + # Optionally, we could try to drive this to a fixed point, but I think this is getting too complex, + # and this would only cause more questions and more problems + # (the following is only an example, most of the statements are probable in the wrong order): + # newsig = sig + # seen = IdSet() + # while !(newsig in seen) + # push!(seen, newsig) + # lsig = length((unwrap_unionall(sig)::DataType).parameters) + # newsig = limit_type_size(newsig, sig, sv.linfo.specTypes, InferenceParams(interp).TUPLE_COMPLEXITY_LIMIT_DEPTH, lsig) + # recomputed = ccall(:jl_type_intersection_with_env, Any, (Any, Any), newsig, method.sig)::SimpleVector + # newsig = recomputed[2] + # end + # sig = ? + sparams = recomputed[2]::SimpleVector + end + + (; rt, edge, edge_effects) = typeinf_edge(interp, method, sig, sparams, sv) + if edge === nothing + edgecycle = edgelimited = true + end + + # we look for the termination effect override here as well, since the :terminates effect + # may have been tainted due to recursion at this point even if it's overridden + if is_effect_overridden(sv, :terminates_globally) + # this frame is known to terminate + edge_effects = Effects(edge_effects, terminates=ALWAYS_TRUE) + elseif is_effect_overridden(method, :terminates_globally) + # this edge is known to terminate + edge_effects = Effects(edge_effects; terminates=ALWAYS_TRUE) + elseif edgecycle + # Some sort of recursion was detected. Even if we did not limit types, + # we cannot guarantee that the call will terminate + edge_effects = Effects(edge_effects; terminates=TRISTATE_UNKNOWN) + end + return MethodCallResult(rt, edgecycle, edgelimited, edge, edge_effects) +end + +function edge_matches_sv(frame::InferenceState, method::Method, @nospecialize(sig), sparams::SimpleVector, hardlimit::Bool, sv::InferenceState) + # The `method_for_inference_heuristics` will expand the given method's generator if + # necessary in order to retrieve this field from the generated `CodeInfo`, if it exists. + # The other `CodeInfo`s we inspect will already have this field inflated, so we just + # access it directly instead (to avoid regeneration). + callee_method2 = method_for_inference_heuristics(method, sig, sparams) # Union{Method, Nothing} + + inf_method2 = frame.src.method_for_inference_limit_heuristics # limit only if user token match + inf_method2 isa Method || (inf_method2 = nothing) + if callee_method2 !== inf_method2 + return false + end + if !hardlimit + # if this is a soft limit, + # also inspect the parent of this edge, + # to see if they are the same Method as sv + # in which case we'll need to ensure it is convergent + # otherwise, we don't + + # check in the cycle list first + # all items in here are mutual parents of all others + if !_any(p::InferenceState->matches_sv(p, sv), frame.callers_in_cycle) + let parent = frame.parent + parent !== nothing || return false + parent = parent::InferenceState + (parent.cached || parent.parent !== nothing) || return false + matches_sv(parent, sv) || return false + end + end + + # If the method defines a recursion relation, give it a chance + # to tell us that this recursion is actually ok. + if isdefined(method, :recursion_relation) + if Core._apply_pure(method.recursion_relation, Any[method, callee_method2, sig, frame.linfo.specTypes]) + return false + end + end + end + return true +end + +# This function is used for computing alternate limit heuristics +function method_for_inference_heuristics(method::Method, @nospecialize(sig), sparams::SimpleVector) + if isdefined(method, :generator) && method.generator.expand_early && may_invoke_generator(method, sig, sparams) + method_instance = specialize_method(method, sig, sparams) + if isa(method_instance, MethodInstance) + cinfo = get_staged(method_instance) + if isa(cinfo, CodeInfo) + method2 = cinfo.method_for_inference_limit_heuristics + if method2 isa Method + return method2 + end + end + end + end + return nothing +end + +function matches_sv(parent::InferenceState, sv::InferenceState) + sv_method2 = sv.src.method_for_inference_limit_heuristics # limit only if user token match + sv_method2 isa Method || (sv_method2 = nothing) + parent_method2 = parent.src.method_for_inference_limit_heuristics # limit only if user token match + parent_method2 isa Method || (parent_method2 = nothing) + return parent.linfo.def === sv.linfo.def && sv_method2 === parent_method2 +end + +# keeps result and context information of abstract_method_call, which will later be used for +# backedge computation, and concrete evaluation or constant-propagation +struct MethodCallResult + rt + edgecycle::Bool + edgelimited::Bool + edge::Union{Nothing,MethodInstance} + edge_effects::Effects + function MethodCallResult(@nospecialize(rt), + edgecycle::Bool, + edgelimited::Bool, + edge::Union{Nothing,MethodInstance}, + edge_effects::Effects) + return new(rt, edgecycle, edgelimited, edge, edge_effects) + end +end + +function pure_eval_eligible(interp::AbstractInterpreter, + @nospecialize(f), applicable::Vector{Any}, arginfo::ArgInfo, sv::InferenceState) + # XXX we need to check that this pure function doesn't call any overlayed method + return f !== nothing && + length(applicable) == 1 && + is_method_pure(applicable[1]::MethodMatch) && + is_all_const_arg(arginfo) +end + +function is_method_pure(method::Method, @nospecialize(sig), sparams::SimpleVector) + if isdefined(method, :generator) + method.generator.expand_early || return false + mi = specialize_method(method, sig, sparams) + isa(mi, MethodInstance) || return false + staged = get_staged(mi) + (staged isa CodeInfo && (staged::CodeInfo).pure) || return false + return true + end + return method.pure +end +is_method_pure(match::MethodMatch) = is_method_pure(match.method, match.spec_types, match.sparams) + +function pure_eval_call(interp::AbstractInterpreter, + @nospecialize(f), applicable::Vector{Any}, arginfo::ArgInfo, sv::InferenceState) + pure_eval_eligible(interp, f, applicable, arginfo, sv) || return nothing + return _pure_eval_call(f, arginfo) +end +function _pure_eval_call(@nospecialize(f), arginfo::ArgInfo) + args = collect_const_args(arginfo) + value = try + Core._apply_pure(f, args) + catch + return nothing + end + return Const(value) +end + +function concrete_eval_eligible(interp::AbstractInterpreter, + @nospecialize(f), result::MethodCallResult, arginfo::ArgInfo, sv::InferenceState) + # disable concrete-evaluation since this function call is tainted by some overlayed + # method and currently there is no direct way to execute overlayed methods + isoverlayed(method_table(interp)) && !is_nonoverlayed(result.edge_effects) && return false + return f !== nothing && + result.edge !== nothing && + is_concrete_eval_eligible(result.edge_effects) && + is_all_const_arg(arginfo) +end + +function is_all_const_arg((; argtypes)::ArgInfo) + for i = 2:length(argtypes) + a = widenconditional(argtypes[i]) + isa(a, Const) || isconstType(a) || issingletontype(a) || return false + end + return true +end + +function collect_const_args((; argtypes)::ArgInfo) + return Any[ let a = widenconditional(argtypes[i]) + isa(a, Const) ? a.val : + isconstType(a) ? (a::DataType).parameters[1] : + (a::DataType).instance + end for i in 2:length(argtypes) ] +end + +function concrete_eval_call(interp::AbstractInterpreter, + @nospecialize(f), result::MethodCallResult, arginfo::ArgInfo, sv::InferenceState) + concrete_eval_eligible(interp, f, result, arginfo, sv) || return nothing + args = collect_const_args(arginfo) + world = get_world_counter(interp) + value = try + Core._call_in_world_total(world, f, args...) + catch + # The evaulation threw. By :consistent-cy, we're guaranteed this would have happened at runtime + return ConstCallResults(Union{}, ConcreteResult(result.edge::MethodInstance, result.edge_effects), result.edge_effects) + end + if is_inlineable_constant(value) || call_result_unused(sv) + # If the constant is not inlineable, still do the const-prop, since the + # code that led to the creation of the Const may be inlineable in the same + # circumstance and may be optimizable. + return ConstCallResults(Const(value), ConcreteResult(result.edge::MethodInstance, EFFECTS_TOTAL, value), EFFECTS_TOTAL) + end + return nothing +end + +function const_prop_enabled(interp::AbstractInterpreter, sv::InferenceState, match::MethodMatch) + if !InferenceParams(interp).ipo_constant_propagation + add_remark!(interp, sv, "[constprop] Disabled by parameter") + return false + end + method = match.method + if method.constprop == 0x02 + add_remark!(interp, sv, "[constprop] Disabled by method parameter") + return false + end + return true +end + +struct ConstCallResults + rt::Any + const_result::ConstResult + effects::Effects + ConstCallResults(@nospecialize(rt), + const_result::ConstResult, + effects::Effects) = + new(rt, const_result, effects) +end + +function abstract_call_method_with_const_args(interp::AbstractInterpreter, result::MethodCallResult, + @nospecialize(f), arginfo::ArgInfo, match::MethodMatch, + sv::InferenceState) + if !const_prop_enabled(interp, sv, match) + return nothing + end + val = concrete_eval_call(interp, f, result, arginfo, sv) + if val !== nothing + add_backedge!(val.const_result.mi, sv) + return val + end + mi = maybe_get_const_prop_profitable(interp, result, f, arginfo, match, sv) + mi === nothing && return nothing + # try constant prop' + inf_cache = get_inference_cache(interp) + inf_result = cache_lookup(mi, arginfo.argtypes, inf_cache) + if inf_result === nothing + # if there might be a cycle, check to make sure we don't end up + # calling ourselves here. + let result = result # prevent capturing + if result.edgecycle && _any(InfStackUnwind(sv)) do infstate + # if the type complexity limiting didn't decide to limit the call signature (`result.edgelimited = false`) + # we can relax the cycle detection by comparing `MethodInstance`s and allow inference to + # propagate different constant elements if the recursion is finite over the lattice + return (result.edgelimited ? match.method === infstate.linfo.def : mi === infstate.linfo) && + any(infstate.result.overridden_by_const) end + add_remark!(interp, sv, "[constprop] Edge cycle encountered") + return nothing end end - # iterate through the cycle before walking to the parent - if cyclei < length(infstate.callers_in_cycle) - cyclei += 1 - infstate = infstate.callers_in_cycle[cyclei] + inf_result = InferenceResult(mi, (arginfo, sv)) + if !any(inf_result.overridden_by_const) + add_remark!(interp, sv, "[constprop] Could not handle constant info in matching_cache_argtypes") + return nothing + end + frame = InferenceState(inf_result, #=cache=#:local, interp) + frame === nothing && return nothing # this is probably a bad generated function (unsound), but just ignore it + frame.parent = sv + typeinf(interp, frame) || return nothing + end + result = inf_result.result + # if constant inference hits a cycle, just bail out + isa(result, InferenceState) && return nothing + add_backedge!(mi, sv) + return ConstCallResults(result, ConstPropResult(inf_result), inf_result.ipo_effects) +end + +# if there's a possibility we could get a better result with these constant arguments +# (hopefully without doing too much work), returns `MethodInstance`, or nothing otherwise +function maybe_get_const_prop_profitable(interp::AbstractInterpreter, result::MethodCallResult, + @nospecialize(f), arginfo::ArgInfo, match::MethodMatch, + sv::InferenceState) + method = match.method + force = force_const_prop(interp, f, method) + force || const_prop_entry_heuristic(interp, result, sv) || return nothing + nargs::Int = method.nargs + method.isva && (nargs -= 1) + length(arginfo.argtypes) < nargs && return nothing + if !const_prop_argument_heuristic(interp, arginfo, sv) + add_remark!(interp, sv, "[constprop] Disabled by argument and rettype heuristics") + return nothing + end + all_overridden = is_all_overridden(arginfo, sv) + if !force && !const_prop_function_heuristic(interp, f, arginfo, nargs, all_overridden, + sv.ipo_effects.nothrow === ALWAYS_TRUE, sv) + add_remark!(interp, sv, "[constprop] Disabled by function heuristic") + return nothing + end + force |= all_overridden + mi = specialize_method(match; preexisting=!force) + if mi === nothing + add_remark!(interp, sv, "[constprop] Failed to specialize") + return nothing + end + mi = mi::MethodInstance + if !force && !const_prop_methodinstance_heuristic(interp, match, mi, arginfo, sv) + add_remark!(interp, sv, "[constprop] Disabled by method instance heuristic") + return nothing + end + return mi +end + +function const_prop_entry_heuristic(interp::AbstractInterpreter, result::MethodCallResult, sv::InferenceState) + if call_result_unused(sv) && result.edgecycle + add_remark!(interp, sv, "[constprop] Disabled by entry heuristic (edgecycle with unused result)") + return false + end + # check if this return type is improvable (i.e. whether it's possible that with more + # information, we might get a more precise type) + rt = result.rt + if isa(rt, Type) + # could always be improved to `Const`, `PartialStruct` or just a more precise type, + # unless we're already at `Bottom` + if rt === Bottom + add_remark!(interp, sv, "[constprop] Disabled by entry heuristic (erroneous result)") + return false else - cyclei = 0 - infstate = infstate.parent + return true + end + elseif isa(rt, PartialStruct) || isa(rt, InterConditional) + # could be improved to `Const` or a more precise wrapper + return true + elseif isa(rt, LimitedAccuracy) + # optimizations like inlining are disabled for limited frames, + # thus there won't be much benefit in constant-prop' here + add_remark!(interp, sv, "[constprop] Disabled by entry heuristic (limited accuracy)") + return false + else + if isa(rt, Const) + if result.edge_effects.nothrow !== ALWAYS_TRUE + # Could still be improved to Bottom (or at least could see the effects improved) + return true + end + end + add_remark!(interp, sv, "[constprop] Disabled by entry heuristic (unimprovable result)") + return false + end +end + +# determines heuristically whether if constant propagation can be worthwhile +# by checking if any of given `argtypes` is "interesting" enough to be propagated +function const_prop_argument_heuristic(_::AbstractInterpreter, (; fargs, argtypes)::ArgInfo, sv::InferenceState) + for i in 1:length(argtypes) + a = argtypes[i] + if isa(a, Conditional) && fargs !== nothing + is_const_prop_profitable_conditional(a, fargs, sv) && return true + else + a = widenconditional(a) + has_nontrivial_const_info(a) && is_const_prop_profitable_arg(a) && return true + end + end + return false +end + +function is_const_prop_profitable_arg(@nospecialize(arg)) + # have new information from argtypes that wasn't available from the signature + if isa(arg, PartialStruct) + for b in arg.fields + isconstType(b) && return true + is_const_prop_profitable_arg(b) && return true + end + end + isa(arg, PartialOpaque) && return true + isa(arg, Const) || return true + val = arg.val + # don't consider mutable values or Strings useful constants + return isa(val, Symbol) || isa(val, Type) || (!isa(val, String) && !ismutable(val)) +end + +function is_const_prop_profitable_conditional(cnd::Conditional, fargs::Vector{Any}, sv::InferenceState) + slotid = find_constrained_arg(cnd, fargs, sv) + if slotid !== nothing + return true + end + # as a minor optimization, we just check the result is a constant or not, + # since both `has_nontrivial_const_info`/`is_const_prop_profitable_arg` return `true` + # for `Const(::Bool)` + return isa(widenconditional(cnd), Const) +end + +function find_constrained_arg(cnd::Conditional, fargs::Vector{Any}, sv::InferenceState) + slot = slot_id(cnd.var) + for i in 1:length(fargs) + arg = ssa_def_slot(fargs[i], sv) + if isa(arg, SlotNumber) && slot_id(arg) == slot + return i + end + end + return nothing +end + +# checks if all argtypes has additional information other than what `Type` can provide +function is_all_overridden((; fargs, argtypes)::ArgInfo, sv::InferenceState) + for a in argtypes + if isa(a, Conditional) && fargs !== nothing + is_const_prop_profitable_conditional(a, fargs, sv) || return false + else + a = widenconditional(a) + is_forwardable_argtype(a) || return false + end + end + return true +end + +function force_const_prop(interp::AbstractInterpreter, @nospecialize(f), method::Method) + return method.constprop == 0x01 || + InferenceParams(interp).aggressive_constant_propagation || + istopfunction(f, :getproperty) || + istopfunction(f, :setproperty!) +end + +function const_prop_function_heuristic( + _::AbstractInterpreter, @nospecialize(f), (; argtypes)::ArgInfo, + nargs::Int, all_overridden::Bool, still_nothrow::Bool, _::InferenceState) + if nargs > 1 + if istopfunction(f, :getindex) || istopfunction(f, :setindex!) + arrty = argtypes[2] + # don't propagate constant index into indexing of non-constant array + if arrty isa Type && arrty <: AbstractArray && !issingletontype(arrty) + # For static arrays, allow the constprop if we could possibly + # deduce nothrow as a result. + if !still_nothrow || ismutabletype(arrty) + return false + end + elseif arrty ⊑ Array + return false + end + elseif istopfunction(f, :iterate) + itrty = argtypes[2] + if itrty ⊑ Array + return false + end end end - - if !(topmost === nothing) - topmost = topmost::InferenceState - sigtuple = unwrap_unionall(sig)::DataType - msig = unwrap_unionall(method.sig)::DataType - spec_len = length(msig.parameters) + 1 - ls = length(sigtuple.parameters) - if method === sv.linfo.def - # Under direct self-recursion, permit much greater use of reducers. - # here we assume that complexity(specTypes) :>= complexity(sig) - comparison = sv.linfo.specTypes - l_comparison = length(unwrap_unionall(comparison).parameters) - spec_len = max(spec_len, l_comparison) - else - comparison = method.sig - end - # see if the type is actually too big (relative to the caller), and limit it if required - newsig = limit_type_size(sig, comparison, hardlimit ? comparison : sv.linfo.specTypes, InferenceParams(interp).TUPLE_COMPLEXITY_LIMIT_DEPTH, spec_len) - - if newsig !== sig - # continue inference, but note that we've limited parameter complexity - # on this call (to ensure convergence), so that we don't cache this result - if call_result_unused(sv) - add_remark!(interp, sv, RECURSION_UNUSED_MSG) - # if we don't (typically) actually care about this result, - # don't bother trying to examine some complex abstract signature - # since it's very unlikely that we'll try to inline this, - # or want make an invoke edge to its calling convention return type. - # (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases) - return Any, true, nothing + if !all_overridden && (istopfunction(f, :+) || istopfunction(f, :-) || istopfunction(f, :*) || + istopfunction(f, :(==)) || istopfunction(f, :!=) || + istopfunction(f, :<=) || istopfunction(f, :>=) || istopfunction(f, :<) || istopfunction(f, :>) || + istopfunction(f, :<<) || istopfunction(f, :>>)) + # it is almost useless to inline the op when all the same type, + # but highly worthwhile to inline promote of a constant + length(argtypes) > 2 || return false + t1 = widenconst(argtypes[2]) + for i in 3:length(argtypes) + at = argtypes[i] + ty = isvarargtype(at) ? unwraptv(at) : widenconst(at) + if ty !== t1 + return true end - poison_callstack(sv, topmost::InferenceState, true) - sig = newsig - sparams = svec() end + return false end + return true +end - # if sig changed, may need to recompute the sparams environment - if isa(method.sig, UnionAll) && isempty(sparams) - recomputed = ccall(:jl_type_intersection_with_env, Any, (Any, Any), sig, method.sig)::SimpleVector - #@assert recomputed[1] !== Bottom - # We must not use `sig` here, since that may re-introduce structural complexity that - # our limiting heuristic sought to eliminate. The alternative would be to not increment depth over covariant contexts, - # but we prefer to permit inference of tuple-destructuring, so we don't do that right now - # For example, with a signature such as `Tuple{T, Ref{T}} where {T <: S}` - # we might want to limit this to `Tuple{S, Ref}`, while type-intersection can instead give us back the original type - # (which moves `S` back up to a lower comparison depth) - # Optionally, we could try to drive this to a fixed point, but I think this is getting too complex, - # and this would only cause more questions and more problems - # (the following is only an example, most of the statements are probable in the wrong order): - # newsig = sig - # seen = IdSet() - # while !(newsig in seen) - # push!(seen, newsig) - # lsig = length((unwrap_unionall(sig)::DataType).parameters) - # newsig = limit_type_size(newsig, sig, sv.linfo.specTypes, InferenceParams(interp).TUPLE_COMPLEXITY_LIMIT_DEPTH, lsig) - # recomputed = ccall(:jl_type_intersection_with_env, Any, (Any, Any), newsig, method.sig)::SimpleVector - # newsig = recomputed[2] - # end - # sig = ? - sparams = recomputed[2]::SimpleVector +# This is a heuristic to avoid trying to const prop through complicated functions +# where we would spend a lot of time, but are probably unlikely to get an improved +# result anyway. +function const_prop_methodinstance_heuristic( + interp::AbstractInterpreter, match::MethodMatch, mi::MethodInstance, + (; argtypes)::ArgInfo, sv::InferenceState) + method = match.method + if method.is_for_opaque_closure + # Not inlining an opaque closure can be very expensive, so be generous + # with the const-prop-ability. It is quite possible that we can't infer + # anything at all without const-propping, so the inlining check below + # isn't particularly helpful here. + return true end - - rt, edge = typeinf_edge(interp, method, sig, sparams, sv) - if edge === nothing - edgecycle = true + # Peek at the inferred result for the function to determine if the optimizer + # was able to cut it down to something simple (inlineable in particular). + # If so, there's a good chance we might be able to const prop all the way + # through and learn something new. + if isdefined(method, :source) && ccall(:jl_ir_flag_inlineable, Bool, (Any,), method.source) + return true + else + flag = get_curr_ssaflag(sv) + if is_stmt_inline(flag) + # force constant propagation for a call that is going to be inlined + # since the inliner will try to find this constant result + # if these constant arguments arrive there + return true + elseif is_stmt_noinline(flag) + # this call won't be inlined, thus this constant-prop' will most likely be unfruitful + return false + else + code = get(code_cache(interp), mi, nothing) + if isdefined(code, :inferred) && inlining_policy( + interp, code.inferred, IR_FLAG_NULL, mi, argtypes) !== nothing + return true + end + end end - return rt, edgecycle, edge + return false # the cache isn't inlineable, so this constant-prop' will most likely be unfruitful end # This is only for use with `Conditional`. @@ -514,34 +1131,32 @@ function precise_container_type(interp::AbstractInterpreter, @nospecialize(itft) if isa(tti, DataType) && tti.name === NamedTuple_typename # A NamedTuple iteration is the same as the iteration of its Tuple parameter: # compute a new `tti == unwrap_unionall(tti0)` based on that Tuple type - tti = tti.parameters[2] - while isa(tti, TypeVar) - tti = tti.ub - end + tti = unwraptv(tti.parameters[2]) tti0 = rewrap_unionall(tti, tti0) end if isa(tti, Union) utis = uniontypes(tti) - if _any(t -> !isa(t, DataType) || !(t <: Tuple) || !isknownlength(t), utis) + if _any(@nospecialize(t) -> !isa(t, DataType) || !(t <: Tuple) || !isknownlength(t), utis) return Any[Vararg{Any}], nothing end - result = Any[rewrap_unionall(p, tti0) for p in utis[1].parameters] - for t in utis[2:end] - if length(t.parameters) != length(result) + ltp = length((utis[1]::DataType).parameters) + for t in utis + if length((t::DataType).parameters) != ltp return Any[Vararg{Any}], nothing end - for j in 1:length(t.parameters) - result[j] = tmerge(result[j], rewrap_unionall(t.parameters[j], tti0)) + end + result = Any[ Union{} for _ in 1:ltp ] + for t in utis + tps = (t::DataType).parameters + _all(valid_as_lattice, tps) || continue + for j in 1:ltp + result[j] = tmerge(result[j], rewrap_unionall(tps[j], tti0)) end end return result, nothing elseif tti0 <: Tuple if isa(tti0, DataType) - if isvatuple(tti0) && length(tti0.parameters) == 1 - return Any[Vararg{unwrapva(tti0.parameters[1])}], nothing - else - return Any[ p for p in tti0.parameters ], nothing - end + return Any[ p for p in tti0.parameters ], nothing elseif !isa(tti, DataType) return Any[Vararg{Any}], nothing else @@ -565,33 +1180,28 @@ end # simulate iteration protocol on container type up to fixpoint function abstract_iteration(interp::AbstractInterpreter, @nospecialize(itft), @nospecialize(itertype), sv::InferenceState) - if !isdefined(Main, :Base) || !isdefined(Main.Base, :iterate) || !isconst(Main.Base, :iterate) - return Any[Vararg{Any}], nothing - end - if itft === nothing - iteratef = getfield(Main.Base, :iterate) - itft = Const(iteratef) - elseif isa(itft, Const) + if isa(itft, Const) iteratef = itft.val else return Any[Vararg{Any}], nothing end @assert !isvarargtype(itertype) - call = abstract_call_known(interp, iteratef, nothing, Any[itft, itertype], sv) + call = abstract_call_known(interp, iteratef, ArgInfo(nothing, Any[itft, itertype]), sv) stateordonet = call.rt info = call.info # Return Bottom if this is not an iterator. # WARNING: Changes to the iteration protocol must be reflected here, # this is not just an optimization. + # TODO: this doesn't realize that Array, SimpleVector, Tuple, and NamedTuple do not use the iterate protocol stateordonet === Bottom && return Any[Bottom], AbstractIterationInfo(CallMeta[CallMeta(Bottom, info)]) valtype = statetype = Bottom ret = Any[] calls = CallMeta[call] + stateordonet_widened = widenconst(stateordonet) # Try to unroll the iteration up to MAX_TUPLE_SPLAT, which covers any finite # length iterators, or interesting prefix while true - stateordonet_widened = widenconst(stateordonet) if stateordonet_widened === Nothing return ret, AbstractIterationInfo(calls) end @@ -610,43 +1220,62 @@ function abstract_iteration(interp::AbstractInterpreter, @nospecialize(itft), @n valtype = getfield_tfunc(stateordonet, Const(1)) push!(ret, valtype) statetype = nstatetype - call = abstract_call_known(interp, iteratef, nothing, Any[Const(iteratef), itertype, statetype], sv) + call = abstract_call_known(interp, iteratef, ArgInfo(nothing, Any[Const(iteratef), itertype, statetype]), sv) stateordonet = call.rt + stateordonet_widened = widenconst(stateordonet) push!(calls, call) end # From here on, we start asking for results on the widened types, rather than # the precise (potentially const) state type - statetype = widenconst(statetype) - valtype = widenconst(valtype) + # statetype and valtype are reinitialized in the first iteration below from the + # (widened) stateordonet, which has not yet been fully analyzed in the loop above + statetype = Bottom + valtype = Bottom + may_have_terminated = Nothing <: stateordonet_widened while valtype !== Any - stateordonet = abstract_call_known(interp, iteratef, nothing, Any[Const(iteratef), itertype, statetype], sv).rt - stateordonet = widenconst(stateordonet) - nounion = typesubtract(stateordonet, Nothing) - if !isa(nounion, DataType) || !(nounion <: Tuple) || isvatuple(nounion) || length(nounion.parameters) != 2 + nounion = typeintersect(stateordonet_widened, Tuple{Any,Any}) + if nounion !== Union{} && !isa(nounion, DataType) + # nounion is of a type we cannot handle valtype = Any break end - if nounion.parameters[1] <: valtype && nounion.parameters[2] <: statetype - if typeintersect(stateordonet, Nothing) === Union{} - # Reached a fixpoint, but Nothing is not possible => iterator is infinite or failing - return Any[Bottom], nothing + if nounion === Union{} || (nounion.parameters[1] <: valtype && nounion.parameters[2] <: statetype) + # reached a fixpoint or iterator failed/gave invalid answer + if !hasintersect(stateordonet_widened, Nothing) + # ... but cannot terminate + if !may_have_terminated + # ... and cannot have terminated prior to this loop + return Any[Bottom], nothing + else + # iterator may have terminated prior to this loop, but not during it + valtype = Bottom + end end break end valtype = tmerge(valtype, nounion.parameters[1]) statetype = tmerge(statetype, nounion.parameters[2]) + stateordonet = abstract_call_known(interp, iteratef, ArgInfo(nothing, Any[Const(iteratef), itertype, statetype]), sv).rt + stateordonet_widened = widenconst(stateordonet) + end + if valtype !== Union{} + push!(ret, Vararg{valtype}) end - push!(ret, Vararg{valtype}) return ret, nothing end # do apply(af, fargs...), where af is a function value -function abstract_apply(interp::AbstractInterpreter, @nospecialize(itft), @nospecialize(aft), aargtypes::Vector{Any}, sv::InferenceState, - max_methods::Int = InferenceParams(interp).MAX_METHODS) +function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, sv::InferenceState, + max_methods::Int = get_max_methods(sv.mod, interp)) + itft = argtype_by_index(argtypes, 2) + aft = argtype_by_index(argtypes, 3) + (itft === Bottom || aft === Bottom) && return CallMeta(Bottom, false) + aargtypes = argtype_tail(argtypes, 4) aftw = widenconst(aft) - if !isa(aft, Const) && (!isType(aftw) || has_free_typevars(aftw)) + if !isa(aft, Const) && !isa(aft, PartialOpaque) && (!isType(aftw) || has_free_typevars(aftw)) if !isconcretetype(aftw) || (aftw <: Builtin) - add_remark!(interp, sv, "Core._apply called on a function of a non-concrete type") + add_remark!(interp, sv, "Core._apply_iterate called on a function of a non-concrete type") + tristate_merge!(sv, Effects()) # bail now, since it seems unlikely that abstract_call will be able to do any better after splitting # this also ensures we don't call abstract_call_gf_by_type below on an IntrinsicFunction or Builtin return CallMeta(Any, false) @@ -654,17 +1283,21 @@ function abstract_apply(interp::AbstractInterpreter, @nospecialize(itft), @nospe end res = Union{} nargs = length(aargtypes) - splitunions = 1 < countunionsplit(aargtypes) <= InferenceParams(interp).MAX_APPLY_UNION_ENUM - ctypes = Any[Any[aft]] - infos = [Union{Nothing, AbstractIterationInfo}[]] + splitunions = 1 < unionsplitcost(aargtypes) <= InferenceParams(interp).MAX_APPLY_UNION_ENUM + ctypes = [Any[aft]] + infos = Vector{MaybeAbstractIterationInfo}[MaybeAbstractIterationInfo[]] for i = 1:nargs - ctypes´ = [] - infos′ = [] + ctypes´ = Vector{Any}[] + infos′ = Vector{MaybeAbstractIterationInfo}[] for ti in (splitunions ? uniontypes(aargtypes[i]) : Any[aargtypes[i]]) if !isvarargtype(ti) - cti, info = precise_container_type(interp, itft, ti, sv) + cti_info = precise_container_type(interp, itft, ti, sv) + cti = cti_info[1]::Vector{Any} + info = cti_info[2]::MaybeAbstractIterationInfo else - cti, info = precise_container_type(interp, itft, unwrapva(ti), sv) + cti_info = precise_container_type(interp, itft, unwrapva(ti), sv) + cti = cti_info[1]::Vector{Any} + info = cti_info[2]::MaybeAbstractIterationInfo # We can't represent a repeating sequence of the same types, # so tmerge everything together to get one type that represents # everything. @@ -681,12 +1314,11 @@ function abstract_apply(interp::AbstractInterpreter, @nospecialize(itft), @nospe continue end for j = 1:length(ctypes) - ct = ctypes[j] + ct = ctypes[j]::Vector{Any} if isvarargtype(ct[end]) # This is vararg, we're not gonna be able to do any inling, # drop the info info = nothing - tail = tuple_tail_elem(unwrapva(ct[end]), cti) push!(ctypes´, push!(ct[1:(end - 1)], tail)) else @@ -706,18 +1338,21 @@ function abstract_apply(interp::AbstractInterpreter, @nospecialize(itft), @nospe lct = length(ct) # truncate argument list at the first Vararg for i = 1:lct-1 - if isvarargtype(ct[i]) - ct[i] = tuple_tail_elem(ct[i], ct[(i+1):lct]) + cti = ct[i] + if isvarargtype(cti) + ct[i] = tuple_tail_elem(unwrapva(cti), ct[(i+1):lct]) resize!(ct, i) break end end - call = abstract_call(interp, nothing, ct, sv, max_methods) + call = abstract_call(interp, ArgInfo(nothing, ct), sv, max_methods) push!(retinfos, ApplyCallInfo(call.info, arginfo)) res = tmerge(res, call.rt) - if res === Any - # No point carrying forward the info, we're not gonna inline it anyway - retinfo = nothing + if bail_out_apply(interp, res, sv) + if i != length(ctypes) + # No point carrying forward the info, we're not gonna inline it anyway + retinfo = false + end break end end @@ -726,40 +1361,11 @@ function abstract_apply(interp::AbstractInterpreter, @nospecialize(itft), @nospe return CallMeta(res, retinfo) end -function is_method_pure(method::Method, @nospecialize(sig), sparams::SimpleVector) - if isdefined(method, :generator) - method.generator.expand_early || return false - mi = specialize_method(method, sig, sparams, false) - isa(mi, MethodInstance) || return false - staged = get_staged(mi) - (staged isa CodeInfo && (staged::CodeInfo).pure) || return false - return true - end - return method.pure -end -is_method_pure(match::MethodMatch) = is_method_pure(match.method, match.spec_types, match.sparams) - -function pure_eval_call(@nospecialize(f), argtypes::Vector{Any}) - for i = 2:length(argtypes) - a = widenconditional(argtypes[i]) - if !(isa(a, Const) || isconstType(a)) - return false - end - end - - args = Any[ (a = widenconditional(argtypes[i]); isa(a, Const) ? a.val : a.parameters[1]) for i in 2:length(argtypes) ] - try - value = Core._apply_pure(f, args) - return Const(value) - catch - return false - end -end - function argtype_by_index(argtypes::Vector{Any}, i::Int) n = length(argtypes) - if isvarargtype(argtypes[n]) - return i >= n ? unwrapva(argtypes[n]) : argtypes[i] + na = argtypes[n] + if isvarargtype(na) + return i >= n ? unwrapva(na) : argtypes[i] else return i > n ? Bottom : argtypes[i] end @@ -773,36 +1379,39 @@ function argtype_tail(argtypes::Vector{Any}, i::Int) return argtypes[i:n] end -function abstract_call_builtin(interp::AbstractInterpreter, f::Builtin, fargs::Union{Nothing,Vector{Any}}, - argtypes::Vector{Any}, sv::InferenceState, max_methods::Int) +function abstract_call_builtin(interp::AbstractInterpreter, f::Builtin, (; fargs, argtypes)::ArgInfo, + sv::InferenceState, max_methods::Int) + @nospecialize f la = length(argtypes) - if f === ifelse && fargs isa Vector{Any} && la == 4 && argtypes[2] isa Conditional - # try to simulate this as a real conditional (`cnd ? x : y`), so that the penalty for using `ifelse` instead isn't too high - cnd = argtypes[2]::Conditional - tx = argtypes[3] - ty = argtypes[4] - a = ssa_def_slot(fargs[3], sv) - b = ssa_def_slot(fargs[4], sv) - if isa(a, Slot) && slot_id(cnd.var) == slot_id(a) - tx = typeintersect(tx, cnd.vtype) - end - if isa(b, Slot) && slot_id(cnd.var) == slot_id(b) - ty = typeintersect(ty, cnd.elsetype) + if f === Core.ifelse && fargs isa Vector{Any} && la == 4 + cnd = argtypes[2] + if isa(cnd, Conditional) + newcnd = widenconditional(cnd) + tx = argtypes[3] + ty = argtypes[4] + if isa(newcnd, Const) + # if `cnd` is constant, we should just respect its constantness to keep inference accuracy + return newcnd.val::Bool ? tx : ty + else + # try to simulate this as a real conditional (`cnd ? x : y`), so that the penalty for using `ifelse` instead isn't too high + a = ssa_def_slot(fargs[3], sv) + b = ssa_def_slot(fargs[4], sv) + if isa(a, SlotNumber) && slot_id(cnd.var) == slot_id(a) + tx = (cnd.vtype ⊑ tx ? cnd.vtype : tmeet(tx, widenconst(cnd.vtype))) + end + if isa(b, SlotNumber) && slot_id(cnd.var) == slot_id(b) + ty = (cnd.elsetype ⊑ ty ? cnd.elsetype : tmeet(ty, widenconst(cnd.elsetype))) + end + return tmerge(tx, ty) + end end - return tmerge(tx, ty) end rt = builtin_tfunction(interp, f, argtypes[2:end], sv) - if f === getfield && isa(fargs, Vector{Any}) && la == 3 && isa(argtypes[3], Const) && isa(argtypes[3].val, Int) && argtypes[2] ⊑ Tuple - cti, _ = precise_container_type(interp, nothing, argtypes[2], sv) - idx = argtypes[3].val - if 1 <= idx <= length(cti) - rt = unwrapva(cti[idx]) - end - elseif (rt === Bool || (isa(rt, Const) && isa(rt.val, Bool))) && isa(fargs, Vector{Any}) + if (rt === Bool || (isa(rt, Const) && isa(rt.val, Bool))) && isa(fargs, Vector{Any}) # perform very limited back-propagation of type information for `is` and `isa` if f === isa a = ssa_def_slot(fargs[2], sv) - if isa(a, Slot) + if isa(a, SlotNumber) aty = widenconst(argtypes[2]) if rt === Const(false) return Conditional(a, Union{}, aty) @@ -814,7 +1423,8 @@ function abstract_call_builtin(interp::AbstractInterpreter, f::Builtin, fargs::U tty_lb = tty_ub # TODO: this would be wrong if !isexact_tty, but instanceof_tfunc doesn't preserve this info if !has_free_typevars(tty_lb) && !has_free_typevars(tty_ub) ifty = typeintersect(aty, tty_ub) - elty = typesubtract(aty, tty_lb) + valid_as_lattice(ifty) || (ifty = Union{}) + elty = typesubtract(aty, tty_lb, InferenceParams(interp).MAX_UNION_SPLITTING) return Conditional(a, ifty, elty) end end @@ -825,31 +1435,31 @@ function abstract_call_builtin(interp::AbstractInterpreter, f::Builtin, fargs::U aty = argtypes[2] bty = argtypes[3] # if doing a comparison to a singleton, consider returning a `Conditional` instead - if isa(aty, Const) && isa(b, Slot) + if isa(aty, Const) && isa(b, SlotNumber) if rt === Const(false) aty = Union{} elseif rt === Const(true) bty = Union{} elseif bty isa Type && isdefined(typeof(aty.val), :instance) # can only widen a if it is a singleton - bty = typesubtract(bty, typeof(aty.val)) + bty = typesubtract(bty, typeof(aty.val), InferenceParams(interp).MAX_UNION_SPLITTING) end return Conditional(b, aty, bty) end - if isa(bty, Const) && isa(a, Slot) + if isa(bty, Const) && isa(a, SlotNumber) if rt === Const(false) bty = Union{} elseif rt === Const(true) aty = Union{} elseif aty isa Type && isdefined(typeof(bty.val), :instance) # same for b - aty = typesubtract(aty, typeof(bty.val)) + aty = typesubtract(aty, typeof(bty.val), InferenceParams(interp).MAX_UNION_SPLITTING) end return Conditional(a, bty, aty) end # narrow the lattice slightly (noting the dependency on one of the slots), to promote more effective smerge - if isa(b, Slot) + if isa(b, SlotNumber) return Conditional(b, rt === Const(false) ? Union{} : bty, rt === Const(true) ? Union{} : bty) end - if isa(a, Slot) + if isa(a, SlotNumber) return Conditional(a, rt === Const(false) ? Union{} : aty, rt === Const(true) ? Union{} : aty) end elseif f === Core.Compiler.not_int @@ -864,18 +1474,42 @@ function abstract_call_builtin(interp::AbstractInterpreter, f::Builtin, fargs::U end return Conditional(aty.var, ifty, elty) end + elseif f === isdefined + uty = argtypes[2] + a = ssa_def_slot(fargs[2], sv) + if isa(uty, Union) && isa(a, SlotNumber) + fld = argtypes[3] + vtype = Union{} + elsetype = Union{} + for ty in uniontypes(uty) + cnd = isdefined_tfunc(ty, fld) + if isa(cnd, Const) + if cnd.val::Bool + vtype = tmerge(vtype, ty) + else + elsetype = tmerge(elsetype, ty) + end + else + vtype = tmerge(vtype, ty) + elsetype = tmerge(elsetype, ty) + end + end + return Conditional(a, vtype, elsetype) + end end end - return isa(rt, TypeVar) ? rt.ub : rt + @assert !isa(rt, TypeVar) "unhandled TypeVar" + return rt end function abstract_call_unionall(argtypes::Vector{Any}) if length(argtypes) == 3 canconst = true - if isa(argtypes[3], Const) - body = argtypes[3].val - elseif isType(argtypes[3]) - body = argtypes[3].parameters[1] + a3 = argtypes[3] + if isa(a3, Const) + body = a3.val + elseif isType(a3) + body = a3.parameters[1] canconst = false else return Any @@ -884,11 +1518,11 @@ function abstract_call_unionall(argtypes::Vector{Any}) return Any end if has_free_typevars(body) - if isa(argtypes[2], Const) - tv = argtypes[2].val - elseif isa(argtypes[2], PartialTypeVar) - ptv = argtypes[2] - tv = ptv.tv + a2 = argtypes[2] + if isa(a2, Const) + tv = a2.val + elseif isa(a2, PartialTypeVar) + tv = a2.tv canconst = false else return Any @@ -902,37 +1536,101 @@ function abstract_call_unionall(argtypes::Vector{Any}) return Any end +function abstract_invoke(interp::AbstractInterpreter, (; fargs, argtypes)::ArgInfo, sv::InferenceState) + ft′ = argtype_by_index(argtypes, 2) + ft = widenconst(ft′) + ft === Bottom && return CallMeta(Bottom, false), EFFECTS_THROWS + (types, isexact, isconcrete, istype) = instanceof_tfunc(argtype_by_index(argtypes, 3)) + types === Bottom && return CallMeta(Bottom, false), EFFECTS_THROWS + isexact || return CallMeta(Any, false), Effects() + argtype = argtypes_to_type(argtype_tail(argtypes, 4)) + nargtype = typeintersect(types, argtype) + nargtype === Bottom && return CallMeta(Bottom, false), EFFECTS_THROWS + nargtype isa DataType || return CallMeta(Any, false), Effects() # other cases are not implemented below + isdispatchelem(ft) || return CallMeta(Any, false), Effects() # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below + ft = ft::DataType + types = rewrap_unionall(Tuple{ft, unwrap_unionall(types).parameters...}, types)::Type + nargtype = Tuple{ft, nargtype.parameters...} + argtype = Tuple{ft, argtype.parameters...} + match, valid_worlds, overlayed = findsup(types, method_table(interp)) + match === nothing && return CallMeta(Any, false), Effects() + update_valid_age!(sv, valid_worlds) + method = match.method + (ti, env::SimpleVector) = ccall(:jl_type_intersection_with_env, Any, (Any, Any), nargtype, method.sig)::SimpleVector + (; rt, edge) = result = abstract_call_method(interp, method, ti, env, false, sv) + effects = result.edge_effects + edge !== nothing && add_backedge!(edge::MethodInstance, sv) + match = MethodMatch(ti, env, method, argtype <: method.sig) + res = nothing + sig = match.spec_types + argtypes′ = invoke_rewrite(argtypes) + fargs′ = fargs === nothing ? nothing : invoke_rewrite(fargs) + arginfo = ArgInfo(fargs′, argtypes′) + # # typeintersect might have narrowed signature, but the accuracy gain doesn't seem worth the cost involved with the lattice comparisons + # for i in 1:length(argtypes′) + # t, a = ti.parameters[i], argtypes′[i] + # argtypes′[i] = t ⊑ a ? t : a + # end + const_call_result = abstract_call_method_with_const_args(interp, result, + overlayed ? nothing : singleton_type(ft′), arginfo, match, sv) + const_result = nothing + if const_call_result !== nothing + if const_call_result.rt ⊑ rt + (; rt, effects, const_result) = const_call_result + end + end + effects = Effects(effects; nonoverlayed=!overlayed) + return CallMeta(from_interprocedural!(rt, sv, arginfo, sig), InvokeCallInfo(match, const_result)), effects +end + +function invoke_rewrite(xs::Vector{Any}) + x0 = xs[2] + newxs = xs[3:end] + newxs[1] = x0 + return newxs +end + # call where the function is known exactly function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), - fargs::Union{Nothing,Vector{Any}}, argtypes::Vector{Any}, - sv::InferenceState, - max_methods::Int = InferenceParams(interp).MAX_METHODS) - + arginfo::ArgInfo, sv::InferenceState, + max_methods::Int = get_max_methods(f, sv.mod, interp)) + (; fargs, argtypes) = arginfo la = length(argtypes) if isa(f, Builtin) - if f === _apply - ft = argtype_by_index(argtypes, 2) - ft === Bottom && return CallMeta(Bottom, false) - return abstract_apply(interp, nothing, ft, argtype_tail(argtypes, 3), sv, max_methods) - elseif f === _apply_iterate - itft = argtype_by_index(argtypes, 2) - ft = argtype_by_index(argtypes, 3) - (itft === Bottom || ft === Bottom) && return CallMeta(Bottom, false) - return abstract_apply(interp, itft, ft, argtype_tail(argtypes, 4), sv, max_methods) - end - return CallMeta(abstract_call_builtin(interp, f, fargs, argtypes, sv, max_methods), nothing) + if f === _apply_iterate + return abstract_apply(interp, argtypes, sv, max_methods) + elseif f === invoke + call, effects = abstract_invoke(interp, arginfo, sv) + tristate_merge!(sv, effects) + return call + elseif f === modifyfield! + tristate_merge!(sv, Effects()) # TODO + return abstract_modifyfield!(interp, argtypes, sv) + end + rt = abstract_call_builtin(interp, f, arginfo, sv, max_methods) + tristate_merge!(sv, builtin_effects(f, argtypes, rt)) + return CallMeta(rt, false) + elseif isa(f, Core.OpaqueClosure) + # calling an OpaqueClosure about which we have no information returns no information + tristate_merge!(sv, Effects()) + return CallMeta(Any, false) elseif f === Core.kwfunc if la == 2 - ft = widenconst(argtypes[2]) - if isa(ft, DataType) && isdefined(ft.name, :mt) && isdefined(ft.name.mt, :kwsorter) - return CallMeta(Const(ft.name.mt.kwsorter), false) + aty = argtypes[2] + if !isvarargtype(aty) + ft = widenconst(aty) + if isa(ft, DataType) && isdefined(ft.name, :mt) && isdefined(ft.name.mt, :kwsorter) + return CallMeta(Const(ft.name.mt.kwsorter), MethodResultPure()) + end end end + tristate_merge!(sv, EFFECTS_UNKNOWN) # TODO return CallMeta(Any, false) elseif f === TypeVar # Manually look through the definition of TypeVar to # make sure to be able to get `PartialTypeVar`s out. + tristate_merge!(sv, EFFECTS_UNKNOWN) # TODO (la < 2 || la > 4) && return CallMeta(Union{}, false) n = argtypes[2] ub_var = Const(Any) @@ -943,33 +1641,36 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), elseif la == 3 ub_var = argtypes[3] end - return CallMeta(typevar_tfunc(n, lb_var, ub_var), nothing) + return CallMeta(typevar_tfunc(n, lb_var, ub_var), false) elseif f === UnionAll + tristate_merge!(sv, EFFECTS_UNKNOWN) # TODO return CallMeta(abstract_call_unionall(argtypes), false) - elseif f === Tuple && la == 2 && !isconcretetype(widenconst(argtypes[2])) - return CallMeta(Tuple, false) - elseif is_return_type(f) - rt_rt = return_type_tfunc(interp, argtypes, sv) - if rt_rt !== nothing - return CallMeta(rt_rt, nothing) + elseif f === Tuple && la == 2 + tristate_merge!(sv, EFFECTS_UNKNOWN) # TODO + aty = argtypes[2] + ty = isvarargtype(aty) ? unwrapva(aty) : widenconst(aty) + if !isconcretetype(ty) + return CallMeta(Tuple, false) end - return CallMeta(Type, nothing) + elseif is_return_type(f) + tristate_merge!(sv, EFFECTS_UNKNOWN) # TODO + return return_type_tfunc(interp, argtypes, sv) elseif la == 2 && istopfunction(f, :!) # handle Conditional propagation through !Bool aty = argtypes[2] if isa(aty, Conditional) - call = abstract_call_gf_by_type(interp, f, Any[Const(f), Bool], Tuple{typeof(f), Bool}, sv) # make sure we've inferred `!(::Bool)` + call = abstract_call_gf_by_type(interp, f, ArgInfo(fargs, Any[Const(f), Bool]), Tuple{typeof(f), Bool}, sv, max_methods) # make sure we've inferred `!(::Bool)` return CallMeta(Conditional(aty.var, aty.elsetype, aty.vtype), call.info) end elseif la == 3 && istopfunction(f, :!==) # mark !== as exactly a negated call to === - rty = abstract_call_known(interp, (===), fargs, argtypes, sv).rt + rty = abstract_call_known(interp, (===), arginfo, sv, max_methods).rt if isa(rty, Conditional) - return CallMeta(Conditional(rty.var, rty.elsetype, rty.vtype), nothing) # swap if-else + return CallMeta(Conditional(rty.var, rty.elsetype, rty.vtype), false) # swap if-else elseif isa(rty, Const) - return CallMeta(Const(rty.val === false), nothing) + return CallMeta(Const(rty.val === false), MethodResultPure()) end - return CallMeta(rty, nothing) + return CallMeta(rty, false) elseif la == 3 && istopfunction(f, :(>:)) # mark issupertype as a exact alias for issubtype # swap T1 and T2 arguments and call <: @@ -979,51 +1680,90 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), fargs = nothing end argtypes = Any[typeof(<:), argtypes[3], argtypes[2]] - return CallMeta(abstract_call_known(interp, <:, fargs, argtypes, sv).rt, false) - elseif la == 2 && isa(argtypes[2], Const) && isa(argtypes[2].val, SimpleVector) && istopfunction(f, :length) + return CallMeta(abstract_call_known(interp, <:, ArgInfo(fargs, argtypes), sv, max_methods).rt, false) + elseif la == 2 && + (a2 = argtypes[2]; isa(a2, Const)) && (svecval = a2.val; isa(svecval, SimpleVector)) && + istopfunction(f, :length) # mark length(::SimpleVector) as @pure - return CallMeta(Const(length(argtypes[2].val)), false) - elseif la == 3 && isa(argtypes[2], Const) && isa(argtypes[3], Const) && - isa(argtypes[2].val, SimpleVector) && isa(argtypes[3].val, Int) && istopfunction(f, :getindex) + return CallMeta(Const(length(svecval)), MethodResultPure()) + elseif la == 3 && + (a2 = argtypes[2]; isa(a2, Const)) && (svecval = a2.val; isa(svecval, SimpleVector)) && + (a3 = argtypes[3]; isa(a3, Const)) && (idx = a3.val; isa(idx, Int)) && + istopfunction(f, :getindex) # mark getindex(::SimpleVector, i::Int) as @pure - svecval = argtypes[2].val::SimpleVector - idx = argtypes[3].val::Int if 1 <= idx <= length(svecval) && isassigned(svecval, idx) - return CallMeta(Const(getindex(svecval, idx)), false) + return CallMeta(Const(getindex(svecval, idx)), MethodResultPure()) end elseif la == 2 && istopfunction(f, :typename) - return CallMeta(typename_static(argtypes[2]), false) - elseif max_methods > 1 && istopfunction(f, :copyto!) - max_methods = 1 + return CallMeta(typename_static(argtypes[2]), MethodResultPure()) elseif la == 3 && istopfunction(f, :typejoin) - val = pure_eval_call(f, argtypes) - return CallMeta(val === false ? Type : val, MethodResultPure()) + if is_all_const_arg(arginfo) + val = _pure_eval_call(f, arginfo) + return CallMeta(val === nothing ? Type : val, MethodResultPure()) + end end atype = argtypes_to_type(argtypes) - return abstract_call_gf_by_type(interp, f, argtypes, atype, sv, max_methods) + return abstract_call_gf_by_type(interp, f, arginfo, atype, sv, max_methods) +end + +function abstract_call_opaque_closure(interp::AbstractInterpreter, closure::PartialOpaque, arginfo::ArgInfo, sv::InferenceState) + sig = argtypes_to_type(arginfo.argtypes) + (; rt, edge) = result = abstract_call_method(interp, closure.source, sig, Core.svec(), false, sv) + edge !== nothing && add_backedge!(edge, sv) + tt = closure.typ + sigT = (unwrap_unionall(tt)::DataType).parameters[1] + match = MethodMatch(sig, Core.svec(), closure.source, sig <: rewrap_unionall(sigT, tt)) + const_result = nothing + if !result.edgecycle + const_call_result = abstract_call_method_with_const_args(interp, result, + nothing, arginfo, match, sv) + if const_call_result !== nothing + if const_call_result.rt ⊑ rt + (; rt, const_result) = const_call_result + end + end + end + info = OpaqueClosureCallInfo(match, const_result) + return CallMeta(from_interprocedural!(rt, sv, arginfo, match.spec_types), info) +end + +function most_general_argtypes(closure::PartialOpaque) + ret = Any[] + cc = widenconst(closure) + argt = (unwrap_unionall(cc)::DataType).parameters[1] + if !isa(argt, DataType) || argt.name !== typename(Tuple) + argt = Tuple + end + return most_general_argtypes(closure.source, argt, false) end # call where the function is any lattice element -function abstract_call(interp::AbstractInterpreter, fargs::Union{Nothing,Vector{Any}}, argtypes::Vector{Any}, - sv::InferenceState, max_methods::Int = InferenceParams(interp).MAX_METHODS) - #print("call ", e.args[1], argtypes, "\n\n") +function abstract_call(interp::AbstractInterpreter, arginfo::ArgInfo, + sv::InferenceState, max_methods::Union{Int, Nothing} = nothing) + argtypes = arginfo.argtypes ft = argtypes[1] - if isa(ft, Const) - f = ft.val - elseif isconstType(ft) - f = ft.parameters[1] - elseif isa(ft, DataType) && isdefined(ft, :instance) - f = ft.instance - else + f = singleton_type(ft) + if isa(ft, PartialOpaque) + newargtypes = copy(argtypes) + newargtypes[1] = ft.env + tristate_merge!(sv, Effects()) # TODO + return abstract_call_opaque_closure(interp, ft, ArgInfo(arginfo.fargs, newargtypes), sv) + elseif (uft = unwrap_unionall(widenconst(ft)); isa(uft, DataType) && uft.name === typename(Core.OpaqueClosure)) + tristate_merge!(sv, Effects()) # TODO + return CallMeta(rewrap_unionall((uft::DataType).parameters[2], widenconst(ft)), false) + elseif f === nothing # non-constant function, but the number of arguments is known # and the ft is not a Builtin or IntrinsicFunction - if typeintersect(widenconst(ft), Builtin) != Union{} + if hasintersect(widenconst(ft), Union{Builtin, Core.OpaqueClosure}) + tristate_merge!(sv, Effects()) add_remark!(interp, sv, "Could not identify method table for call") return CallMeta(Any, false) end - return abstract_call_gf_by_type(interp, nothing, argtypes, argtypes_to_type(argtypes), sv, max_methods) + max_methods = max_methods === nothing ? get_max_methods(sv.mod, interp) : max_methods + return abstract_call_gf_by_type(interp, nothing, arginfo, argtypes_to_type(argtypes), sv, max_methods) end - return abstract_call_known(interp, f, fargs, argtypes, sv, max_methods) + max_methods = max_methods === nothing ? get_max_methods(f, sv.mod, interp) : max_methods + return abstract_call_known(interp, f, arginfo, sv, max_methods) end function sp_type_rewrap(@nospecialize(T), linfo::MethodInstance, isreturn::Bool) @@ -1045,10 +1785,11 @@ function sp_type_rewrap(@nospecialize(T), linfo::MethodInstance, isreturn::Bool) spsig = linfo.def.sig if isa(spsig, UnionAll) if !isempty(linfo.sparam_vals) - env = pointer_from_objref(linfo.sparam_vals) + sizeof(Ptr{Cvoid}) - T = ccall(:jl_instantiate_type_in_env, Any, (Any, Any, Ptr{Any}), T, spsig, env) + sparam_vals = Any[isvarargtype(v) ? TypeVar(:N, Union{}, Any) : + v for v in linfo.sparam_vals] + T = ccall(:jl_instantiate_type_in_env, Any, (Any, Any, Ptr{Any}), T, spsig, sparam_vals) isref && isreturn && T === Any && return Bottom # catch invalid return Ref{T} where T = Any - for v in linfo.sparam_vals + for v in sparam_vals if isa(v, TypeVar) T = UnionAll(v, T) end @@ -1058,10 +1799,7 @@ function sp_type_rewrap(@nospecialize(T), linfo::MethodInstance, isreturn::Bool) end end end - while isa(T, TypeVar) - T = T.ub - end - return T + return unwraptv(T) end function abstract_eval_cfunction(interp::AbstractInterpreter, e::Expr, vtypes::VarTable, sv::InferenceState) @@ -1072,13 +1810,13 @@ function abstract_eval_cfunction(interp::AbstractInterpreter, e::Expr, vtypes::V # this may be the wrong world for the call, # but some of the result is likely to be valid anyways # and that may help generate better codegen - abstract_call(interp, nothing, at, sv) + abstract_call(interp, ArgInfo(nothing, at), sv) nothing end function abstract_eval_value_expr(interp::AbstractInterpreter, e::Expr, vtypes::VarTable, sv::InferenceState) if e.head === :static_parameter - n = e.args[1] + n = e.args[1]::Int t = Any if 1 <= n <= length(sv.sptypes) t = sv.sptypes[n] @@ -1093,13 +1831,13 @@ end function abstract_eval_special_value(interp::AbstractInterpreter, @nospecialize(e), vtypes::VarTable, sv::InferenceState) if isa(e, QuoteNode) - return Const((e::QuoteNode).value) + return Const(e.value) elseif isa(e, SSAValue) - return abstract_eval_ssavalue(e::SSAValue, sv.src) - elseif isa(e, Slot) + return abstract_eval_ssavalue(e, sv) + elseif isa(e, SlotNumber) || isa(e, Argument) return vtypes[slot_id(e)].typ elseif isa(e, GlobalRef) - return abstract_eval_global(e.mod, e.name) + return abstract_eval_global(e.mod, e.name, sv) end return Const(e) @@ -1109,80 +1847,139 @@ function abstract_eval_value(interp::AbstractInterpreter, @nospecialize(e), vtyp if isa(e, Expr) return abstract_eval_value_expr(interp, e, vtypes, sv) else - return abstract_eval_special_value(interp, e, vtypes, sv) + typ = abstract_eval_special_value(interp, e, vtypes, sv) + return collect_limitations!(typ, sv) + end +end + +function collect_argtypes(interp::AbstractInterpreter, ea::Vector{Any}, vtypes::VarTable, sv::InferenceState) + n = length(ea) + argtypes = Vector{Any}(undef, n) + @inbounds for i = 1:n + ai = abstract_eval_value(interp, ea[i], vtypes, sv) + if ai === Bottom + return nothing + end + argtypes[i] = ai end + return argtypes end function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), vtypes::VarTable, sv::InferenceState) if !isa(e, Expr) + if isa(e, PhiNode) + rt = Union{} + for val in e.values + rt = tmerge(rt, abstract_eval_special_value(interp, val, vtypes, sv)) + end + return rt + end return abstract_eval_special_value(interp, e, vtypes, sv) end e = e::Expr - if e.head === :call + ehead = e.head + if ehead === :call ea = e.args - n = length(ea) - argtypes = Vector{Any}(undef, n) - @inbounds for i = 1:n - ai = abstract_eval_value(interp, ea[i], vtypes, sv) - if ai === Bottom - return Bottom - end - argtypes[i] = ai - end - callinfo = abstract_call(interp, ea, argtypes, sv) - sv.stmt_info[sv.currpc] = callinfo.info - t = callinfo.rt - elseif e.head === :new - t = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv))[1] - if isconcretetype(t) && !t.mutable - args = Vector{Any}(undef, length(e.args)-1) - ats = Vector{Any}(undef, length(e.args)-1) - anyconst = false - allconst = true + argtypes = collect_argtypes(interp, ea, vtypes, sv) + if argtypes === nothing + t = Bottom + else + callinfo = abstract_call(interp, ArgInfo(ea, argtypes), sv) + sv.stmt_info[sv.currpc] = callinfo.info + t = callinfo.rt + end + elseif ehead === :new + t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) + is_nothrow = true + if isconcretedispatch(t) + fcount = fieldcount(t) + nargs = length(e.args) - 1 + is_nothrow && (is_nothrow = fcount ≥ nargs) + ats = Vector{Any}(undef, nargs) + local anyrefine = false + local allconst = true for i = 2:length(e.args) - at = abstract_eval_value(interp, e.args[i], vtypes, sv) - if !anyconst - anyconst = has_nontrivial_const_info(at) - end - ats[i-1] = at + at = widenconditional(abstract_eval_value(interp, e.args[i], vtypes, sv)) + ft = fieldtype(t, i-1) + is_nothrow && (is_nothrow = at ⊑ ft) + at = tmeet(at, ft) if at === Bottom t = Bottom - allconst = anyconst = false - break - elseif at isa Const - if !(at.val isa fieldtype(t, i - 1)) - t = Bottom - allconst = anyconst = false - break - end - args[i-1] = at.val - else + tristate_merge!(sv, Effects(EFFECTS_TOTAL; + # consistent = ALWAYS_TRUE, # N.B depends on !ismutabletype(t) above + nothrow = TRISTATE_UNKNOWN)) + @goto t_computed + elseif !isa(at, Const) allconst = false end + if !anyrefine + anyrefine = has_nontrivial_const_info(at) || # constant information + at ⋤ ft # just a type-level information, but more precise than the declared type + end + ats[i-1] = at end - # For now, don't allow partially initialized Const/PartialStruct - if t !== Bottom && fieldcount(t) == length(ats) + # For now, don't allow: + # - Const/PartialStruct of mutables + # - partially initialized Const/PartialStruct + if !ismutabletype(t) && fcount == nargs if allconst - t = Const(ccall(:jl_new_structv, Any, (Any, Ptr{Cvoid}, UInt32), t, args, length(args))) - elseif anyconst + argvals = Vector{Any}(undef, nargs) + for j in 1:nargs + argvals[j] = (ats[j]::Const).val + end + t = Const(ccall(:jl_new_structv, Any, (Any, Ptr{Cvoid}, UInt32), t, argvals, nargs)) + elseif anyrefine t = PartialStruct(t, ats) end end - end - elseif e.head === :splatnew - t = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv))[1] - if length(e.args) == 2 && isconcretetype(t) && !t.mutable + else + is_nothrow = false + end + tristate_merge!(sv, Effects(EFFECTS_TOTAL; + consistent = !ismutabletype(t) ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + nothrow = is_nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN)) + elseif ehead === :splatnew + t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) + is_nothrow = false # TODO: More precision + if length(e.args) == 2 && isconcretetype(t) && !ismutabletype(t) at = abstract_eval_value(interp, e.args[2], vtypes, sv) n = fieldcount(t) - if isa(at, Const) && isa(at.val, Tuple) && n == length(at.val) && - let t = t, at = at; _all(i->at.val[i] isa fieldtype(t, i), 1:n); end + if isa(at, Const) && isa(at.val, Tuple) && n == length(at.val::Tuple) && + let t = t, at = at; _all(i->getfield(at.val::Tuple, i) isa fieldtype(t, i), 1:n); end + is_nothrow = isexact && isconcretedispatch(t) t = Const(ccall(:jl_new_structt, Any, (Any, Any), t, at.val)) - elseif isa(at, PartialStruct) && at ⊑ Tuple && n == length(at.fields) && - let t = t, at = at; _all(i->at.fields[i] ⊑ fieldtype(t, i), 1:n); end - t = PartialStruct(t, at.fields) + elseif isa(at, PartialStruct) && at ⊑ Tuple && n == length(at.fields::Vector{Any}) && + let t = t, at = at; _all(i->(at.fields::Vector{Any})[i] ⊑ fieldtype(t, i), 1:n); end + is_nothrow = isexact && isconcretedispatch(t) + t = PartialStruct(t, at.fields::Vector{Any}) + end + end + tristate_merge!(sv, Effects(EFFECTS_TOTAL; + consistent = ismutabletype(t) ? TRISTATE_UNKNOWN : ALWAYS_TRUE, + nothrow = is_nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN)) + elseif ehead === :new_opaque_closure + tristate_merge!(sv, Effects()) # TODO + t = Union{} + if length(e.args) >= 4 + ea = e.args + argtypes = collect_argtypes(interp, ea, vtypes, sv) + if argtypes === nothing + t = Bottom + else + t = _opaque_closure_tfunc(argtypes[1], argtypes[2], argtypes[3], + argtypes[4], argtypes[5:end], sv.linfo) + if isa(t, PartialOpaque) + # Infer this now so that the specialization is available to + # optimization. + argtypes = most_general_argtypes(t) + pushfirst!(argtypes, t.env) + callinfo = abstract_call_opaque_closure(interp, t, + ArgInfo(nothing, argtypes), sv) + sv.stmt_info[sv.currpc] = OpaqueClosureCreateInfo(callinfo) + end end end - elseif e.head === :foreigncall + elseif ehead === :foreigncall abstract_eval_value(interp, e.args[1], vtypes, sv) t = sp_type_rewrap(e.args[2], sv.linfo, true) for i = 3:length(e.args) @@ -1190,24 +1987,41 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), t = Bottom end end - elseif e.head === :cfunction + cconv = e.args[5] + if isa(cconv, QuoteNode) && (v = cconv.value; isa(v, Tuple{Symbol, UInt8})) + effects = v[2] + effects = decode_effects_override(effects) + tristate_merge!(sv, Effects( + effects.consistent ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + effects.effect_free ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + effects.nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + effects.terminates_globally ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + #=nonoverlayed=#true + )) + else + tristate_merge!(sv, EFFECTS_UNKNOWN) + end + elseif ehead === :cfunction + tristate_merge!(sv, EFFECTS_UNKNOWN) t = e.args[1] isa(t, Type) || (t = Any) abstract_eval_cfunction(interp, e, vtypes, sv) - elseif e.head === :method + elseif ehead === :method + tristate_merge!(sv, EFFECTS_UNKNOWN) t = (length(e.args) == 1) ? Any : Nothing - elseif e.head === :copyast + elseif ehead === :copyast + tristate_merge!(sv, EFFECTS_UNKNOWN) t = abstract_eval_value(interp, e.args[1], vtypes, sv) if t isa Const && t.val isa Expr # `copyast` makes copies of Exprs t = Expr end - elseif e.head === :invoke + elseif ehead === :invoke || ehead === :invoke_modify error("type inference data-flow error: tried to double infer a function") - elseif e.head === :isdefined + elseif ehead === :isdefined sym = e.args[1] t = Bool - if isa(sym, Slot) + if isa(sym, SlotNumber) vtyp = vtypes[slot_id(sym)] if vtyp.typ === Bottom t = Const(false) # never assigned previously @@ -1223,7 +2037,7 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), t = Const(true) end elseif isa(sym, Expr) && sym.head === :static_parameter - n = sym.args[1] + n = sym.args[1]::Int if 1 <= n <= length(sv.sptypes) spty = sv.sptypes[n] if isa(spty, Const) @@ -1232,211 +2046,371 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), end end else - return abstract_eval_value_expr(interp, e, vtypes, sv) + t = abstract_eval_value_expr(interp, e, vtypes, sv) end - @assert !isa(t, TypeVar) + @label t_computed + @assert !isa(t, TypeVar) "unhandled TypeVar" if isa(t, DataType) && isdefined(t, :instance) # replace singleton types with their equivalent Const object t = Const(t.instance) end + if !isempty(sv.pclimitations) + if t isa Const || t === Union{} + empty!(sv.pclimitations) + else + t = LimitedAccuracy(t, sv.pclimitations) + sv.pclimitations = IdSet{InferenceState}() + end + end return t end function abstract_eval_global(M::Module, s::Symbol) - if isdefined(M,s) && isconst(M,s) - return Const(getfield(M,s)) + if isdefined(M, s) && isconst(M, s) + return Const(getglobal(M, s)) end - return Any + ty = ccall(:jl_binding_type, Any, (Any, Any), M, s) + ty === nothing && return Any + return ty +end + +function abstract_eval_global(M::Module, s::Symbol, frame::InferenceState) + ty = abstract_eval_global(M, s) + isa(ty, Const) && return ty + if isdefined(M,s) + tristate_merge!(frame, Effects(EFFECTS_TOTAL; consistent=TRISTATE_UNKNOWN)) + else + tristate_merge!(frame, Effects(EFFECTS_TOTAL; + consistent=TRISTATE_UNKNOWN, + nothrow=TRISTATE_UNKNOWN)) + end + return ty end +abstract_eval_ssavalue(s::SSAValue, sv::InferenceState) = abstract_eval_ssavalue(s, sv.src) function abstract_eval_ssavalue(s::SSAValue, src::CodeInfo) - typ = src.ssavaluetypes[s.id] + typ = (src.ssavaluetypes::Vector{Any})[s.id] if typ === NOT_FOUND return Bottom end return typ end +function widenreturn(@nospecialize(rt), @nospecialize(bestguess), nargs::Int, slottypes::Vector{Any}, changes::VarTable) + if !(bestguess ⊑ Bool) || bestguess === Bool + # give up inter-procedural constraint back-propagation + # when tmerge would widen the result anyways (as an optimization) + rt = widenconditional(rt) + else + if isa(rt, Conditional) + id = slot_id(rt.var) + if 1 ≤ id ≤ nargs + old_id_type = widenconditional(slottypes[id]) # same as `(states[1]::VarTable)[id].typ` + if (!(rt.vtype ⊑ old_id_type) || old_id_type ⊑ rt.vtype) && + (!(rt.elsetype ⊑ old_id_type) || old_id_type ⊑ rt.elsetype) + # discard this `Conditional` since it imposes + # no new constraint on the argument type + # (the caller will recreate it if needed) + rt = widenconditional(rt) + end + else + # discard this `Conditional` imposed on non-call arguments, + # since it's not interesting in inter-procedural context; + # we may give constraints on other call argument + rt = widenconditional(rt) + end + end + if isa(rt, Conditional) + rt = InterConditional(slot_id(rt.var), rt.vtype, rt.elsetype) + elseif is_lattice_bool(rt) + if isa(bestguess, InterConditional) + # if the bestguess so far is already `Conditional`, try to convert + # this `rt` into `Conditional` on the slot to avoid overapproximation + # due to conflict of different slots + rt = bool_rt_to_conditional(rt, slottypes, changes, bestguess.slot) + else + # pick up the first "interesting" slot, convert `rt` to its `Conditional` + # TODO: ideally we want `Conditional` and `InterConditional` to convey + # constraints on multiple slots + for slot_id in 1:nargs + rt = bool_rt_to_conditional(rt, slottypes, changes, slot_id) + rt isa InterConditional && break + end + end + end + end + + # only propagate information we know we can store + # and is valid and good inter-procedurally + isa(rt, Conditional) && return InterConditional(slot_id(rt.var), rt.vtype, rt.elsetype) + isa(rt, InterConditional) && return rt + return widenreturn_noconditional(rt) +end + +function widenreturn_noconditional(@nospecialize(rt)) + isa(rt, Const) && return rt + isa(rt, Type) && return rt + if isa(rt, PartialStruct) + fields = copy(rt.fields) + local anyrefine = false + for i in 1:length(fields) + a = fields[i] + a = isvarargtype(a) ? a : widenreturn_noconditional(widenconditional(a)) + if !anyrefine + # TODO: consider adding && const_prop_profitable(a) here? + anyrefine = has_const_info(a) || + a ⊏ fieldtype(rt.typ, i) + end + fields[i] = a + end + anyrefine && return PartialStruct(rt.typ, fields) + end + if isa(rt, PartialOpaque) + return rt # XXX: this case was missed in #39512 + end + return widenconst(rt) +end + + +function handle_control_backedge!(frame::InferenceState, from::Int, to::Int) + if from > to + if is_effect_overridden(frame, :terminates_globally) + # this frame is known to terminate + elseif is_effect_overridden(frame, :terminates_locally) + # this backedge is known to terminate + else + tristate_merge!(frame, Effects(EFFECTS_TOTAL; terminates=TRISTATE_UNKNOWN)) + end + end + return nothing +end + # make as much progress on `frame` as possible (without handling cycles) function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) @assert !frame.inferred frame.dont_work_on_me = true # mark that this function is currently on the stack W = frame.ip - s = frame.stmt_types - n = frame.nstmts - while frame.pc´´ <= n + states = frame.stmt_types + def = frame.linfo.def + isva = isa(def, Method) && def.isva + nargs = length(frame.result.argtypes) - isva + slottypes = frame.slottypes + ssavaluetypes = frame.src.ssavaluetypes::Vector{Any} + while !isempty(W) # make progress on the active ip set - local pc::Int = frame.pc´´ # current program-counter - while true # inner loop optimizes the common case where it can run straight from pc to pc + 1 - #print(pc,": ",s[pc],"\n") - local pc´::Int = pc + 1 # next program-counter (after executing instruction) - if pc == frame.pc´´ - # need to update pc´´ to point at the new lowest instruction in W - min_pc = _bits_findnext(W.bits, pc + 1) - frame.pc´´ = min_pc == -1 ? n + 1 : min_pc - end - delete!(W, pc) - frame.currpc = pc - frame.cur_hand = frame.handler_at[pc] - frame.stmt_edges[pc] === nothing || empty!(frame.stmt_edges[pc]) - stmt = frame.src.code[pc] - changes = s[pc]::VarTable - t = nothing - - hd = isa(stmt, Expr) ? stmt.head : nothing - - if isa(stmt, NewvarNode) - sn = slot_id(stmt.slot) - changes[sn] = VarState(Bottom, true) - elseif isa(stmt, GotoNode) - pc´ = (stmt::GotoNode).label - elseif isa(stmt, GotoIfNot) - condt = abstract_eval_value(interp, stmt.cond, s[pc], frame) - if condt === Bottom - break - end - condval = maybe_extract_const_bool(condt) - l = stmt.dest::Int - # constant conditions - if condval === true - elseif condval === false - pc´ = l - else - # general case - frame.handler_at[l] = frame.cur_hand - changes_else = changes - if isa(condt, Conditional) - if condt.elsetype !== Any && condt.elsetype !== changes[slot_id(condt.var)] - changes_else = StateUpdate(condt.var, VarState(condt.elsetype, false), changes_else) - end - if condt.vtype !== Any && condt.vtype !== changes[slot_id(condt.var)] - changes = StateUpdate(condt.var, VarState(condt.vtype, false), changes) - end - end - newstate_else = stupdate!(s[l], changes_else) - if newstate_else !== false - # add else branch to active IP list - if l < frame.pc´´ - frame.pc´´ = l - end - push!(W, l) - s[l] = newstate_else - end + local pc::Int = popfirst!(W) + local pc´::Int = pc + 1 # next program-counter (after executing instruction) + frame.currpc = pc + edges = frame.stmt_edges[pc] + edges === nothing || empty!(edges) + frame.stmt_info[pc] = nothing + stmt = frame.src.code[pc] + changes = states[pc]::VarTable + t = nothing + + hd = isa(stmt, Expr) ? stmt.head : nothing + + if isa(stmt, NewvarNode) + sn = slot_id(stmt.slot) + changes[sn] = VarState(Bottom, true) + elseif isa(stmt, GotoNode) + l = (stmt::GotoNode).label + handle_control_backedge!(frame, pc, l) + pc´ = l + elseif isa(stmt, GotoIfNot) + condx = stmt.cond + condt = abstract_eval_value(interp, condx, changes, frame) + if condt === Bottom + empty!(frame.pclimitations) + continue + end + if !(isa(condt, Const) || isa(condt, Conditional)) && isa(condx, SlotNumber) + # if this non-`Conditional` object is a slot, we form and propagate + # the conditional constraint on it + condt = Conditional(condx, Const(true), Const(false)) + end + condval = maybe_extract_const_bool(condt) + l = stmt.dest::Int + if !isempty(frame.pclimitations) + # we can't model the possible effect of control + # dependencies on the return value, so we propagate it + # directly to all the return values (unless we error first) + condval isa Bool || union!(frame.limitations, frame.pclimitations) + empty!(frame.pclimitations) + end + # constant conditions + if condval === true + elseif condval === false + handle_control_backedge!(frame, pc, l) + pc´ = l + else + # general case + changes_else = changes + if isa(condt, Conditional) + changes_else = conditional_changes(changes_else, condt.elsetype, condt.var) + changes = conditional_changes(changes, condt.vtype, condt.var) end - elseif isa(stmt, ReturnNode) - pc´ = n + 1 - rt = widenconditional(abstract_eval_value(interp, stmt.val, s[pc], frame)) - if !isa(rt, Const) && !isa(rt, Type) && !isa(rt, PartialStruct) - # only propagate information we know we can store - # and is valid inter-procedurally - rt = widenconst(rt) + newstate_else = stupdate!(states[l], changes_else) + if newstate_else !== nothing + handle_control_backedge!(frame, pc, l) + # add else branch to active IP list + push!(W, l) + states[l] = newstate_else end - if tchanged(rt, frame.bestguess) - # new (wider) return type for frame - frame.bestguess = tmerge(frame.bestguess, rt) - for (caller, caller_pc) in frame.cycle_backedges - # notify backedges of updated type information - typeassert(caller.stmt_types[caller_pc], VarTable) # we must have visited this statement before - if !(caller.src.ssavaluetypes[caller_pc] === Any) - # no reason to revisit if that call-site doesn't affect the final result - if caller_pc < caller.pc´´ - caller.pc´´ = caller_pc - end - push!(caller.ip, caller_pc) - end + end + elseif isa(stmt, ReturnNode) + bestguess = frame.bestguess + rt = abstract_eval_value(interp, stmt.val, changes, frame) + rt = widenreturn(rt, bestguess, nargs, slottypes, changes) + # narrow representation of bestguess slightly to prepare for tmerge with rt + if rt isa InterConditional && bestguess isa Const + let slot_id = rt.slot + old_id_type = slottypes[slot_id] + if bestguess.val === true && rt.elsetype !== Bottom + bestguess = InterConditional(slot_id, old_id_type, Bottom) + elseif bestguess.val === false && rt.vtype !== Bottom + bestguess = InterConditional(slot_id, Bottom, old_id_type) end end - elseif hd === :enter - l = stmt.args[1]::Int - frame.cur_hand = Pair{Any,Any}(l, frame.cur_hand) - # propagate type info to exception handler - old = s[l] - new = s[pc]::Array{Any,1} - newstate_catch = stupdate!(old, new) - if newstate_catch !== false - if l < frame.pc´´ - frame.pc´´ = l + end + # copy limitations to return value + if !isempty(frame.pclimitations) + union!(frame.limitations, frame.pclimitations) + empty!(frame.pclimitations) + end + if !isempty(frame.limitations) + rt = LimitedAccuracy(rt, copy(frame.limitations)) + end + if tchanged(rt, bestguess) + # new (wider) return type for frame + bestguess = tmerge(bestguess, rt) + # TODO: if bestguess isa InterConditional && !interesting(bestguess); bestguess = widenconditional(bestguess); end + frame.bestguess = bestguess + for (caller, caller_pc) in frame.cycle_backedges + # notify backedges of updated type information + typeassert(caller.stmt_types[caller_pc], VarTable) # we must have visited this statement before + if !((caller.src.ssavaluetypes::Vector{Any})[caller_pc] === Any) + # no reason to revisit if that call-site doesn't affect the final result + push!(caller.ip, caller_pc) end - push!(W, l) - s[l] = newstate_catch end - typeassert(s[l], VarTable) - frame.handler_at[l] = frame.cur_hand - elseif hd === :leave - for i = 1:((stmt.args[1])::Int) - frame.cur_hand = (frame.cur_hand::Pair{Any,Any}).second + end + continue + elseif hd === :enter + stmt = stmt::Expr + l = stmt.args[1]::Int + # propagate type info to exception handler + old = states[l] + newstate_catch = stupdate!(old, changes) + if newstate_catch !== nothing + push!(W, l) + states[l] = newstate_catch + end + typeassert(states[l], VarTable) + elseif hd === :leave + else + if hd === :(=) + stmt = stmt::Expr + t = abstract_eval_statement(interp, stmt.args[2], changes, frame) + if t === Bottom + continue + end + ssavaluetypes[pc] = t + lhs = stmt.args[1] + if isa(lhs, SlotNumber) + changes = StateUpdate(lhs, VarState(t, false), changes, false) + elseif isa(lhs, GlobalRef) + tristate_merge!(frame, Effects(EFFECTS_TOTAL, + effect_free=TRISTATE_UNKNOWN, + nothrow=TRISTATE_UNKNOWN)) + elseif !isa(lhs, SSAValue) + tristate_merge!(frame, EFFECTS_UNKNOWN) + end + elseif hd === :method + stmt = stmt::Expr + fname = stmt.args[1] + if isa(fname, SlotNumber) + changes = StateUpdate(fname, VarState(Any, false), changes, false) end + elseif hd === :code_coverage_effect || + (hd !== :boundscheck && # :boundscheck can be narrowed to Bool + hd !== nothing && is_meta_expr_head(hd)) + # these do not generate code else - if hd === :(=) - t = abstract_eval_statement(interp, stmt.args[2], changes, frame) - t === Bottom && break - frame.src.ssavaluetypes[pc] = t - lhs = stmt.args[1] - if isa(lhs, Slot) - changes = StateUpdate(lhs, VarState(t, false), changes) - end - elseif hd === :method - fname = stmt.args[1] - if isa(fname, Slot) - changes = StateUpdate(fname, VarState(Any, false), changes) - end - elseif hd === :inbounds || hd === :meta || hd === :loopinfo || hd === :code_coverage_effect - # these do not generate code + t = abstract_eval_statement(interp, stmt, changes, frame) + if t === Bottom + continue + end + if !isempty(frame.ssavalue_uses[pc]) + record_ssa_assign(pc, t, frame) else - t = abstract_eval_statement(interp, stmt, changes, frame) - t === Bottom && break - if !isempty(frame.ssavalue_uses[pc]) - record_ssa_assign(pc, t, frame) - else - frame.src.ssavaluetypes[pc] = t - end + ssavaluetypes[pc] = t end - if frame.cur_hand !== nothing && isa(changes, StateUpdate) - # propagate new type info to exception handler - # the handling for Expr(:enter) propagates all changes from before the try/catch - # so this only needs to propagate any changes - l = frame.cur_hand.first::Int - if stupdate1!(s[l]::VarTable, changes::StateUpdate) !== false - if l < frame.pc´´ - frame.pc´´ = l + end + if isa(changes, StateUpdate) + let cur_hand = frame.handler_at[pc], l, enter + while cur_hand != 0 + enter = frame.src.code[cur_hand] + l = (enter::Expr).args[1]::Int + # propagate new type info to exception handler + # the handling for Expr(:enter) propagates all changes from before the try/catch + # so this only needs to propagate any changes + if stupdate1!(states[l]::VarTable, changes::StateUpdate) !== false + push!(W, l) end - push!(W, l) + cur_hand = frame.handler_at[cur_hand] end end end + end - if t === nothing - # mark other reached expressions as `Any` to indicate they don't throw - frame.src.ssavaluetypes[pc] = Any - end + @assert isempty(frame.pclimitations) "unhandled LimitedAccuracy" - pc´ > n && break # can't proceed with the fast-path fall-through - frame.handler_at[pc´] = frame.cur_hand - newstate = stupdate!(s[pc´], changes) - if isa(stmt, GotoNode) && frame.pc´´ < pc´ - # if we are processing a goto node anyways, - # (such as a terminator for a loop, if-else, or try block), - # consider whether we should jump to an older backedge first, - # to try to traverse the statements in approximate dominator order - if newstate !== false - s[pc´] = newstate - end - push!(W, pc´) - pc = frame.pc´´ - elseif newstate !== false - s[pc´] = newstate - pc = pc´ - elseif pc´ in W - pc = pc´ - else - break - end + if t === nothing + # mark other reached expressions as `Any` to indicate they don't throw + ssavaluetypes[pc] = Any + end + + newstate = stupdate!(states[pc´], changes) + if newstate !== nothing + states[pc´] = newstate + push!(W, pc´) end end frame.dont_work_on_me = false nothing end +function conditional_changes(changes::VarTable, @nospecialize(typ), var::SlotNumber) + oldtyp = changes[slot_id(var)].typ + # approximate test for `typ ∩ oldtyp` being better than `oldtyp` + # since we probably formed these types with `typesubstract`, the comparison is likely simple + if ignorelimited(typ) ⊑ ignorelimited(oldtyp) + # typ is better unlimited, but we may still need to compute the tmeet with the limit "causes" since we ignored those in the comparison + oldtyp isa LimitedAccuracy && (typ = tmerge(typ, LimitedAccuracy(Bottom, oldtyp.causes))) + return StateUpdate(var, VarState(typ, false), changes, true) + end + return changes +end + +function bool_rt_to_conditional(@nospecialize(rt), slottypes::Vector{Any}, state::VarTable, slot_id::Int) + old = slottypes[slot_id] + new = widenconditional(state[slot_id].typ) # avoid nested conditional + if new ⊑ old && !(old ⊑ new) + if isa(rt, Const) + val = rt.val + if val === true + return InterConditional(slot_id, new, Bottom) + elseif val === false + return InterConditional(slot_id, Bottom, new) + end + elseif rt === Bool + return InterConditional(slot_id, new, new) + end + end + return rt +end + # make as much progress on `frame` as possible (by handling cycles) function typeinf_nocycle(interp::AbstractInterpreter, frame::InferenceState) typeinf_local(interp, frame) @@ -1447,7 +2421,7 @@ function typeinf_nocycle(interp::AbstractInterpreter, frame::InferenceState) no_active_ips_in_callers = true for caller in frame.callers_in_cycle caller.dont_work_on_me && return false # cycle is above us on the stack - if caller.pc´´ <= caller.nstmts # equivalent to `isempty(caller.ip)` + if !isempty(caller.ip) # Note that `typeinf_local(interp, caller)` can potentially modify the other frames # `frame.callers_in_cycle`, which is why making incremental progress requires the # outer while loop. diff --git a/base/compiler/bootstrap.jl b/base/compiler/bootstrap.jl index f351429aff7eb7..f335cf31a8467b 100644 --- a/base/compiler/bootstrap.jl +++ b/base/compiler/bootstrap.jl @@ -5,10 +5,21 @@ # especially try to make sure any recursive and leaf functions have concrete signatures, # since we won't be able to specialize & infer them at runtime -let fs = Any[typeinf_ext, typeinf, typeinf_edge, pure_eval_call, run_passes], - world = get_world_counter(), +time() = ccall(:jl_clock_now, Float64, ()) + +let + world = get_world_counter() interp = NativeInterpreter(world) + analyze_escapes_tt = Tuple{typeof(analyze_escapes), IRCode, Int, Bool, typeof(null_escape_cache)} + fs = Any[ + # we first create caches for the optimizer, because they contain many loop constructions + # and they're better to not run in interpreter even during bootstrapping + #=analyze_escapes_tt,=# run_passes, + # then we create caches for inference entries + typeinf_ext, typeinf, typeinf_edge, + ] + # tfuncs can't be inferred from the inference entries above, so here we infer them manually for x in T_FFUNC_VAL push!(fs, x[3]) end @@ -20,16 +31,22 @@ let fs = Any[typeinf_ext, typeinf, typeinf_edge, pure_eval_call, run_passes], println(stderr, "WARNING: tfunc missing for ", reinterpret(IntrinsicFunction, Int32(i))) end end + starttime = time() for f in fs - for m in _methods_by_ftype(Tuple{typeof(f), Vararg{Any}}, 10, typemax(UInt)) + if isa(f, DataType) && f.name === typename(Tuple) + tt = f + else + tt = Tuple{typeof(f), Vararg{Any}} + end + for m in _methods_by_ftype(tt, 10, typemax(UInt)) # remove any TypeVars from the intersection typ = Any[m.spec_types.parameters...] for i = 1:length(typ) - if isa(typ[i], TypeVar) - typ[i] = typ[i].ub - end + typ[i] = unwraptv(typ[i]) end typeinf_type(interp, m.method, Tuple{typ...}, m.sparams) end end + endtime = time() + println("Core.Compiler ──── ", sub_float(endtime,starttime), " seconds") end diff --git a/base/compiler/cicache.jl b/base/compiler/cicache.jl index 9adaf6ded0b0fd..294b1f0055f790 100644 --- a/base/compiler/cicache.jl +++ b/base/compiler/cicache.jl @@ -1,3 +1,5 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + """ struct InternalCodeCache diff --git a/base/compiler/compiler.jl b/base/compiler/compiler.jl index 986b8f6497fa37..1132b8976e53c6 100644 --- a/base/compiler/compiler.jl +++ b/base/compiler/compiler.jl @@ -1,15 +1,19 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -getfield(getfield(Main, :Core), :eval)(getfield(Main, :Core), :(baremodule Compiler +getfield(Core, :eval)(Core, :(baremodule Compiler using Core.Intrinsics, Core.IR import Core: print, println, show, write, unsafe_write, stdout, stderr, - _apply, _apply_iterate, svec, apply_type, Builtin, IntrinsicFunction, - MethodInstance, CodeInstance, MethodMatch + _apply_iterate, svec, apply_type, Builtin, IntrinsicFunction, + MethodInstance, CodeInstance, MethodMatch, PartialOpaque, + TypeofVararg const getproperty = Core.getfield const setproperty! = Core.setfield! +const swapproperty! = Core.swapfield! +const modifyproperty! = Core.modifyfield! +const replaceproperty! = Core.replacefield! ccall(:jl_set_istopmod, Cvoid, (Any, Bool), Compiler, false) @@ -19,9 +23,13 @@ eval(m, x) = Core.eval(m, x) include(x) = Core.include(Compiler, x) include(mod, x) = Core.include(mod, x) -############# -# from Base # -############# +# The @inline/@noinline macros that can be applied to a function declaration are not available +# until after array.jl, and so we will mark them within a function body instead. +macro inline() Expr(:meta, :inline) end +macro noinline() Expr(:meta, :noinline) end + +convert(::Type{Any}, Core.@nospecialize x) = x +convert(::Type{T}, x::T) where {T} = x # essential files and libraries include("essentials.jl") @@ -50,6 +58,9 @@ include("operators.jl") include("pointer.jl") include("refvalue.jl") +# the same constructor as defined in float.jl, but with a different name to avoid redefinition +_Bool(x::Real) = x==0 ? false : x==1 ? true : throw(InexactError(:Bool, Bool, x)) + # checked arithmetic const checked_add = + const checked_sub = - @@ -62,6 +73,8 @@ add_with_overflow(x::T, y::T) where {T<:SignedInt} = checked_sadd_int(x, y) add_with_overflow(x::T, y::T) where {T<:UnsignedInt} = checked_uadd_int(x, y) add_with_overflow(x::Bool, y::Bool) = (x+y, false) +include("strings/lazy.jl") + # core array operations include("indices.jl") include("array.jl") @@ -80,14 +93,16 @@ using .Iterators: Flatten, Filter, product # for generators include("namedtuple.jl") ntuple(f, ::Val{0}) = () -ntuple(f, ::Val{1}) = (@_inline_meta; (f(1),)) -ntuple(f, ::Val{2}) = (@_inline_meta; (f(1), f(2))) -ntuple(f, ::Val{3}) = (@_inline_meta; (f(1), f(2), f(3))) +ntuple(f, ::Val{1}) = (@inline; (f(1),)) +ntuple(f, ::Val{2}) = (@inline; (f(1), f(2))) +ntuple(f, ::Val{3}) = (@inline; (f(1), f(2), f(3))) ntuple(f, ::Val{n}) where {n} = ntuple(f, n::Int) ntuple(f, n) = (Any[f(i) for i = 1:n]...,) # core docsystem include("docs/core.jl") +import Core.Compiler.CoreDocs +Core.atdoc!(CoreDocs.docm) # sorting function sort end @@ -99,15 +114,18 @@ using .Order include("sort.jl") using .Sort +# We don't include some.jl, but this definition is still useful. +something(x::Nothing, y...) = something(y...) +something(x::Any, y...) = x + ############ # compiler # ############ +include("compiler/cicache.jl") include("compiler/types.jl") include("compiler/utilities.jl") include("compiler/validation.jl") - -include("compiler/cicache.jl") include("compiler/methodtable.jl") include("compiler/inferenceresult.jl") @@ -123,6 +141,21 @@ include("compiler/abstractinterpretation.jl") include("compiler/typeinfer.jl") include("compiler/optimize.jl") # TODO: break this up further + extract utilities +# required for bootstrap because sort.jl uses extrema +# to decide whether to dispatch to counting sort. +# +# TODO: remove it. +function extrema(x::Array) + isempty(x) && throw(ArgumentError("collection must be non-empty")) + vmin = vmax = x[1] + for i in 2:length(x) + xi = x[i] + vmax = max(vmax, xi) + vmin = min(vmin, xi) + end + return vmin, vmax +end + include("compiler/bootstrap.jl") ccall(:jl_set_typeinf_func, Cvoid, (Any,), typeinf_ext_toplevel) @@ -131,4 +164,3 @@ Core.eval(Core, :(_parse = Compiler.fl_parse)) end # baremodule Compiler )) - diff --git a/base/compiler/inferenceresult.jl b/base/compiler/inferenceresult.jl index 89176b54ec93b5..36702382ef6f5b 100644 --- a/base/compiler/inferenceresult.jl +++ b/base/compiler/inferenceresult.jl @@ -1,63 +1,116 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -const EMPTY_VECTOR = Vector{Any}() - function is_argtype_match(@nospecialize(given_argtype), @nospecialize(cache_argtype), overridden_by_const::Bool) - if isa(given_argtype, Const) || isa(given_argtype, PartialStruct) + if is_forwardable_argtype(given_argtype) return is_lattice_equal(given_argtype, cache_argtype) end return !overridden_by_const end +function is_forwardable_argtype(@nospecialize x) + return isa(x, Const) || + isa(x, Conditional) || + isa(x, PartialStruct) || + isa(x, PartialOpaque) +end + # In theory, there could be a `cache` containing a matching `InferenceResult` # for the provided `linfo` and `given_argtypes`. The purpose of this function is # to return a valid value for `cache_lookup(linfo, argtypes, cache).argtypes`, # so that we can construct cache-correct `InferenceResult`s in the first place. -function matching_cache_argtypes(linfo::MethodInstance, given_argtypes::Vector) - @assert isa(linfo.def, Method) # ensure the next line works - nargs::Int = linfo.def.nargs - @assert length(given_argtypes) >= (nargs - 1) - given_argtypes = anymap(widenconditional, given_argtypes) - if linfo.def.isva +function matching_cache_argtypes( + linfo::MethodInstance, (arginfo, sv)#=::Tuple{ArgInfo,InferenceState}=#) + (; fargs, argtypes) = arginfo + def = linfo.def + @assert isa(def, Method) # ensure the next line works + nargs::Int = def.nargs + cache_argtypes, overridden_by_const = matching_cache_argtypes(linfo, nothing) + given_argtypes = Vector{Any}(undef, length(argtypes)) + local condargs = nothing + for i in 1:length(argtypes) + argtype = argtypes[i] + # forward `Conditional` if it conveys a constraint on any other argument + if isa(argtype, Conditional) && fargs !== nothing + cnd = argtype + slotid = find_constrained_arg(cnd, fargs, sv) + if slotid !== nothing + # using union-split signature, we may be able to narrow down `Conditional` + sigt = widenconst(slotid > nargs ? argtypes[slotid] : cache_argtypes[slotid]) + vtype = tmeet(cnd.vtype, sigt) + elsetype = tmeet(cnd.elsetype, sigt) + if vtype === Bottom && elsetype === Bottom + # we accidentally proved this method match is impossible + # TODO bail out here immediately rather than just propagating Bottom ? + given_argtypes[i] = Bottom + else + if condargs === nothing + condargs = Tuple{Int,Int}[] + end + push!(condargs, (slotid, i)) + given_argtypes[i] = Conditional(SlotNumber(slotid), vtype, elsetype) + end + continue + end + end + given_argtypes[i] = widenconditional(argtype) + end + isva = def.isva + if isva || isvarargtype(given_argtypes[end]) isva_given_argtypes = Vector{Any}(undef, nargs) - for i = 1:(nargs - 1) + for i = 1:(nargs - isva) isva_given_argtypes[i] = argtype_by_index(given_argtypes, i) end - if length(given_argtypes) >= nargs || !isvarargtype(given_argtypes[end]) - isva_given_argtypes[nargs] = tuple_tfunc(given_argtypes[nargs:end]) - else - isva_given_argtypes[nargs] = tuple_tfunc(given_argtypes[end:end]) + if isva + if length(given_argtypes) < nargs && isvarargtype(given_argtypes[end]) + last = length(given_argtypes) + else + last = nargs + end + isva_given_argtypes[nargs] = tuple_tfunc(given_argtypes[last:end]) + # invalidate `Conditional` imposed on varargs + if condargs !== nothing + for (slotid, i) in condargs + if slotid ≥ last + isva_given_argtypes[i] = widenconditional(isva_given_argtypes[i]) + end + end + end end given_argtypes = isva_given_argtypes end - cache_argtypes, overridden_by_const = matching_cache_argtypes(linfo, nothing) - if nargs === length(given_argtypes) - for i in 1:nargs - given_argtype = given_argtypes[i] - cache_argtype = cache_argtypes[i] - if !is_argtype_match(given_argtype, cache_argtype, overridden_by_const[i]) - # prefer the argtype we were given over the one computed from `linfo` - cache_argtypes[i] = given_argtype - overridden_by_const[i] = true - end + @assert length(given_argtypes) == nargs + for i in 1:nargs + given_argtype = given_argtypes[i] + cache_argtype = cache_argtypes[i] + if !is_argtype_match(given_argtype, cache_argtype, false) + # prefer the argtype we were given over the one computed from `linfo` + cache_argtypes[i] = given_argtype + overridden_by_const[i] = true end end return cache_argtypes, overridden_by_const end -function matching_cache_argtypes(linfo::MethodInstance, ::Nothing) - toplevel = !isa(linfo.def, Method) - linfo_argtypes = Any[unwrap_unionall(linfo.specTypes).parameters...] - nargs::Int = toplevel ? 0 : linfo.def.nargs +function most_general_argtypes(method::Union{Method, Nothing}, @nospecialize(specTypes), + withfirst::Bool = true) + toplevel = method === nothing + isva = !toplevel && method.isva + linfo_argtypes = Any[(unwrap_unionall(specTypes)::DataType).parameters...] + nargs::Int = toplevel ? 0 : method.nargs + if !withfirst + # For opaque closure, the closure environment is processed elsewhere + nargs -= 1 + end cache_argtypes = Vector{Any}(undef, nargs) # First, if we're dealing with a varargs method, then we set the last element of `args` # to the appropriate `Tuple` type or `PartialStruct` instance. - if !toplevel && linfo.def.isva - if linfo.specTypes == Tuple + if !toplevel && isva + if specTypes::Type == Tuple if nargs > 1 - linfo_argtypes = svec(Any[Any for i = 1:(nargs - 1)]..., Tuple.parameters[1]) + linfo_argtypes = Any[Any for i = 1:nargs] + linfo_argtypes[end] = Vararg{Any} end vargtype = Tuple else @@ -65,18 +118,17 @@ function matching_cache_argtypes(linfo::MethodInstance, ::Nothing) if nargs > linfo_argtypes_length va = linfo_argtypes[linfo_argtypes_length] if isvarargtype(va) - new_va = rewrap_unionall(unconstrain_vararg_length(va), linfo.specTypes) - vargtype_elements = Any[new_va] + new_va = rewrap_unionall(unconstrain_vararg_length(va), specTypes) vargtype = Tuple{new_va} else - vargtype_elements = Any[] vargtype = Tuple{} end else vargtype_elements = Any[] - for p in linfo_argtypes[nargs:linfo_argtypes_length] - p = isvarargtype(p) ? unconstrain_vararg_length(p) : p - push!(vargtype_elements, rewrap(p, linfo.specTypes)) + for i in nargs:linfo_argtypes_length + p = linfo_argtypes[i] + p = unwraptv(isvarargtype(p) ? unconstrain_vararg_length(p) : p) + push!(vargtype_elements, elim_free_typevars(rewrap_unionall(p, specTypes))) end for i in 1:length(vargtype_elements) atyp = vargtype_elements[i] @@ -108,16 +160,14 @@ function matching_cache_argtypes(linfo::MethodInstance, ::Nothing) atyp = unwrapva(atyp) tail_index -= 1 end - while isa(atyp, TypeVar) - atyp = atyp.ub - end + atyp = unwraptv(atyp) if isa(atyp, DataType) && isdefined(atyp, :instance) # replace singleton types with their equivalent Const object atyp = Const(atyp.instance) elseif isconstType(atyp) atyp = Const(atyp.parameters[1]) else - atyp = rewrap(atyp, linfo.specTypes) + atyp = elim_free_typevars(rewrap_unionall(atyp, specTypes)) end i == n && (lastatype = atyp) cache_argtypes[i] = atyp @@ -128,6 +178,25 @@ function matching_cache_argtypes(linfo::MethodInstance, ::Nothing) else @assert nargs == 0 "invalid specialization of method" # wrong number of arguments end + cache_argtypes +end + +# eliminate free `TypeVar`s in order to make the life much easier down the road: +# at runtime only `Type{...}::DataType` can contain invalid type parameters, and other +# malformed types here are user-constructed type arguments given at an inference entry +# so this function will replace only the malformed `Type{...}::DataType` with `Type` +# and simply replace other possibilities with `Any` +function elim_free_typevars(@nospecialize t) + if has_free_typevars(t) + return isType(t) ? Type : Any + else + return t + end +end + +function matching_cache_argtypes(linfo::MethodInstance, ::Nothing) + mthd = isa(linfo.def, Method) ? linfo.def::Method : nothing + cache_argtypes = most_general_argtypes(mthd, linfo.specTypes) return cache_argtypes, falses(length(cache_argtypes)) end diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index 56b766592787ea..24423deef86239 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -1,135 +1,344 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -const LineNum = Int +# The type of a variable load is either a value or an UndefVarError +# (only used in abstractinterpret, doesn't appear in optimize) +struct VarState + typ + undef::Bool + VarState(@nospecialize(typ), undef::Bool) = new(typ, undef) +end + +""" + const VarTable = Vector{VarState} + +The extended lattice that maps local variables to inferred type represented as `AbstractLattice`. +Each index corresponds to the `id` of `SlotNumber` which identifies each local variable. +Note that `InferenceState` will maintain multiple `VarTable`s at each SSA statement +to enable flow-sensitive analysis. +""" +const VarTable = Vector{VarState} + +mutable struct BitSetBoundedMinPrioritySet <: AbstractSet{Int} + elems::BitSet + min::Int + # Stores whether min is exact or a lower bound + # If exact, it is not set in elems + min_exact::Bool + max::Int +end + +function BitSetBoundedMinPrioritySet(max::Int) + bs = BitSet() + bs.offset = 0 + BitSetBoundedMinPrioritySet(bs, max+1, true, max) +end + +@noinline function _advance_bsbmp!(bsbmp::BitSetBoundedMinPrioritySet) + @assert !bsbmp.min_exact + bsbmp.min = _bits_findnext(bsbmp.elems.bits, bsbmp.min)::Int + bsbmp.min < 0 && (bsbmp.min = bsbmp.max + 1) + bsbmp.min_exact = true + delete!(bsbmp.elems, bsbmp.min) + return nothing +end + +function isempty(bsbmp::BitSetBoundedMinPrioritySet) + if bsbmp.min > bsbmp.max + return true + end + bsbmp.min_exact && return false + _advance_bsbmp!(bsbmp) + return bsbmp.min > bsbmp.max +end + +function popfirst!(bsbmp::BitSetBoundedMinPrioritySet) + bsbmp.min_exact || _advance_bsbmp!(bsbmp) + m = bsbmp.min + m > bsbmp.max && throw(ArgumentError("BitSetBoundedMinPrioritySet must be non-empty")) + bsbmp.min = m+1 + bsbmp.min_exact = false + return m +end + +function push!(bsbmp::BitSetBoundedMinPrioritySet, idx::Int) + if idx <= bsbmp.min + if bsbmp.min_exact && bsbmp.min < bsbmp.max && idx != bsbmp.min + push!(bsbmp.elems, bsbmp.min) + end + bsbmp.min = idx + bsbmp.min_exact = true + return nothing + end + push!(bsbmp.elems, idx) + return nothing +end + +function in(idx::Int, bsbmp::BitSetBoundedMinPrioritySet) + if bsbmp.min_exact && idx == bsbmp.min + return true + end + return idx in bsbmp.elems +end mutable struct InferenceState - params::InferenceParams - result::InferenceResult # remember where to put the result + #= information about this method instance =# linfo::MethodInstance - sptypes::Vector{Any} # types of static parameter - slottypes::Vector{Any} + world::UInt mod::Module - currpc::LineNum - - # info on the state of inference and the linfo + sptypes::Vector{Any} + slottypes::Vector{Any} src::CodeInfo - world::UInt - valid_worlds::WorldRange - nargs::Int - stmt_types::Vector{Any} - stmt_edges::Vector{Any} + + #= intermediate states for local abstract interpretation =# + currpc::Int + ip::BitSetBoundedMinPrioritySet # current active instruction pointers + handler_at::Vector{Int} # current exception handler info + ssavalue_uses::Vector{BitSet} # ssavalue sparsity and restart info + stmt_types::Vector{Union{Nothing, VarTable}} + stmt_edges::Vector{Union{Nothing, Vector{Any}}} stmt_info::Vector{Any} - # return type - bestguess #::Type - # current active instruction pointers - ip::BitSet - pc´´::LineNum - nstmts::Int - # current exception handler info - cur_hand #::Union{Nothing, Pair{LineNum, prev_handler}} - handler_at::Vector{Any} - n_handlers::Int - # ssavalue sparsity and restart info - ssavalue_uses::Vector{BitSet} - throw_blocks::BitSet - - cycle_backedges::Vector{Tuple{InferenceState, LineNum}} # call-graph backedges connecting from callee to caller + + #= interprocedural intermediate states for abstract interpretation =# + pclimitations::IdSet{InferenceState} # causes of precision restrictions (LimitedAccuracy) on currpc ssavalue + limitations::IdSet{InferenceState} # causes of precision restrictions (LimitedAccuracy) on return + cycle_backedges::Vector{Tuple{InferenceState, Int}} # call-graph backedges connecting from callee to caller callers_in_cycle::Vector{InferenceState} + dont_work_on_me::Bool parent::Union{Nothing, InferenceState} + inferred::Bool # TODO move this to InferenceResult? - # TODO: move these to InferenceResult / Params? - cached::Bool - limited::Bool - inferred::Bool - dont_work_on_me::Bool + #= results =# + result::InferenceResult # remember where to put the result + valid_worlds::WorldRange + bestguess #::Type + ipo_effects::Effects - # The place to look up methods while working on this function. - # In particular, we cache method lookup results for the same function to - # fast path repeated queries. - method_table::CachedMethodTable{InternalMethodTable} + #= flags =# + params::InferenceParams + # Whether to restrict inference of abstract call sites to avoid excessive work + # Set by default for toplevel frame. + restrict_abstract_call_sites::Bool + cached::Bool # TODO move this to InferenceResult? # The interpreter that created this inference state. Not looked at by # NativeInterpreter. But other interpreters may use this to detect cycles interp::AbstractInterpreter # src is assumed to be a newly-allocated CodeInfo, that can be modified in-place to contain intermediate results - function InferenceState(result::InferenceResult, src::CodeInfo, - cached::Bool, interp::AbstractInterpreter) + function InferenceState(result::InferenceResult, + src::CodeInfo, cache::Symbol, interp::AbstractInterpreter) linfo = result.linfo - code = src.code::Array{Any,1} - toplevel = !isa(linfo.def, Method) - - sp = sptypes_from_meth_instance(linfo::MethodInstance) + world = get_world_counter(interp) + def = linfo.def + mod = isa(def, Method) ? def.module : def + sptypes = sptypes_from_meth_instance(linfo) + code = src.code::Vector{Any} + nstmts = length(code) + currpc = 1 + ip = BitSetBoundedMinPrioritySet(nstmts) + handler_at = compute_trycatch(code, ip.elems) + push!(ip, 1) nssavalues = src.ssavaluetypes::Int - src.ssavaluetypes = Any[ NOT_FOUND for i = 1:nssavalues ] - stmt_info = Any[ nothing for i = 1:length(code) ] - - n = length(code) - s_edges = Any[ nothing for i = 1:n ] - s_types = Any[ nothing for i = 1:n ] + ssavalue_uses = find_ssavalue_uses(code, nssavalues) + stmt_types = Union{Nothing, VarTable}[ nothing for i = 1:nstmts ] + stmt_edges = Union{Nothing, Vector{Any}}[ nothing for i = 1:nstmts ] + stmt_info = Any[ nothing for i = 1:nstmts ] - # initial types nslots = length(src.slotflags) + slottypes = Vector{Any}(undef, nslots) argtypes = result.argtypes nargs = length(argtypes) - s_argtypes = VarTable(undef, nslots) - slottypes = Vector{Any}(undef, nslots) + stmt_types[1] = stmt_type1 = VarTable(undef, nslots) for i in 1:nslots - at = (i > nargs) ? Bottom : argtypes[i] - s_argtypes[i] = VarState(at, i > nargs) - slottypes[i] = at + argtyp = (i > nargs) ? Bottom : argtypes[i] + stmt_type1[i] = VarState(argtyp, i > nargs) + slottypes[i] = argtyp end - s_types[1] = s_argtypes - - ssavalue_uses = find_ssavalue_uses(code, nssavalues) - throw_blocks = find_throw_blocks(code) - # exception handlers - cur_hand = nothing - handler_at = Any[ nothing for i=1:n ] - n_handlers = 0 + pclimitations = IdSet{InferenceState}() + limitations = IdSet{InferenceState}() + cycle_backedges = Vector{Tuple{InferenceState,Int}}() + callers_in_cycle = Vector{InferenceState}() + dont_work_on_me = false + parent = nothing + inferred = false - W = BitSet() - push!(W, 1) #initial pc to visit + valid_worlds = WorldRange(src.min_world, src.max_world == typemax(UInt) ? get_world_counter() : src.max_world) + bestguess = Bottom + # TODO: Currently, any :inbounds declaration taints consistency, + # because we cannot be guaranteed whether or not boundschecks + # will be eliminated and if they are, we cannot be guaranteed + # that no undefined behavior will occur (the effects assumptions + # are stronger than the inbounds assumptions, since the latter + # requires dynamic reachability, while the former is global). + inbounds = inbounds_option() + inbounds_taints_consistency = !(inbounds === :on || (inbounds === :default && !any_inbounds(code))) + consistent = inbounds_taints_consistency ? TRISTATE_UNKNOWN : ALWAYS_TRUE + ipo_effects = Effects(EFFECTS_TOTAL; consistent, inbounds_taints_consistency) - if !toplevel - meth = linfo.def - inmodule = meth.module - else - inmodule = linfo.def::Module - end + params = InferenceParams(interp) + restrict_abstract_call_sites = isa(linfo.def, Module) + @assert cache === :no || cache === :local || cache === :global + cached = cache === :global - valid_worlds = WorldRange(src.min_world, - src.max_world == typemax(UInt) ? get_world_counter() : src.max_world) frame = new( - InferenceParams(interp), result, linfo, - sp, slottypes, inmodule, 0, - src, get_world_counter(interp), valid_worlds, - nargs, s_types, s_edges, stmt_info, - Union{}, W, 1, n, - cur_hand, handler_at, n_handlers, - ssavalue_uses, throw_blocks, - Vector{Tuple{InferenceState,LineNum}}(), # cycle_backedges - Vector{InferenceState}(), # callers_in_cycle - #=parent=#nothing, - cached, false, false, false, - CachedMethodTable(method_table(interp)), + linfo, world, mod, sptypes, slottypes, src, + currpc, ip, handler_at, ssavalue_uses, stmt_types, stmt_edges, stmt_info, + pclimitations, limitations, cycle_backedges, callers_in_cycle, dont_work_on_me, parent, inferred, + result, valid_worlds, bestguess, ipo_effects, + params, restrict_abstract_call_sites, cached, interp) + + # some more setups + src.ssavaluetypes = Any[ NOT_FOUND for i = 1:nssavalues ] + params.unoptimize_throw_blocks && mark_throw_blocks!(src, handler_at) result.result = frame - cached && push!(get_inference_cache(interp), result) + cache !== :no && push!(get_inference_cache(interp), result) + return frame end end -method_table(interp::AbstractInterpreter, sv::InferenceState) = sv.method_table +Effects(state::InferenceState) = state.ipo_effects + +function tristate_merge!(caller::InferenceState, effects::Effects) + caller.ipo_effects = tristate_merge(caller.ipo_effects, effects) +end +tristate_merge!(caller::InferenceState, callee::InferenceState) = + tristate_merge!(caller, Effects(callee)) + +is_effect_overridden(sv::InferenceState, effect::Symbol) = is_effect_overridden(sv.linfo, effect) +function is_effect_overridden(linfo::MethodInstance, effect::Symbol) + def = linfo.def + return isa(def, Method) && is_effect_overridden(def, effect) +end +is_effect_overridden(method::Method, effect::Symbol) = is_effect_overridden(decode_effects_override(method.purity), effect) +is_effect_overridden(override::EffectsOverride, effect::Symbol) = getfield(override, effect) + +function any_inbounds(code::Vector{Any}) + for i=1:length(code) + stmt = code[i] + if isa(stmt, Expr) && stmt.head === :inbounds + return true + end + end + return false +end + +function compute_trycatch(code::Vector{Any}, ip::BitSet) + # The goal initially is to record the frame like this for the state at exit: + # 1: (enter 3) # == 0 + # 3: (expr) # == 1 + # 3: (leave 1) # == 1 + # 4: (expr) # == 0 + # then we can find all trys by walking backwards from :enter statements, + # and all catches by looking at the statement after the :enter + n = length(code) + empty!(ip) + ip.offset = 0 # for _bits_findnext + push!(ip, n + 1) + handler_at = fill(0, n) + + # start from all :enter statements and record the location of the try + for pc = 1:n + stmt = code[pc] + if isexpr(stmt, :enter) + l = stmt.args[1]::Int + handler_at[pc + 1] = pc + push!(ip, pc + 1) + handler_at[l] = pc + push!(ip, l) + end + end + + # now forward those marks to all :leave statements + pc´´ = 0 + while true + # make progress on the active ip set + pc = _bits_findnext(ip.bits, pc´´)::Int + pc > n && break + while true # inner loop optimizes the common case where it can run straight from pc to pc + 1 + pc´ = pc + 1 # next program-counter (after executing instruction) + if pc == pc´´ + pc´´ = pc´ + end + delete!(ip, pc) + cur_hand = handler_at[pc] + @assert cur_hand != 0 "unbalanced try/catch" + stmt = code[pc] + if isa(stmt, GotoNode) + pc´ = stmt.label + elseif isa(stmt, GotoIfNot) + l = stmt.dest::Int + if handler_at[l] != cur_hand + @assert handler_at[l] == 0 "unbalanced try/catch" + handler_at[l] = cur_hand + if l < pc´´ + pc´´ = l + end + push!(ip, l) + end + elseif isa(stmt, ReturnNode) + @assert !isdefined(stmt, :val) "unbalanced try/catch" + break + elseif isa(stmt, Expr) + head = stmt.head + if head === :enter + cur_hand = pc + elseif head === :leave + l = stmt.args[1]::Int + for i = 1:l + cur_hand = handler_at[cur_hand] + end + cur_hand == 0 && break + end + end -function InferenceState(result::InferenceResult, cached::Bool, interp::AbstractInterpreter) + pc´ > n && break # can't proceed with the fast-path fall-through + if handler_at[pc´] != cur_hand + @assert handler_at[pc´] == 0 "unbalanced try/catch" + handler_at[pc´] = cur_hand + elseif !in(pc´, ip) + break # already visited + end + pc = pc´ + end + end + + @assert first(ip) == n + 1 + return handler_at +end + +""" + Iterate through all callers of the given InferenceState in the abstract + interpretation stack (including the given InferenceState itself), vising + children before their parents (i.e. ascending the tree from the given + InferenceState). Note that cycles may be visited in any order. +""" +struct InfStackUnwind + inf::InferenceState +end +iterate(unw::InfStackUnwind) = (unw.inf, (unw.inf, 0)) +function iterate(unw::InfStackUnwind, (infstate, cyclei)::Tuple{InferenceState, Int}) + # iterate through the cycle before walking to the parent + if cyclei < length(infstate.callers_in_cycle) + cyclei += 1 + infstate = infstate.callers_in_cycle[cyclei] + else + cyclei = 0 + infstate = infstate.parent + end + infstate === nothing && return nothing + (infstate::InferenceState, (infstate, cyclei)) +end + +function InferenceState(result::InferenceResult, cache::Symbol, interp::AbstractInterpreter) # prepare an InferenceState object for inferring lambda src = retrieve_code_info(result.linfo) src === nothing && return nothing validate_code_in_debug_mode(result.linfo, src, "lowered") - return InferenceState(result, src, cached, interp) + return InferenceState(result, src, cache, interp) end function sptypes_from_meth_instance(linfo::MethodInstance) @@ -160,7 +369,7 @@ function sptypes_from_meth_instance(linfo::MethodInstance) while temp isa UnionAll temp = temp.body end - sigtypes = temp.parameters + sigtypes = (temp::DataType).parameters for j = 1:length(sigtypes) tj = sigtypes[j] if isType(tj) && tj.parameters[1] === Pi @@ -192,6 +401,8 @@ function sptypes_from_meth_instance(linfo::MethodInstance) ty = UnionAll(tv, Type{tv}) end end + elseif isvarargtype(v) + ty = Int else ty = Const(v) end @@ -212,19 +423,17 @@ end update_valid_age!(edge::InferenceState, sv::InferenceState) = update_valid_age!(sv, edge.valid_worlds) function record_ssa_assign(ssa_id::Int, @nospecialize(new), frame::InferenceState) - old = frame.src.ssavaluetypes[ssa_id] + ssavaluetypes = frame.src.ssavaluetypes::Vector{Any} + old = ssavaluetypes[ssa_id] if old === NOT_FOUND || !(new ⊑ old) # typically, we expect that old ⊑ new (that output information only # gets less precise with worse input information), but to actually # guarantee convergence we need to use tmerge here to ensure that is true - frame.src.ssavaluetypes[ssa_id] = old === NOT_FOUND ? new : tmerge(old, new) + ssavaluetypes[ssa_id] = old === NOT_FOUND ? new : tmerge(old, new) W = frame.ip s = frame.stmt_types for r in frame.ssavalue_uses[ssa_id] if s[r] !== nothing # s[r] === nothing => unreached statement - if r < frame.pc´´ - frame.pc´´ = r - end push!(W, r) end end @@ -243,61 +452,37 @@ end # temporarily accumulate our edges to later add as backedges in the callee function add_backedge!(li::MethodInstance, caller::InferenceState) isa(caller.linfo.def, Method) || return # don't add backedges to toplevel exprs - if caller.stmt_edges[caller.currpc] === nothing - caller.stmt_edges[caller.currpc] = [] + edges = caller.stmt_edges[caller.currpc] + if edges === nothing + edges = caller.stmt_edges[caller.currpc] = [] end - push!(caller.stmt_edges[caller.currpc], li) + push!(edges, li) nothing end # used to temporarily accumulate our no method errors to later add as backedges in the callee method table function add_mt_backedge!(mt::Core.MethodTable, @nospecialize(typ), caller::InferenceState) isa(caller.linfo.def, Method) || return # don't add backedges to toplevel exprs - if caller.stmt_edges[caller.currpc] === nothing - caller.stmt_edges[caller.currpc] = [] + edges = caller.stmt_edges[caller.currpc] + if edges === nothing + edges = caller.stmt_edges[caller.currpc] = [] end - push!(caller.stmt_edges[caller.currpc], mt) - push!(caller.stmt_edges[caller.currpc], typ) + push!(edges, mt) + push!(edges, typ) nothing end -function poison_callstack(infstate::InferenceState, topmost::InferenceState, poison_topmost::Bool) - poison_topmost && (topmost = topmost.parent) - while !(infstate === topmost) - if call_result_unused(infstate) - # If we won't propagate the result any further (since it's typically unused), - # it's OK that we keep and cache the "limited" result in the parents - # (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases) - # TODO: we might be able to halt progress much more strongly here, - # since now we know we won't be able to keep anything much that we learned. - # We were mainly only here to compute the calling convention return type, - # but in most situations now, we are unlikely to be able to use that information. - break - end - infstate.limited = true - for infstate_cycle in infstate.callers_in_cycle - infstate_cycle.limited = true - end - infstate = infstate.parent - infstate === nothing && return - end -end - -function is_specializable_vararg_slot(@nospecialize(arg), nargs::Int, vargs::Vector{Any}) - return (isa(arg, Slot) && slot_id(arg) == nargs && !isempty(vargs)) -end - function print_callstack(sv::InferenceState) while sv !== nothing print(sv.linfo) - sv.limited && print(" [limited]") !sv.cached && print(" [uncached]") println() for cycle in sv.callers_in_cycle print(' ', cycle.linfo) - cycle.limited && print(" [limited]") println() end sv = sv.parent end end + +get_curr_ssaflag(sv::InferenceState) = sv.src.ssaflags[sv.currpc] diff --git a/base/compiler/methodtable.jl b/base/compiler/methodtable.jl index 76c5ca4189dff2..7aa686009c1af5 100644 --- a/base/compiler/methodtable.jl +++ b/base/compiler/methodtable.jl @@ -1,5 +1,28 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + abstract type MethodTableView; end +""" + struct InternalMethodTable <: MethodTableView + +A struct representing the state of the internal method table at a +particular world age. +""" +struct InternalMethodTable <: MethodTableView + world::UInt +end + +""" + struct OverlayMethodTable <: MethodTableView + +Overlays the internal method table such that specific queries can be redirected to an +external table, e.g., to override existing method. +""" +struct OverlayMethodTable <: MethodTableView + world::UInt + mt::Core.MethodTable +end + struct MethodLookupResult # Really Vector{Core.MethodMatch}, but it's easier to represent this as # and work with Vector{Any} on the C side. @@ -17,77 +40,95 @@ end getindex(result::MethodLookupResult, idx::Int) = getindex(result.matches, idx)::MethodMatch """ - struct InternalMethodTable <: MethodTableView + findall(sig::Type, view::MethodTableView; limit::Int=typemax(Int)) -> + (matches::MethodLookupResult, overlayed::Bool) or missing -A struct representing the state of the internal method table at a -particular world age. +Find all methods in the given method table `view` that are applicable to the given signature `sig`. +If no applicable methods are found, an empty result is returned. +If the number of applicable methods exceeded the specified limit, `missing` is returned. +`overlayed` indicates if any of the matching methods comes from an overlayed method table. """ -struct InternalMethodTable <: MethodTableView - world::UInt +function findall(@nospecialize(sig::Type), table::InternalMethodTable; limit::Int=Int(typemax(Int32))) + result = _findall(sig, nothing, table.world, limit) + result === missing && return missing + return result, false end -""" - struct CachedMethodTable <: MethodTableView - -Overlays another method table view with an additional local fast path cache that -can respond to repeated, identical queries faster than the original method table. -""" -struct CachedMethodTable{T} <: MethodTableView - cache::IdDict{Any, Union{Missing, MethodLookupResult}} - table::T +function findall(@nospecialize(sig::Type), table::OverlayMethodTable; limit::Int=Int(typemax(Int32))) + result = _findall(sig, table.mt, table.world, limit) + result === missing && return missing + nr = length(result) + if nr ≥ 1 && result[nr].fully_covers + # no need to fall back to the internal method table + return result, true + end + # fall back to the internal method table + fallback_result = _findall(sig, nothing, table.world, limit) + fallback_result === missing && return missing + # merge the fallback match results with the internal method table + return MethodLookupResult( + vcat(result.matches, fallback_result.matches), + WorldRange( + max(result.valid_worlds.min_world, fallback_result.valid_worlds.min_world), + min(result.valid_worlds.max_world, fallback_result.valid_worlds.max_world)), + result.ambig | fallback_result.ambig), !isempty(result) end -CachedMethodTable(table::T) where T = - CachedMethodTable{T}(IdDict{Any, Union{Missing, MethodLookupResult}}(), - table) - -""" - findall(sig::Type{<:Tuple}, view::MethodTableView; limit=typemax(Int)) -Find all methods in the given method table `view` that are applicable to the -given signature `sig`. If no applicable methods are found, an empty result is -returned. If the number of applicable methods exeeded the specified limit, -`missing` is returned. -""" -function findall(@nospecialize(sig::Type{<:Tuple}), table::InternalMethodTable; limit::Int=typemax(Int)) +function _findall(@nospecialize(sig::Type), mt::Union{Nothing,Core.MethodTable}, world::UInt, limit::Int) _min_val = RefValue{UInt}(typemin(UInt)) _max_val = RefValue{UInt}(typemax(UInt)) _ambig = RefValue{Int32}(0) - ms = _methods_by_ftype(sig, limit, table.world, false, _min_val, _max_val, _ambig) + ms = _methods_by_ftype(sig, mt, limit, world, false, _min_val, _max_val, _ambig) if ms === false return missing end return MethodLookupResult(ms::Vector{Any}, WorldRange(_min_val[], _max_val[]), _ambig[] != 0) end -function findall(@nospecialize(sig::Type{<:Tuple}), table::CachedMethodTable; limit::Int=typemax(Int)) - box = Core.Box(sig) - return get!(table.cache, sig) do - findall(box.contents, table.table; limit=limit) - end -end - """ - findsup(sig::Type{<:Tuple}, view::MethodTableView)::Union{Tuple{MethodMatch, WorldRange}, Nothing} - -Find the (unique) method `m` such that `sig <: m.sig`, while being more -specific than any other method with the same property. In other words, find -the method which is the least upper bound (supremum) under the specificity/subtype -relation of the queried `signature`. If `sig` is concrete, this is equivalent to -asking for the method that will be called given arguments whose types match the -given signature. This query is also used to implement `invoke`. - -Such a method `m` need not exist. It is possible that no method is an -upper bound of `sig`, or it is possible that among the upper bounds, there -is no least element. In both cases `nothing` is returned. + findsup(sig::Type, view::MethodTableView) -> + (match::MethodMatch, valid_worlds::WorldRange, overlayed::Bool) or nothing + +Find the (unique) method such that `sig <: match.method.sig`, while being more +specific than any other method with the same property. In other words, find the method +which is the least upper bound (supremum) under the specificity/subtype relation of +the queried `sig`nature. If `sig` is concrete, this is equivalent to asking for the method +that will be called given arguments whose types match the given signature. +Note that this query is also used to implement `invoke`. + +Such a matching method `match` doesn't necessarily exist. +It is possible that no method is an upper bound of `sig`, or +it is possible that among the upper bounds, there is no least element. +In both cases `nothing` is returned. + +`overlayed` indicates if any of the matching methods comes from an overlayed method table. """ -function findsup(@nospecialize(sig::Type{<:Tuple}), table::InternalMethodTable) +function findsup(@nospecialize(sig::Type), table::InternalMethodTable) + return (_findsup(sig, nothing, table.world)..., false) +end + +function findsup(@nospecialize(sig::Type), table::OverlayMethodTable) + match, valid_worlds = _findsup(sig, table.mt, table.world) + match !== nothing && return match, valid_worlds, true + # fall back to the internal method table + fallback_match, fallback_valid_worlds = _findsup(sig, nothing, table.world) + return ( + fallback_match, + WorldRange( + max(valid_worlds.min_world, fallback_valid_worlds.min_world), + min(valid_worlds.max_world, fallback_valid_worlds.max_world)), + false) +end + +function _findsup(@nospecialize(sig::Type), mt::Union{Nothing,Core.MethodTable}, world::UInt) min_valid = RefValue{UInt}(typemin(UInt)) max_valid = RefValue{UInt}(typemax(UInt)) - result = ccall(:jl_gf_invoke_lookup_worlds, Any, (Any, UInt, Ptr{Csize_t}, Ptr{Csize_t}), - sig, table.world, min_valid, max_valid)::Union{Method, Nothing} - result === nothing && return nothing - (result, WorldRange(min_valid[], max_valid[])) + match = ccall(:jl_gf_invoke_lookup_worlds, Any, (Any, Any, UInt, Ptr{Csize_t}, Ptr{Csize_t}), + sig, mt, world, min_valid, max_valid)::Union{MethodMatch, Nothing} + valid_worlds = WorldRange(min_valid[], max_valid[]) + return match, valid_worlds end -# This query is not cached -findsup(sig::Type{<:Tuple}, table::CachedMethodTable) = findsup(sig, table.table) +isoverlayed(::MethodTableView) = error("unsatisfied MethodTableView interface") +isoverlayed(::InternalMethodTable) = false +isoverlayed(::OverlayMethodTable) = true diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index d53b8193e639ad..b00e24aec97348 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -1,5 +1,35 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license +############# +# constants # +############# + +# The slot has uses that are not statically dominated by any assignment +# This is implied by `SLOT_USEDUNDEF`. +# If this is not set, all the uses are (statically) dominated by the defs. +# In particular, if a slot has `AssignedOnce && !StaticUndef`, it is an SSA. +const SLOT_STATICUNDEF = 1 # slot might be used before it is defined (structurally) +const SLOT_ASSIGNEDONCE = 16 # slot is assigned to only once +const SLOT_USEDUNDEF = 32 # slot has uses that might raise UndefVarError +# const SLOT_CALLED = 64 + +# NOTE make sure to sync the flag definitions below with julia.h and `jl_code_info_set_ir` in method.c + +const IR_FLAG_NULL = 0x00 +# This statement is marked as @inbounds by user. +# Ff replaced by inlining, any contained boundschecks may be removed. +const IR_FLAG_INBOUNDS = 0x01 << 0 +# This statement is marked as @inline by user +const IR_FLAG_INLINE = 0x01 << 1 +# This statement is marked as @noinline by user +const IR_FLAG_NOINLINE = 0x01 << 2 +const IR_FLAG_THROW_BLOCK = 0x01 << 3 +# This statement may be removed if its result is unused. In particular it must +# thus be both pure and effect free. +const IR_FLAG_EFFECT_FREE = 0x01 << 4 + +const TOP_TUPLE = GlobalRef(Core, :tuple) + ##################### # OptimizationState # ##################### @@ -21,45 +51,60 @@ function push!(et::EdgeTracker, ci::CodeInstance) push!(et, ci.def) end -struct InferenceCaches{T, S} - inf_cache::T - mi_cache::S -end - -struct InliningState{S <: Union{EdgeTracker, Nothing}, T <: Union{InferenceCaches, Nothing}, V <: Union{Nothing, MethodTableView}} +struct InliningState{S <: Union{EdgeTracker, Nothing}, MICache, I<:AbstractInterpreter} params::OptimizationParams et::S - caches::T - method_table::V + mi_cache::MICache # TODO move this to `OptimizationState` (as used by EscapeAnalysis as well) + interp::I end +is_source_inferred(@nospecialize(src::Union{CodeInfo, Vector{UInt8}})) = + ccall(:jl_ir_flag_inferred, Bool, (Any,), src) + +function inlining_policy(interp::AbstractInterpreter, @nospecialize(src), stmt_flag::UInt8, + mi::MethodInstance, argtypes::Vector{Any}) + if isa(src, CodeInfo) || isa(src, Vector{UInt8}) + src_inferred = is_source_inferred(src) + src_inlineable = is_stmt_inline(stmt_flag) || ccall(:jl_ir_flag_inlineable, Bool, (Any,), src) + return src_inferred && src_inlineable ? src : nothing + elseif src === nothing && is_stmt_inline(stmt_flag) + # if this statement is forced to be inlined, make an additional effort to find the + # inferred source in the local cache + # we still won't find a source for recursive call because the "single-level" inlining + # seems to be more trouble and complex than it's worth + inf_result = cache_lookup(mi, argtypes, get_inference_cache(interp)) + inf_result === nothing && return nothing + src = inf_result.src + if isa(src, CodeInfo) + src_inferred = is_source_inferred(src) + return src_inferred ? src : nothing + else + return nothing + end + end + return nothing +end + +include("compiler/ssair/driver.jl") + mutable struct OptimizationState linfo::MethodInstance src::CodeInfo + ir::Union{Nothing, IRCode} stmt_info::Vector{Any} mod::Module - nargs::Int sptypes::Vector{Any} # static parameters slottypes::Vector{Any} - const_api::Bool inlining::InliningState function OptimizationState(frame::InferenceState, params::OptimizationParams, interp::AbstractInterpreter) - s_edges = frame.stmt_edges[1] - if s_edges === nothing - s_edges = [] - frame.stmt_edges[1] = s_edges - end - src = frame.src + s_edges = frame.stmt_edges[1]::Vector{Any} inlining = InliningState(params, - EdgeTracker(s_edges::Vector{Any}, frame.valid_worlds), - InferenceCaches( - get_inference_cache(interp), - WorldView(code_cache(interp), frame.world)), - method_table(interp)) + EdgeTracker(s_edges, frame.valid_worlds), + WorldView(code_cache(interp), frame.world), + interp) return new(frame.linfo, - src, frame.stmt_info, frame.mod, frame.nargs, - frame.sptypes, frame.slottypes, false, - inlining) + frame.src, nothing, frame.stmt_info, frame.mod, + frame.sptypes, frame.slottypes, inlining) end function OptimizationState(linfo::MethodInstance, src::CodeInfo, params::OptimizationParams, interp::AbstractInterpreter) # prepare src for running optimization passes @@ -67,6 +112,8 @@ mutable struct OptimizationState nssavalues = src.ssavaluetypes if nssavalues isa Int src.ssavaluetypes = Any[ Any for i = 1:nssavalues ] + else + nssavalues = length(src.ssavaluetypes::Vector{Any}) end nslots = length(src.slotflags) slottypes = src.slottypes @@ -75,28 +122,18 @@ mutable struct OptimizationState end stmt_info = Any[nothing for i = 1:nssavalues] # cache some useful state computations - toplevel = !isa(linfo.def, Method) - if !toplevel - meth = linfo.def - inmodule = meth.module - nargs = meth.nargs - else - inmodule = linfo.def::Module - nargs = 0 - end + def = linfo.def + mod = isa(def, Method) ? def.module : def # Allow using the global MI cache, but don't track edges. # This method is mostly used for unit testing the optimizer inlining = InliningState(params, nothing, - InferenceCaches( - get_inference_cache(interp), - WorldView(code_cache(interp), get_world_counter())), - method_table(interp)) + WorldView(code_cache(interp), get_world_counter()), + interp) return new(linfo, - src, stmt_info, inmodule, nargs, - sptypes_from_meth_instance(linfo), slottypes, false, - inlining) - end + src, nothing, stmt_info, mod, + sptypes_from_meth_instance(linfo), slottypes, inlining) + end end function OptimizationState(linfo::MethodInstance, params::OptimizationParams, interp::AbstractInterpreter) @@ -105,33 +142,17 @@ function OptimizationState(linfo::MethodInstance, params::OptimizationParams, in return OptimizationState(linfo, src, params, interp) end - -############# -# constants # -############# - -# The slot has uses that are not statically dominated by any assignment -# This is implied by `SLOT_USEDUNDEF`. -# If this is not set, all the uses are (statically) dominated by the defs. -# In particular, if a slot has `AssignedOnce && !StaticUndef`, it is an SSA. -const SLOT_STATICUNDEF = 1 # slot might be used before it is defined (structurally) -const SLOT_ASSIGNEDONCE = 16 # slot is assigned to only once -const SLOT_USEDUNDEF = 32 # slot has uses that might raise UndefVarError -# const SLOT_CALLED = 64 - -const IR_FLAG_INBOUNDS = 0x01 - -# known to be always effect-free (in particular nothrow) -const _PURE_BUILTINS = Any[tuple, svec, ===, typeof, nfields] - -# known to be effect-free if the are nothrow -const _PURE_OR_ERROR_BUILTINS = [ - fieldtype, apply_type, isa, UnionAll, - getfield, arrayref, const_arrayref, isdefined, Core.sizeof, - Core.kwfunc, ifelse, Core._typevar, (<:) -] - -const TOP_TUPLE = GlobalRef(Core, :tuple) +function ir_to_codeinf!(opt::OptimizationState) + (; linfo, src) = opt + optdef = linfo.def + replace_code_newstyle!(src, opt.ir::IRCode, isa(optdef, Method) ? Int(optdef.nargs) : 0) + opt.ir = nothing + widen_all_consts!(src) + src.inferred = true + # finish updating the result struct + validate_code_in_debug_mode(linfo, src, "optimized") + return src +end ######### # logic # @@ -139,28 +160,9 @@ const TOP_TUPLE = GlobalRef(Core, :tuple) _topmod(sv::OptimizationState) = _topmod(sv.mod) -function isinlineable(m::Method, me::OptimizationState, params::OptimizationParams, union_penalties::Bool, bonus::Int=0) - # compute the cost (size) of inlining this code - inlineable = false - cost_threshold = params.inline_cost_threshold - if m.module === _topmod(m.module) - # a few functions get special treatment - name = m.name - sig = m.sig - if ((name === :+ || name === :* || name === :min || name === :max) && - isa(sig,DataType) && - sig == Tuple{sig.parameters[1],Any,Any,Any,Vararg{Any}}) - inlineable = true - elseif (name === :iterate || name === :unsafe_convert || - name === :cconvert) - cost_threshold *= 4 - end - end - if !inlineable - inlineable = inline_worthy(me.src.code, me.src, me.sptypes, me.slottypes, params, union_penalties, cost_threshold + bonus) - end - return inlineable -end +is_stmt_inline(stmt_flag::UInt8) = stmt_flag & IR_FLAG_INLINE ≠ 0 +is_stmt_noinline(stmt_flag::UInt8) = stmt_flag & IR_FLAG_NOINLINE ≠ 0 +is_stmt_throw_block(stmt_flag::UInt8) = stmt_flag & IR_FLAG_THROW_BLOCK ≠ 0 # These affect control flow within the function (so may not be removed # if there is no usage within the function), but don't affect the purity @@ -170,7 +172,7 @@ function stmt_affects_purity(@nospecialize(stmt), ir) return false end if isa(stmt, GotoIfNot) - t = argextype(stmt.cond, ir, ir.sptypes) + t = argextype(stmt.cond, ir) return !(t ⊑ Bool) end if isa(stmt, Expr) @@ -179,32 +181,248 @@ function stmt_affects_purity(@nospecialize(stmt), ir) return true end -# run the optimization work -function optimize(opt::OptimizationState, params::OptimizationParams, @nospecialize(result)) - def = opt.linfo.def - nargs = Int(opt.nargs) - 1 - @timeit "optimizer" ir = run_passes(opt.src, nargs, opt) - force_noinline = _any(@nospecialize(x) -> isexpr(x, :meta) && x.args[1] === :noinline, ir.meta) +""" + stmt_effect_free(stmt, rt, src::Union{IRCode,IncrementalCompact}) + +Determine whether a `stmt` is "side-effect-free", i.e. may be removed if it has no uses. +""" +function stmt_effect_free(@nospecialize(stmt), @nospecialize(rt), src::Union{IRCode,IncrementalCompact}) + isa(stmt, PiNode) && return true + isa(stmt, PhiNode) && return true + isa(stmt, ReturnNode) && return false + isa(stmt, GotoNode) && return false + isa(stmt, GotoIfNot) && return false + isa(stmt, Slot) && return false # Slots shouldn't occur in the IR at this point, but let's be defensive here + isa(stmt, GlobalRef) && return isdefined(stmt.mod, stmt.name) + if isa(stmt, Expr) + (; head, args) = stmt + if head === :static_parameter + etyp = (isa(src, IRCode) ? src.sptypes : src.ir.sptypes)[args[1]::Int] + # if we aren't certain enough about the type, it might be an UndefVarError at runtime + return isa(etyp, Const) + end + if head === :call + f = argextype(args[1], src) + f = singleton_type(f) + f === nothing && return false + is_return_type(f) && return true + if isa(f, IntrinsicFunction) + intrinsic_effect_free_if_nothrow(f) || return false + return intrinsic_nothrow(f, + Any[argextype(args[i], src) for i = 2:length(args)]) + end + contains_is(_PURE_BUILTINS, f) && return true + # `get_binding_type` sets the type to Any if the binding doesn't exist yet + if f === Core.get_binding_type + length(args) == 3 || return false + M, s = argextype(args[2], src), argextype(args[3], src) + return get_binding_type_effect_free(M, s) + end + contains_is(_EFFECT_FREE_BUILTINS, f) || return false + rt === Bottom && return false + return _builtin_nothrow(f, Any[argextype(args[i], src) for i = 2:length(args)], rt) + elseif head === :new + typ = argextype(args[1], src) + # `Expr(:new)` of unknown type could raise arbitrary TypeError. + typ, isexact = instanceof_tfunc(typ) + isexact || return false + isconcretedispatch(typ) || return false + typ = typ::DataType + fieldcount(typ) >= length(args) - 1 || return false + for fld_idx in 1:(length(args) - 1) + eT = argextype(args[fld_idx + 1], src) + fT = fieldtype(typ, fld_idx) + eT ⊑ fT || return false + end + return true + elseif head === :foreigncall + return foreigncall_effect_free(stmt, src) + elseif head === :new_opaque_closure + length(args) < 4 && return false + typ = argextype(args[1], src) + typ, isexact = instanceof_tfunc(typ) + isexact || return false + typ ⊑ Tuple || return false + rt_lb = argextype(args[2], src) + rt_ub = argextype(args[3], src) + src = argextype(args[4], src) + if !(rt_lb ⊑ Type && rt_ub ⊑ Type && src ⊑ Method) + return false + end + return true + elseif head === :isdefined || head === :the_exception || head === :copyast || head === :inbounds || head === :boundscheck + return true + else + # e.g. :loopinfo + return false + end + end + return true +end + +function foreigncall_effect_free(stmt::Expr, src::Union{IRCode,IncrementalCompact}) + args = stmt.args + name = args[1] + isa(name, QuoteNode) && (name = name.value) + isa(name, Symbol) || return false + ndims = alloc_array_ndims(name) + if ndims !== nothing + if ndims == 0 + return new_array_no_throw(args, src) + else + return alloc_array_no_throw(args, ndims, src) + end + end + return false +end + +function alloc_array_ndims(name::Symbol) + if name === :jl_alloc_array_1d + return 1 + elseif name === :jl_alloc_array_2d + return 2 + elseif name === :jl_alloc_array_3d + return 3 + elseif name === :jl_new_array + return 0 + end + return nothing +end + +const FOREIGNCALL_ARG_START = 6 + +function alloc_array_no_throw(args::Vector{Any}, ndims::Int, src::Union{IRCode,IncrementalCompact}) + length(args) ≥ ndims+FOREIGNCALL_ARG_START || return false + atype = instanceof_tfunc(argextype(args[FOREIGNCALL_ARG_START], src))[1] + dims = Csize_t[] + for i in 1:ndims + dim = argextype(args[i+FOREIGNCALL_ARG_START], src) + isa(dim, Const) || return false + dimval = dim.val + isa(dimval, Int) || return false + push!(dims, reinterpret(Csize_t, dimval)) + end + return _new_array_no_throw(atype, ndims, dims) +end + +function new_array_no_throw(args::Vector{Any}, src::Union{IRCode,IncrementalCompact}) + length(args) ≥ FOREIGNCALL_ARG_START+1 || return false + atype = instanceof_tfunc(argextype(args[FOREIGNCALL_ARG_START], src))[1] + dims = argextype(args[FOREIGNCALL_ARG_START+1], src) + isa(dims, Const) || return dims === Tuple{} + dimsval = dims.val + isa(dimsval, Tuple{Vararg{Int}}) || return false + ndims = nfields(dimsval) + isa(ndims, Int) || return false + dims = Csize_t[reinterpret(Csize_t, dimval) for dimval in dimsval] + return _new_array_no_throw(atype, ndims, dims) +end + +function _new_array_no_throw(@nospecialize(atype), ndims::Int, dims::Vector{Csize_t}) + isa(atype, DataType) || return false + eltype = atype.parameters[1] + iskindtype(typeof(eltype)) || return false + elsz = aligned_sizeof(eltype) + return ccall(:jl_array_validate_dims, Cint, + (Ptr{Csize_t}, Ptr{Csize_t}, UInt32, Ptr{Csize_t}, Csize_t), + #=nel=#RefValue{Csize_t}(), #=tot=#RefValue{Csize_t}(), ndims, dims, elsz) == 0 +end + +""" + argextype(x, src::Union{IRCode,IncrementalCompact}) -> t + argextype(x, src::CodeInfo, sptypes::Vector{Any}) -> t + +Return the type of value `x` in the context of inferred source `src`. +Note that `t` might be an extended lattice element. +Use `widenconst(t)` to get the native Julia type of `x`. +""" +argextype(@nospecialize(x), ir::IRCode, sptypes::Vector{Any} = ir.sptypes) = + argextype(x, ir, sptypes, ir.argtypes) +function argextype(@nospecialize(x), compact::IncrementalCompact, sptypes::Vector{Any} = compact.ir.sptypes) + isa(x, AnySSAValue) && return types(compact)[x] + return argextype(x, compact, sptypes, compact.ir.argtypes) +end +argextype(@nospecialize(x), src::CodeInfo, sptypes::Vector{Any}) = argextype(x, src, sptypes, src.slottypes::Vector{Any}) +function argextype( + @nospecialize(x), src::Union{IRCode,IncrementalCompact,CodeInfo}, + sptypes::Vector{Any}, slottypes::Vector{Any}) + if isa(x, Expr) + if x.head === :static_parameter + return sptypes[x.args[1]::Int] + elseif x.head === :boundscheck + return Bool + elseif x.head === :copyast + return argextype(x.args[1], src, sptypes, slottypes) + end + @assert false "argextype only works on argument-position values" + elseif isa(x, SlotNumber) + return slottypes[x.id] + elseif isa(x, TypedSlot) + return x.typ + elseif isa(x, SSAValue) + return abstract_eval_ssavalue(x, src) + elseif isa(x, Argument) + return slottypes[x.n] + elseif isa(x, QuoteNode) + return Const(x.value) + elseif isa(x, GlobalRef) + return abstract_eval_global(x.mod, x.name) + elseif isa(x, PhiNode) + return Any + elseif isa(x, PiNode) + return x.typ + else + return Const(x) + end +end +abstract_eval_ssavalue(s::SSAValue, src::Union{IRCode,IncrementalCompact}) = types(src)[s] + +struct ConstAPI + val + ConstAPI(@nospecialize val) = new(val) +end + +""" + finish(interp::AbstractInterpreter, opt::OptimizationState, + params::OptimizationParams, ir::IRCode, caller::InferenceResult) -> analyzed::Union{Nothing,ConstAPI} + +Post process information derived by Julia-level optimizations for later uses: +- computes "purity", i.e. side-effect-freeness +- computes inlining cost + +In a case when the purity is proven, `finish` can return `ConstAPI` object wrapping the constant +value so that the runtime system will use the constant calling convention for the method calls. +""" +function finish(interp::AbstractInterpreter, opt::OptimizationState, + params::OptimizationParams, ir::IRCode, caller::InferenceResult) + (; src, linfo) = opt + (; def, specTypes) = linfo + + analyzed = nothing # `ConstAPI` if this call can use constant calling convention + force_noinline = _any(x::Expr -> x.head === :meta && x.args[1] === :noinline, ir.meta) # compute inlining and other related optimizations + result = caller.result + @assert !(result isa LimitedAccuracy) + result = isa(result, InterConditional) ? widenconditional(result) : result if (isa(result, Const) || isconstType(result)) proven_pure = false - # must be proven pure to use const_api; otherwise we might skip throwing errors - # (issue #20704) + # must be proven pure to use constant calling convention; + # otherwise we might skip throwing errors (issue #20704) # TODO: Improve this analysis; if a function is marked @pure we should really # only care about certain errors (e.g. method errors and type errors). - if length(ir.stmts) < 10 + if length(ir.stmts) < 15 proven_pure = true for i in 1:length(ir.stmts) node = ir.stmts[i] stmt = node[:inst] - if stmt_affects_purity(stmt, ir) && !stmt_effect_free(stmt, node[:type], ir, ir.sptypes) + if stmt_affects_purity(stmt, ir) && !stmt_effect_free(stmt, node[:type], ir) proven_pure = false break end end if proven_pure - for fl in opt.src.slotflags + for fl in src.slotflags if (fl & SLOT_USEDUNDEF) != 0 proven_pure = false break @@ -212,9 +430,6 @@ function optimize(opt::OptimizationState, params::OptimizationParams, @nospecial end end end - if proven_pure - opt.src.pure = true - end if proven_pure # use constant calling convention @@ -223,19 +438,26 @@ function optimize(opt::OptimizationState, params::OptimizationParams, @nospecial # to the `jl_call_method_internal` fast path # Still set pure flag to make sure `inference` tests pass # and to possibly enable more optimization in the future - if !(isa(result, Const) && !is_inlineable_constant(result.val)) - opt.const_api = true + src.pure = true + if isa(result, Const) + val = result.val + if is_inlineable_constant(val) + analyzed = ConstAPI(val) + end + else + @assert isconstType(result) + analyzed = ConstAPI(result.parameters[1]) end - force_noinline || (opt.src.inlineable = true) + force_noinline || (src.inlineable = true) end end - replace_code_newstyle!(opt.src, ir, nargs) + opt.ir = ir # determine and cache inlineability union_penalties = false if !force_noinline - sig = unwrap_unionall(opt.linfo.specTypes) + sig = unwrap_unionall(specTypes) if isa(sig, DataType) && sig.name === Tuple.name for P in sig.parameters P = unwrap_unionall(P) @@ -247,44 +469,185 @@ function optimize(opt::OptimizationState, params::OptimizationParams, @nospecial else force_noinline = true end - if !opt.src.inlineable && result === Union{} + if !src.inlineable && result === Bottom force_noinline = true end end if force_noinline - opt.src.inlineable = false + src.inlineable = false elseif isa(def, Method) - if opt.src.inlineable && isdispatchtuple(opt.linfo.specTypes) + if src.inlineable && isdispatchtuple(specTypes) # obey @inline declaration if a dispatch barrier would not help else - bonus = 0 - if result ⊑ Tuple && !isbitstype(widenconst(result)) - bonus = params.inline_tupleret_bonus + # compute the cost (size) of inlining this code + cost_threshold = default = params.inline_cost_threshold + if result ⊑ Tuple && !isconcretetype(widenconst(result)) + cost_threshold += params.inline_tupleret_bonus end - if opt.src.inlineable - # For functions declared @inline, increase the cost threshold 20x - bonus += params.inline_cost_threshold*19 + # if the method is declared as `@inline`, increase the cost threshold 20x + if src.inlineable + cost_threshold += 19*default + end + # a few functions get special treatment + if def.module === _topmod(def.module) + name = def.name + if name === :iterate || name === :unsafe_convert || name === :cconvert + cost_threshold += 4*default + end end - opt.src.inlineable = isinlineable(def, opt, params, union_penalties, bonus) + src.inlineable = inline_worthy(ir, params, union_penalties, cost_threshold) + end + end + + return analyzed +end + +# run the optimization work +function optimize(interp::AbstractInterpreter, opt::OptimizationState, + params::OptimizationParams, caller::InferenceResult) + @timeit "optimizer" ir = run_passes(opt.src, opt, caller) + return finish(interp, opt, params, ir, caller) +end + +using .EscapeAnalysis +import .EscapeAnalysis: EscapeState, ArgEscapeCache, is_ipo_profitable + +""" + cache_escapes!(caller::InferenceResult, estate::EscapeState) + +Transforms escape information of call arguments of `caller`, +and then caches it into a global cache for later interprocedural propagation. +""" +cache_escapes!(caller::InferenceResult, estate::EscapeState) = + caller.argescapes = ArgEscapeCache(estate) + +function ipo_escape_cache(mi_cache::MICache) where MICache + return function (linfo::Union{InferenceResult,MethodInstance}) + if isa(linfo, InferenceResult) + argescapes = linfo.argescapes + else + codeinst = get(mi_cache, linfo, nothing) + isa(codeinst, CodeInstance) || return nothing + argescapes = codeinst.argescapes end + return argescapes !== nothing ? argescapes::ArgEscapeCache : nothing + end +end +null_escape_cache(linfo::Union{InferenceResult,MethodInstance}) = nothing + +function run_passes(ci::CodeInfo, sv::OptimizationState, caller::InferenceResult) + @timeit "convert" ir = convert_to_ircode(ci, sv) + @timeit "slot2reg" ir = slot2reg(ir, ci, sv) + # TODO: Domsorting can produce an updated domtree - no need to recompute here + @timeit "compact 1" ir = compact!(ir) + @timeit "Inlining" ir = ssa_inlining_pass!(ir, ir.linetable, sv.inlining, ci.propagate_inbounds) + # @timeit "verify 2" verify_ir(ir) + @timeit "compact 2" ir = compact!(ir) + @timeit "SROA" ir = sroa_pass!(ir) + @timeit "ADCE" ir = adce_pass!(ir) + @timeit "type lift" ir = type_lift_pass!(ir) + @timeit "compact 3" ir = compact!(ir) + if JLOptions().debug_level == 2 + @timeit "verify 3" (verify_ir(ir); verify_linetable(ir.linetable)) end - nothing + return ir end +function convert_to_ircode(ci::CodeInfo, sv::OptimizationState) + linetable = ci.linetable + if !isa(linetable, Vector{LineInfoNode}) + linetable = collect(LineInfoNode, linetable::Vector{Any})::Vector{LineInfoNode} + end + + # check if coverage mode is enabled + coverage = coverage_enabled(sv.mod) + if !coverage && JLOptions().code_coverage == 3 # path-specific coverage mode + for line in linetable + if is_file_tracked(line.file) + # if any line falls in a tracked file enable coverage for all + coverage = true + break + end + end + end -# whether `f` is pure for inference -function is_pure_intrinsic_infer(f::IntrinsicFunction) - return !(f === Intrinsics.pointerref || # this one is volatile - f === Intrinsics.pointerset || # this one is never effect-free - f === Intrinsics.llvmcall || # this one is never effect-free - f === Intrinsics.arraylen || # this one is volatile - f === Intrinsics.sqrt_llvm || # this one may differ at runtime (by a few ulps) - f === Intrinsics.sqrt_llvm_fast || # this one may differ at runtime (by a few ulps) - f === Intrinsics.cglobal) # cglobal lookup answer changes at runtime + # Go through and add an unreachable node after every + # Union{} call. Then reindex labels. + code = copy_exprargs(ci.code) + stmtinfo = sv.stmt_info + codelocs = ci.codelocs + ssavaluetypes = ci.ssavaluetypes::Vector{Any} + ssaflags = ci.ssaflags + meta = Expr[] + idx = 1 + oldidx = 1 + ssachangemap = fill(0, length(code)) + labelchangemap = coverage ? fill(0, length(code)) : ssachangemap + prevloc = zero(eltype(ci.codelocs)) + while idx <= length(code) + codeloc = codelocs[idx] + if coverage && codeloc != prevloc && codeloc != 0 + # insert a side-effect instruction before the current instruction in the same basic block + insert!(code, idx, Expr(:code_coverage_effect)) + insert!(codelocs, idx, codeloc) + insert!(ssavaluetypes, idx, Nothing) + insert!(stmtinfo, idx, nothing) + insert!(ssaflags, idx, IR_FLAG_NULL) + ssachangemap[oldidx] += 1 + if oldidx < length(labelchangemap) + labelchangemap[oldidx + 1] += 1 + end + idx += 1 + prevloc = codeloc + end + if code[idx] isa Expr && ssavaluetypes[idx] === Union{} + if !(idx < length(code) && isa(code[idx + 1], ReturnNode) && !isdefined((code[idx + 1]::ReturnNode), :val)) + # insert unreachable in the same basic block after the current instruction (splitting it) + insert!(code, idx + 1, ReturnNode()) + insert!(codelocs, idx + 1, codelocs[idx]) + insert!(ssavaluetypes, idx + 1, Union{}) + insert!(stmtinfo, idx + 1, nothing) + insert!(ssaflags, idx + 1, ssaflags[idx]) + if oldidx < length(ssachangemap) + ssachangemap[oldidx + 1] += 1 + coverage && (labelchangemap[oldidx + 1] += 1) + end + idx += 1 + end + end + idx += 1 + oldidx += 1 + end + + renumber_ir_elements!(code, ssachangemap, labelchangemap) + + for i = 1:length(code) + code[i] = process_meta!(meta, code[i]) + end + strip_trailing_junk!(ci, code, stmtinfo) + types = Any[] + stmts = InstructionStream(code, types, stmtinfo, codelocs, ssaflags) + cfg = compute_basic_blocks(code) + return IRCode(stmts, cfg, linetable, sv.slottypes, meta, sv.sptypes) end -# whether `f` is effect free if nothrow -intrinsic_effect_free_if_nothrow(f) = f === Intrinsics.pointerref || is_pure_intrinsic_infer(f) +function process_meta!(meta::Vector{Expr}, @nospecialize stmt) + if isexpr(stmt, :meta) && length(stmt.args) ≥ 1 + push!(meta, stmt) + return nothing + end + return stmt +end + +function slot2reg(ir::IRCode, ci::CodeInfo, sv::OptimizationState) + # need `ci` for the slot metadata, IR for the code + svdef = sv.linfo.def + nargs = isa(svdef, Method) ? Int(svdef.nargs) : 0 + @timeit "domtree 1" domtree = construct_domtree(ir.cfg.blocks) + defuse_insts = scan_slot_def_use(nargs, ci, ir.stmts.inst) + @timeit "construct_ssa" ir = construct_ssa!(ci, ir, domtree, defuse_insts, sv.slottypes) # consumes `ir` + return ir +end ## Computing the cost of a function body @@ -294,21 +657,20 @@ plus_saturate(x::Int, y::Int) = max(x, y, x+y) # known return type isknowntype(@nospecialize T) = (T === Union{}) || isa(T, Const) || isconcretetype(widenconst(T)) -function statement_cost(ex::Expr, line::Int, src::CodeInfo, sptypes::Vector{Any}, - slottypes::Vector{Any}, union_penalties::Bool, - params::OptimizationParams, error_path::Bool = false) +function statement_cost(ex::Expr, line::Int, src::Union{CodeInfo, IRCode}, sptypes::Vector{Any}, + union_penalties::Bool, params::OptimizationParams, error_path::Bool = false) head = ex.head if is_meta_expr_head(head) return 0 elseif head === :call farg = ex.args[1] - ftyp = argextype(farg, src, sptypes, slottypes) + ftyp = argextype(farg, src, sptypes) if ftyp === IntrinsicFunction && farg isa SSAValue # if this comes from code that was already inlined into another function, # Consts have been widened. try to recover in simple cases. - farg = src.code[farg.id] + farg = isa(src, CodeInfo) ? src.code[farg.id] : src.stmts[farg.id][:inst] if isa(farg, GlobalRef) || isa(farg, QuoteNode) || isa(farg, IntrinsicFunction) || isexpr(farg, :static_parameter) - ftyp = argextype(farg, src, sptypes, slottypes) + ftyp = argextype(farg, src, sptypes) end end f = singleton_type(ftyp) @@ -324,42 +686,44 @@ function statement_cost(ex::Expr, line::Int, src::CodeInfo, sptypes::Vector{Any} # The efficiency of operations like a[i] and s.b # depend strongly on whether the result can be # inferred, so check the type of ex - if f === Main.Core.getfield || f === Main.Core.tuple + if f === Core.getfield || f === Core.tuple || f === Core.getglobal # we might like to penalize non-inferrability, but # tuple iteration/destructuring makes that impossible # return plus_saturate(argcost, isknowntype(extyp) ? 1 : params.inline_nonleaf_penalty) return 0 - elseif f === Main.Core.isa + elseif (f === Core.arrayref || f === Core.const_arrayref || f === Core.arrayset) && length(ex.args) >= 3 + atyp = argextype(ex.args[3], src, sptypes) + return isknowntype(atyp) ? 4 : error_path ? params.inline_error_path_cost : params.inline_nonleaf_penalty + elseif f === typeassert && isconstType(widenconst(argextype(ex.args[3], src, sptypes))) + return 1 + elseif f === Core.isa # If we're in a union context, we penalize type computations # on union types. In such cases, it is usually better to perform # union splitting on the outside. - if union_penalties && isa(argextype(ex.args[2], src, sptypes, slottypes), Union) + if union_penalties && isa(argextype(ex.args[2], src, sptypes), Union) return params.inline_nonleaf_penalty end - elseif (f === Main.Core.arrayref || f === Main.Core.const_arrayref) && length(ex.args) >= 3 - atyp = argextype(ex.args[3], src, sptypes, slottypes) - return isknowntype(atyp) ? 4 : error_path ? params.inline_error_path_cost : params.inline_nonleaf_penalty end fidx = find_tfunc(f) if fidx === nothing - # unknown/unhandled builtin or anonymous function + # unknown/unhandled builtin # Use the generic cost of a direct function call return 20 end return T_FFUNC_COST[fidx] end - extyp = line == -1 ? Any : src.ssavaluetypes[line] + extyp = line == -1 ? Any : argextype(SSAValue(line), src, sptypes) if extyp === Union{} return 0 end return error_path ? params.inline_error_path_cost : params.inline_nonleaf_penalty - elseif head === :foreigncall || head === :invoke + elseif head === :foreigncall || head === :invoke || head == :invoke_modify # Calls whose "return type" is Union{} do not actually return: # they are errors. Since these are not part of the typical # run-time of the function, we omit them from # consideration. This way, non-inlined error branches do not # prevent inlining. - extyp = line == -1 ? Any : src.ssavaluetypes[line] + extyp = line == -1 ? Any : argextype(SSAValue(line), src, sptypes) return extyp === Union{} ? 0 : 20 elseif head === :(=) if ex.args[1] isa GlobalRef @@ -369,7 +733,7 @@ function statement_cost(ex::Expr, line::Int, src::CodeInfo, sptypes::Vector{Any} end a = ex.args[2] if a isa Expr - cost = plus_saturate(cost, statement_cost(a, -1, src, sptypes, slottypes, params, error_path)) + cost = plus_saturate(cost, statement_cost(a, -1, src, sptypes, union_penalties, params, error_path)) end return cost elseif head === :copyast @@ -384,43 +748,42 @@ function statement_cost(ex::Expr, line::Int, src::CodeInfo, sptypes::Vector{Any} return 0 end -function statement_or_branch_cost(@nospecialize(stmt), line::Int, src::CodeInfo, sptypes::Vector{Any}, - slottypes::Vector{Any}, union_penalties::Bool, params::OptimizationParams, - throw_blocks::Union{Nothing,BitSet}) +function statement_or_branch_cost(@nospecialize(stmt), line::Int, src::Union{CodeInfo, IRCode}, sptypes::Vector{Any}, + union_penalties::Bool, params::OptimizationParams) thiscost = 0 + dst(tgt) = isa(src, IRCode) ? first(src.cfg.blocks[tgt].stmts) : tgt if stmt isa Expr - thiscost = statement_cost(stmt, line, src, sptypes, slottypes, union_penalties, params, - params.unoptimize_throw_blocks && line in throw_blocks)::Int + thiscost = statement_cost(stmt, line, src, sptypes, union_penalties, params, + is_stmt_throw_block(isa(src, IRCode) ? src.stmts.flag[line] : src.ssaflags[line]))::Int elseif stmt isa GotoNode # loops are generally always expensive # but assume that forward jumps are already counted for from # summing the cost of the not-taken branch - thiscost = stmt.label < line ? 40 : 0 + thiscost = dst(stmt.label) < line ? 40 : 0 elseif stmt isa GotoIfNot - thiscost = stmt.dest < line ? 40 : 0 + thiscost = dst(stmt.dest) < line ? 40 : 0 end return thiscost end -function inline_worthy(body::Array{Any,1}, src::CodeInfo, sptypes::Vector{Any}, slottypes::Vector{Any}, +function inline_worthy(ir::IRCode, params::OptimizationParams, union_penalties::Bool=false, cost_threshold::Integer=params.inline_cost_threshold) bodycost::Int = 0 - throw_blocks = params.unoptimize_throw_blocks ? find_throw_blocks(body) : nothing - for line = 1:length(body) - stmt = body[line] - thiscost = statement_or_branch_cost(stmt, line, src, sptypes, slottypes, union_penalties, params, throw_blocks) + for line = 1:length(ir.stmts) + stmt = ir.stmts[line][:inst] + thiscost = statement_or_branch_cost(stmt, line, ir, ir.sptypes, union_penalties, params) bodycost = plus_saturate(bodycost, thiscost) bodycost > cost_threshold && return false end return true end -function statement_costs!(cost::Vector{Int}, body::Vector{Any}, src::CodeInfo, sptypes::Vector{Any}, unionpenalties::Bool, params::OptimizationParams) - throw_blocks = params.unoptimize_throw_blocks ? find_throw_blocks(body) : nothing +function statement_costs!(cost::Vector{Int}, body::Vector{Any}, src::Union{CodeInfo, IRCode}, sptypes::Vector{Any}, unionpenalties::Bool, params::OptimizationParams) maxcost = 0 for line = 1:length(body) stmt = body[line] - thiscost = statement_or_branch_cost(stmt, line, src, sptypes, src.slottypes, unionpenalties, params, throw_blocks) + thiscost = statement_or_branch_cost(stmt, line, src, sptypes, + unionpenalties, params) cost[line] = thiscost if thiscost > maxcost maxcost = thiscost @@ -429,30 +792,33 @@ function statement_costs!(cost::Vector{Int}, body::Vector{Any}, src::CodeInfo, s return maxcost end -function is_known_call(e::Expr, @nospecialize(func), src, sptypes::Vector{Any}, slottypes::Vector{Any} = empty_slottypes) - if e.head !== :call - return false - end - f = argextype(e.args[1], src, sptypes, slottypes) - return isa(f, Const) && f.val === func +function renumber_ir_elements!(body::Vector{Any}, ssachangemap::Vector{Int}) + return renumber_ir_elements!(body, ssachangemap, ssachangemap) end -function renumber_ir_elements!(body::Vector{Any}, changemap::Vector{Int}) - return renumber_ir_elements!(body, changemap, changemap) +function cumsum_ssamap!(ssachangemap::Vector{Int}) + any_change = false + rel_change = 0 + for i = 1:length(ssachangemap) + val = ssachangemap[i] + any_change |= val ≠ 0 + rel_change += val + if val == -1 + # Keep a marker that this statement was deleted + ssachangemap[i] = typemin(Int) + else + ssachangemap[i] = rel_change + end + end + return any_change end function renumber_ir_elements!(body::Vector{Any}, ssachangemap::Vector{Int}, labelchangemap::Vector{Int}) - for i = 2:length(labelchangemap) - labelchangemap[i] += labelchangemap[i - 1] - end + any_change = cumsum_ssamap!(labelchangemap) if ssachangemap !== labelchangemap - for i = 2:length(ssachangemap) - ssachangemap[i] += ssachangemap[i - 1] - end - end - if labelchangemap[end] == 0 && ssachangemap[end] == 0 - return + any_change |= cumsum_ssamap!(ssachangemap) end + any_change || return for i = 1:length(body) el = body[i] if isa(el, GotoNode) @@ -462,13 +828,35 @@ function renumber_ir_elements!(body::Vector{Any}, ssachangemap::Vector{Int}, lab if isa(cond, SSAValue) cond = SSAValue(cond.id + ssachangemap[cond.id]) end - body[i] = GotoIfNot(cond, el.dest + labelchangemap[el.dest]) + was_deleted = labelchangemap[el.dest] == typemin(Int) + body[i] = was_deleted ? cond : GotoIfNot(cond, el.dest + labelchangemap[el.dest]) elseif isa(el, ReturnNode) - if isdefined(el, :val) && isa(el.val, SSAValue) - body[i] = ReturnNode(SSAValue(el.val.id + ssachangemap[el.val.id])) + if isdefined(el, :val) + val = el.val + if isa(val, SSAValue) + body[i] = ReturnNode(SSAValue(val.id + ssachangemap[val.id])) + end end elseif isa(el, SSAValue) body[i] = SSAValue(el.id + ssachangemap[el.id]) + elseif isa(el, PhiNode) + i = 1 + edges = el.edges + values = el.values + while i <= length(edges) + was_deleted = ssachangemap[edges[i]] == typemin(Int) + if was_deleted + deleteat!(edges, i) + deleteat!(values, i) + else + edges[i] += ssachangemap[edges[i]] + val = values[i] + if isa(val, SSAValue) + values[i] = SSAValue(val.id + ssachangemap[val.id]) + end + i += 1 + end + end elseif isa(el, Expr) if el.head === :(=) && el.args[2] isa Expr el = el.args[2]::Expr @@ -488,5 +876,3 @@ function renumber_ir_elements!(body::Vector{Any}, ssachangemap::Vector{Int}, lab end end end - -include("compiler/ssair/driver.jl") diff --git a/base/compiler/parsing.jl b/base/compiler/parsing.jl index 19aaf08cdde53b..8b474cf148fb29 100644 --- a/base/compiler/parsing.jl +++ b/base/compiler/parsing.jl @@ -3,17 +3,17 @@ # Call Julia's builtin flisp-based parser. `offset` is 0-based offset into the # byte buffer or string. function fl_parse(text::Union{Core.SimpleVector,String}, - filename::String, offset, options) + filename::String, lineno, offset, options) if text isa Core.SimpleVector # Will be generated by C entry points jl_parse_string etc text, text_len = text else text_len = sizeof(text) end - ccall(:jl_fl_parse, Any, (Ptr{UInt8}, Csize_t, Any, Csize_t, Any), - text, text_len, filename, offset, options) + ccall(:jl_fl_parse, Any, (Ptr{UInt8}, Csize_t, Any, Csize_t, Csize_t, Any), + text, text_len, filename, lineno, offset, options) end -function fl_parse(text::AbstractString, filename::AbstractString, offset, options) - fl_parse(String(text), String(filename), offset, options) +function fl_parse(text::AbstractString, filename::AbstractString, lineno, offset, options) + fl_parse(String(text), String(filename), lineno, offset, options) end diff --git a/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl b/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl new file mode 100644 index 00000000000000..407b447a228a39 --- /dev/null +++ b/base/compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl @@ -0,0 +1,1913 @@ +baremodule EscapeAnalysis + +export + analyze_escapes, + getaliases, + isaliased, + has_no_escape, + has_arg_escape, + has_return_escape, + has_thrown_escape, + has_all_escape + +const _TOP_MOD = ccall(:jl_base_relative_to, Any, (Any,), EscapeAnalysis)::Module + +# imports +import ._TOP_MOD: ==, getindex, setindex! +# usings +import Core: + MethodInstance, Const, Argument, SSAValue, PiNode, PhiNode, UpsilonNode, PhiCNode, + ReturnNode, GotoNode, GotoIfNot, SimpleVector, MethodMatch, CodeInstance, + sizeof, ifelse, arrayset, arrayref, arraysize +import ._TOP_MOD: # Base definitions + @__MODULE__, @eval, @assert, @specialize, @nospecialize, @inbounds, @inline, @noinline, + @label, @goto, !, !==, !=, ≠, +, -, *, ≤, <, ≥, >, &, |, <<, error, missing, copy, + Vector, BitSet, IdDict, IdSet, UnitRange, Csize_t, Callable, ∪, ⊆, ∩, :, ∈, ∉, =>, + in, length, get, first, last, haskey, keys, get!, isempty, isassigned, + pop!, push!, pushfirst!, empty!, delete!, max, min, enumerate, unwrap_unionall, + ismutabletype +import Core.Compiler: # Core.Compiler specific definitions + Bottom, InferenceResult, IRCode, IR_FLAG_EFFECT_FREE, + isbitstype, isexpr, is_meta_expr_head, println, widenconst, argextype, singleton_type, + fieldcount_noerror, try_compute_field, try_compute_fieldidx, hasintersect, ⊑, + intrinsic_nothrow, array_builtin_common_typecheck, arrayset_typecheck, + setfield!_nothrow, alloc_array_ndims, stmt_effect_free, check_effect_free! + +include(x) = _TOP_MOD.include(@__MODULE__, x) +if _TOP_MOD === Core.Compiler + include("compiler/ssair/EscapeAnalysis/disjoint_set.jl") +else + include("disjoint_set.jl") +end + +const AInfo = IdSet{Any} +const LivenessSet = BitSet + +""" + x::EscapeInfo + +A lattice for escape information, which holds the following properties: +- `x.Analyzed::Bool`: not formally part of the lattice, only indicates `x` has not been analyzed or not +- `x.ReturnEscape::Bool`: indicates `x` can escape to the caller via return +- `x.ThrownEscape::BitSet`: records SSA statement numbers where `x` can be thrown as exception: + * `isempty(x.ThrownEscape)`: `x` will never be thrown in this call frame (the bottom) + * `pc ∈ x.ThrownEscape`: `x` may be thrown at the SSA statement at `pc` + * `-1 ∈ x.ThrownEscape`: `x` may be thrown at arbitrary points of this call frame (the top) + This information will be used by `escape_exception!` to propagate potential escapes via exception. +- `x.AliasInfo::Union{Bool,IndexableFields,IndexableElements,Unindexable}`: maintains all possible values + that can be aliased to fields or array elements of `x`: + * `x.AliasInfo === false` indicates the fields/elements of `x` aren't analyzed yet + * `x.AliasInfo === true` indicates the fields/elements of `x` can't be analyzed, + e.g. the type of `x` is not known or is not concrete and thus its fields/elements + can't be known precisely + * `x.AliasInfo::IndexableFields` records all the possible values that can be aliased to fields of object `x` with precise index information + * `x.AliasInfo::IndexableElements` records all the possible values that can be aliased to elements of array `x` with precise index information + * `x.AliasInfo::Unindexable` records all the possible values that can be aliased to fields/elements of `x` without precise index information +- `x.Liveness::BitSet`: records SSA statement numbers where `x` should be live, e.g. + to be used as a call argument, to be returned to a caller, or preserved for `:foreigncall`: + * `isempty(x.Liveness)`: `x` is never be used in this call frame (the bottom) + * `0 ∈ x.Liveness` also has the special meaning that it's a call argument of the currently + analyzed call frame (and thus it's visible from the caller immediately). + * `pc ∈ x.Liveness`: `x` may be used at the SSA statement at `pc` + * `-1 ∈ x.Liveness`: `x` may be used at arbitrary points of this call frame (the top) + +There are utility constructors to create common `EscapeInfo`s, e.g., +- `NoEscape()`: the bottom(-like) element of this lattice, meaning it won't escape to anywhere +- `AllEscape()`: the topmost element of this lattice, meaning it will escape to everywhere + +`analyze_escapes` will transition these elements from the bottom to the top, +in the same direction as Julia's native type inference routine. +An abstract state will be initialized with the bottom(-like) elements: +- the call arguments are initialized as `ArgEscape()`, whose `Liveness` property includes `0` + to indicate that it is passed as a call argument and visible from a caller immediately +- the other states are initialized as `NotAnalyzed()`, which is a special lattice element that + is slightly lower than `NoEscape`, but at the same time doesn't represent any meaning + other than it's not analyzed yet (thus it's not formally part of the lattice) +""" +struct EscapeInfo + Analyzed::Bool + ReturnEscape::Bool + ThrownEscape::LivenessSet + AliasInfo #::Union{IndexableFields,IndexableElements,Unindexable,Bool} + Liveness::LivenessSet + + function EscapeInfo( + Analyzed::Bool, + ReturnEscape::Bool, + ThrownEscape::LivenessSet, + AliasInfo#=::Union{IndexableFields,IndexableElements,Unindexable,Bool}=#, + Liveness::LivenessSet, + ) + @nospecialize AliasInfo + return new( + Analyzed, + ReturnEscape, + ThrownEscape, + AliasInfo, + Liveness, + ) + end + function EscapeInfo( + x::EscapeInfo, + # non-concrete fields should be passed as default arguments + # in order to avoid allocating non-concrete `NamedTuple`s + AliasInfo#=::Union{IndexableFields,IndexableElements,Unindexable,Bool}=# = x.AliasInfo; + Analyzed::Bool = x.Analyzed, + ReturnEscape::Bool = x.ReturnEscape, + ThrownEscape::LivenessSet = x.ThrownEscape, + Liveness::LivenessSet = x.Liveness, + ) + @nospecialize AliasInfo + return new( + Analyzed, + ReturnEscape, + ThrownEscape, + AliasInfo, + Liveness, + ) + end +end + +# precomputed default values in order to eliminate computations at each callsite + +const BOT_THROWN_ESCAPE = LivenessSet() +# NOTE the lattice operations should try to avoid actual set computations on this top value, +# and e.g. LivenessSet(0:1000000) should also work without incurring excessive computations +const TOP_THROWN_ESCAPE = LivenessSet(-1) + +const BOT_LIVENESS = LivenessSet() +# NOTE the lattice operations should try to avoid actual set computations on this top value, +# and e.g. LivenessSet(0:1000000) should also work without incurring excessive computations +const TOP_LIVENESS = LivenessSet(-1:0) +const ARG_LIVENESS = LivenessSet(0) + +# the constructors +NotAnalyzed() = EscapeInfo(false, false, BOT_THROWN_ESCAPE, false, BOT_LIVENESS) # not formally part of the lattice +NoEscape() = EscapeInfo(true, false, BOT_THROWN_ESCAPE, false, BOT_LIVENESS) +ArgEscape() = EscapeInfo(true, false, BOT_THROWN_ESCAPE, true, ARG_LIVENESS) +ReturnEscape(pc::Int) = EscapeInfo(true, true, BOT_THROWN_ESCAPE, false, LivenessSet(pc)) +AllReturnEscape() = EscapeInfo(true, true, BOT_THROWN_ESCAPE, false, TOP_LIVENESS) +ThrownEscape(pc::Int) = EscapeInfo(true, false, LivenessSet(pc), false, BOT_LIVENESS) +AllEscape() = EscapeInfo(true, true, TOP_THROWN_ESCAPE, true, TOP_LIVENESS) + +const ⊥, ⊤ = NotAnalyzed(), AllEscape() + +# Convenience names for some ⊑ₑ queries +has_no_escape(x::EscapeInfo) = !x.ReturnEscape && isempty(x.ThrownEscape) && 0 ∉ x.Liveness +has_arg_escape(x::EscapeInfo) = 0 in x.Liveness +has_return_escape(x::EscapeInfo) = x.ReturnEscape +has_return_escape(x::EscapeInfo, pc::Int) = x.ReturnEscape && (-1 ∈ x.Liveness || pc in x.Liveness) +has_thrown_escape(x::EscapeInfo) = !isempty(x.ThrownEscape) +has_thrown_escape(x::EscapeInfo, pc::Int) = -1 ∈ x.ThrownEscape || pc in x.ThrownEscape +has_all_escape(x::EscapeInfo) = ⊤ ⊑ₑ x + +# utility lattice constructors +ignore_argescape(x::EscapeInfo) = EscapeInfo(x; Liveness=delete!(copy(x.Liveness), 0)) +ignore_thrownescapes(x::EscapeInfo) = EscapeInfo(x; ThrownEscape=BOT_THROWN_ESCAPE) +ignore_aliasinfo(x::EscapeInfo) = EscapeInfo(x, false) +ignore_liveness(x::EscapeInfo) = EscapeInfo(x; Liveness=BOT_LIVENESS) + +# AliasInfo +struct IndexableFields + infos::Vector{AInfo} +end +struct IndexableElements + infos::IdDict{Int,AInfo} +end +struct Unindexable + info::AInfo +end +IndexableFields(nflds::Int) = IndexableFields(AInfo[AInfo() for _ in 1:nflds]) +Unindexable() = Unindexable(AInfo()) + +merge_to_unindexable(AliasInfo::IndexableFields) = Unindexable(merge_to_unindexable(AliasInfo.infos)) +merge_to_unindexable(AliasInfo::Unindexable, AliasInfos::IndexableFields) = Unindexable(merge_to_unindexable(AliasInfo.info, AliasInfos.infos)) +merge_to_unindexable(infos::Vector{AInfo}) = merge_to_unindexable(AInfo(), infos) +function merge_to_unindexable(info::AInfo, infos::Vector{AInfo}) + for i = 1:length(infos) + info = info ∪ infos[i] + end + return info +end +merge_to_unindexable(AliasInfo::IndexableElements) = Unindexable(merge_to_unindexable(AliasInfo.infos)) +merge_to_unindexable(AliasInfo::Unindexable, AliasInfos::IndexableElements) = Unindexable(merge_to_unindexable(AliasInfo.info, AliasInfos.infos)) +merge_to_unindexable(infos::IdDict{Int,AInfo}) = merge_to_unindexable(AInfo(), infos) +function merge_to_unindexable(info::AInfo, infos::IdDict{Int,AInfo}) + for idx in keys(infos) + info = info ∪ infos[idx] + end + return info +end + +# we need to make sure this `==` operator corresponds to lattice equality rather than object equality, +# otherwise `propagate_changes` can't detect the convergence +x::EscapeInfo == y::EscapeInfo = begin + # fast pass: better to avoid top comparison + x === y && return true + x.Analyzed === y.Analyzed || return false + x.ReturnEscape === y.ReturnEscape || return false + xt, yt = x.ThrownEscape, y.ThrownEscape + if xt === TOP_THROWN_ESCAPE + yt === TOP_THROWN_ESCAPE || return false + elseif yt === TOP_THROWN_ESCAPE + return false # x.ThrownEscape === TOP_THROWN_ESCAPE + else + xt == yt || return false + end + xa, ya = x.AliasInfo, y.AliasInfo + if isa(xa, Bool) + xa === ya || return false + elseif isa(xa, IndexableFields) + isa(ya, IndexableFields) || return false + xa.infos == ya.infos || return false + elseif isa(xa, IndexableElements) + isa(ya, IndexableElements) || return false + xa.infos == ya.infos || return false + else + xa = xa::Unindexable + isa(ya, Unindexable) || return false + xa.info == ya.info || return false + end + xl, yl = x.Liveness, y.Liveness + if xl === TOP_LIVENESS + yl === TOP_LIVENESS || return false + elseif yl === TOP_LIVENESS + return false # x.Liveness === TOP_LIVENESS + else + xl == yl || return false + end + return true +end + +""" + x::EscapeInfo ⊑ₑ y::EscapeInfo -> Bool + +The non-strict partial order over [`EscapeInfo`](@ref). +""" +x::EscapeInfo ⊑ₑ y::EscapeInfo = begin + # fast pass: better to avoid top comparison + if y === ⊤ + return true + elseif x === ⊤ + return false # return y === ⊤ + elseif x === ⊥ + return true + elseif y === ⊥ + return false # return x === ⊥ + end + x.Analyzed ≤ y.Analyzed || return false + x.ReturnEscape ≤ y.ReturnEscape || return false + xt, yt = x.ThrownEscape, y.ThrownEscape + if xt === TOP_THROWN_ESCAPE + yt !== TOP_THROWN_ESCAPE && return false + elseif yt !== TOP_THROWN_ESCAPE + xt ⊆ yt || return false + end + xa, ya = x.AliasInfo, y.AliasInfo + if isa(xa, Bool) + xa && ya !== true && return false + elseif isa(xa, IndexableFields) + if isa(ya, IndexableFields) + xinfos, yinfos = xa.infos, ya.infos + xn, yn = length(xinfos), length(yinfos) + xn > yn && return false + for i in 1:xn + xinfos[i] ⊆ yinfos[i] || return false + end + elseif isa(ya, IndexableElements) + return false + elseif isa(ya, Unindexable) + xinfos, yinfo = xa.infos, ya.info + for i = length(xinfos) + xinfos[i] ⊆ yinfo || return false + end + else + ya === true || return false + end + elseif isa(xa, IndexableElements) + if isa(ya, IndexableElements) + xinfos, yinfos = xa.infos, ya.infos + keys(xinfos) ⊆ keys(yinfos) || return false + for idx in keys(xinfos) + xinfos[idx] ⊆ yinfos[idx] || return false + end + elseif isa(ya, IndexableFields) + return false + elseif isa(ya, Unindexable) + xinfos, yinfo = xa.infos, ya.info + for idx in keys(xinfos) + xinfos[idx] ⊆ yinfo || return false + end + else + ya === true || return false + end + else + xa = xa::Unindexable + if isa(ya, Unindexable) + xinfo, yinfo = xa.info, ya.info + xinfo ⊆ yinfo || return false + else + ya === true || return false + end + end + xl, yl = x.Liveness, y.Liveness + if xl === TOP_LIVENESS + yl !== TOP_LIVENESS && return false + elseif yl !== TOP_LIVENESS + xl ⊆ yl || return false + end + return true +end + +""" + x::EscapeInfo ⊏ₑ y::EscapeInfo -> Bool + +The strict partial order over [`EscapeInfo`](@ref). +This is defined as the irreflexive kernel of `⊏ₑ`. +""" +x::EscapeInfo ⊏ₑ y::EscapeInfo = x ⊑ₑ y && !(y ⊑ₑ x) + +""" + x::EscapeInfo ⋤ₑ y::EscapeInfo -> Bool + +This order could be used as a slightly more efficient version of the strict order `⊏ₑ`, +where we can safely assume `x ⊑ₑ y` holds. +""" +x::EscapeInfo ⋤ₑ y::EscapeInfo = !(y ⊑ₑ x) + +""" + x::EscapeInfo ⊔ₑ y::EscapeInfo -> EscapeInfo + +Computes the join of `x` and `y` in the partial order defined by [`EscapeInfo`](@ref). +""" +x::EscapeInfo ⊔ₑ y::EscapeInfo = begin + # fast pass: better to avoid top join + if x === ⊤ || y === ⊤ + return ⊤ + elseif x === ⊥ + return y + elseif y === ⊥ + return x + end + xt, yt = x.ThrownEscape, y.ThrownEscape + if xt === TOP_THROWN_ESCAPE || yt === TOP_THROWN_ESCAPE + ThrownEscape = TOP_THROWN_ESCAPE + elseif xt === BOT_THROWN_ESCAPE + ThrownEscape = yt + elseif yt === BOT_THROWN_ESCAPE + ThrownEscape = xt + else + ThrownEscape = xt ∪ yt + end + AliasInfo = merge_alias_info(x.AliasInfo, y.AliasInfo) + xl, yl = x.Liveness, y.Liveness + if xl === TOP_LIVENESS || yl === TOP_LIVENESS + Liveness = TOP_LIVENESS + elseif xl === BOT_LIVENESS + Liveness = yl + elseif yl === BOT_LIVENESS + Liveness = xl + else + Liveness = xl ∪ yl + end + return EscapeInfo( + x.Analyzed | y.Analyzed, + x.ReturnEscape | y.ReturnEscape, + ThrownEscape, + AliasInfo, + Liveness, + ) +end + +function merge_alias_info(@nospecialize(xa), @nospecialize(ya)) + if xa === true || ya === true + return true + elseif xa === false + return ya + elseif ya === false + return xa + elseif isa(xa, IndexableFields) + if isa(ya, IndexableFields) + xinfos, yinfos = xa.infos, ya.infos + xn, yn = length(xinfos), length(yinfos) + nmax, nmin = max(xn, yn), min(xn, yn) + infos = Vector{AInfo}(undef, nmax) + for i in 1:nmax + if i > nmin + infos[i] = (xn > yn ? xinfos : yinfos)[i] + else + infos[i] = xinfos[i] ∪ yinfos[i] + end + end + return IndexableFields(infos) + elseif isa(ya, Unindexable) + xinfos, yinfo = xa.infos, ya.info + return merge_to_unindexable(ya, xa) + else + return true # handle conflicting case conservatively + end + elseif isa(xa, IndexableElements) + if isa(ya, IndexableElements) + xinfos, yinfos = xa.infos, ya.infos + infos = IdDict{Int,AInfo}() + for idx in keys(xinfos) + if !haskey(yinfos, idx) + infos[idx] = xinfos[idx] + else + infos[idx] = xinfos[idx] ∪ yinfos[idx] + end + end + for idx in keys(yinfos) + haskey(xinfos, idx) && continue # unioned already + infos[idx] = yinfos[idx] + end + return IndexableElements(infos) + elseif isa(ya, Unindexable) + return merge_to_unindexable(ya, xa) + else + return true # handle conflicting case conservatively + end + else + xa = xa::Unindexable + if isa(ya, IndexableFields) + return merge_to_unindexable(xa, ya) + elseif isa(ya, IndexableElements) + return merge_to_unindexable(xa, ya) + else + ya = ya::Unindexable + xinfo, yinfo = xa.info, ya.info + info = xinfo ∪ yinfo + return Unindexable(info) + end + end +end + +const AliasSet = IntDisjointSet{Int} + +const ArrayInfo = IdDict{Int,Vector{Int}} + +""" + estate::EscapeState + +Extended lattice that maps arguments and SSA values to escape information represented as [`EscapeInfo`](@ref). +Escape information imposed on SSA IR element `x` can be retrieved by `estate[x]`. +""" +struct EscapeState + escapes::Vector{EscapeInfo} + aliasset::AliasSet + nargs::Int + arrayinfo::Union{Nothing,ArrayInfo} +end +function EscapeState(nargs::Int, nstmts::Int, arrayinfo::Union{Nothing,ArrayInfo}) + escapes = EscapeInfo[ + 1 ≤ i ≤ nargs ? ArgEscape() : ⊥ for i in 1:(nargs+nstmts)] + aliasset = AliasSet(nargs+nstmts) + return EscapeState(escapes, aliasset, nargs, arrayinfo) +end +function getindex(estate::EscapeState, @nospecialize(x)) + xidx = iridx(x, estate) + return xidx === nothing ? nothing : estate.escapes[xidx] +end +function setindex!(estate::EscapeState, v::EscapeInfo, @nospecialize(x)) + xidx = iridx(x, estate) + if xidx !== nothing + estate.escapes[xidx] = v + end + return estate +end + +""" + iridx(x, estate::EscapeState) -> xidx::Union{Int,Nothing} + +Tries to convert analyzable IR element `x::Union{Argument,SSAValue}` to +its unique identifier number `xidx` that is valid in the analysis context of `estate`. +Returns `nothing` if `x` isn't maintained by `estate` and thus unanalyzable (e.g. `x::GlobalRef`). + +`irval` is the inverse function of `iridx` (not formally), i.e. +`irval(iridx(x::Union{Argument,SSAValue}, state), state) === x`. +""" +function iridx(@nospecialize(x), estate::EscapeState) + if isa(x, Argument) + xidx = x.n + @assert 1 ≤ xidx ≤ estate.nargs "invalid Argument" + elseif isa(x, SSAValue) + xidx = x.id + estate.nargs + else + return nothing + end + return xidx +end + +""" + irval(xidx::Int, estate::EscapeState) -> x::Union{Argument,SSAValue} + +Converts its unique identifier number `xidx` to the original IR element `x::Union{Argument,SSAValue}` +that is analyzable in the context of `estate`. + +`iridx` is the inverse function of `irval` (not formally), i.e. +`iridx(irval(xidx, state), state) === xidx`. +""" +function irval(xidx::Int, estate::EscapeState) + x = xidx > estate.nargs ? SSAValue(xidx-estate.nargs) : Argument(xidx) + return x +end + +function getaliases(x::Union{Argument,SSAValue}, estate::EscapeState) + xidx = iridx(x, estate) + aliases = getaliases(xidx, estate) + aliases === nothing && return nothing + return Union{Argument,SSAValue}[irval(aidx, estate) for aidx in aliases] +end +function getaliases(xidx::Int, estate::EscapeState) + aliasset = estate.aliasset + root = find_root!(aliasset, xidx) + if xidx ≠ root || aliasset.ranks[xidx] > 0 + # the size of this alias set containing `key` is larger than 1, + # collect the entire alias set + aliases = Int[] + for aidx in 1:length(aliasset.parents) + if aliasset.parents[aidx] == root + push!(aliases, aidx) + end + end + return aliases + else + return nothing + end +end + +isaliased(x::Union{Argument,SSAValue}, y::Union{Argument,SSAValue}, estate::EscapeState) = + isaliased(iridx(x, estate), iridx(y, estate), estate) +isaliased(xidx::Int, yidx::Int, estate::EscapeState) = + in_same_set(estate.aliasset, xidx, yidx) + +struct ArgEscapeInfo + EscapeBits::UInt8 +end +function ArgEscapeInfo(x::EscapeInfo) + x === ⊤ && return ArgEscapeInfo(ARG_ALL_ESCAPE) + EscapeBits = 0x00 + has_return_escape(x) && (EscapeBits |= ARG_RETURN_ESCAPE) + has_thrown_escape(x) && (EscapeBits |= ARG_THROWN_ESCAPE) + return ArgEscapeInfo(EscapeBits) +end + +const ARG_ALL_ESCAPE = 0x01 << 0 +const ARG_RETURN_ESCAPE = 0x01 << 1 +const ARG_THROWN_ESCAPE = 0x01 << 2 + +has_no_escape(x::ArgEscapeInfo) = !has_all_escape(x) && !has_return_escape(x) && !has_thrown_escape(x) +has_all_escape(x::ArgEscapeInfo) = x.EscapeBits & ARG_ALL_ESCAPE ≠ 0 +has_return_escape(x::ArgEscapeInfo) = x.EscapeBits & ARG_RETURN_ESCAPE ≠ 0 +has_thrown_escape(x::ArgEscapeInfo) = x.EscapeBits & ARG_THROWN_ESCAPE ≠ 0 + +struct ArgAliasing + aidx::Int + bidx::Int +end + +struct ArgEscapeCache + argescapes::Vector{ArgEscapeInfo} + argaliases::Vector{ArgAliasing} +end + +function ArgEscapeCache(estate::EscapeState) + nargs = estate.nargs + argescapes = Vector{ArgEscapeInfo}(undef, nargs) + argaliases = ArgAliasing[] + for i = 1:nargs + info = estate.escapes[i] + @assert info.AliasInfo === true + argescapes[i] = ArgEscapeInfo(info) + for j = (i+1):nargs + if isaliased(i, j, estate) + push!(argaliases, ArgAliasing(i, j)) + end + end + end + return ArgEscapeCache(argescapes, argaliases) +end + +""" + is_ipo_profitable(ir::IRCode, nargs::Int) -> Bool + +Heuristically checks if there is any profitability to run the escape analysis on `ir` +and generate IPO escape information cache. Specifically, this function examines +if any call argument is "interesting" in terms of their escapability. +""" +function is_ipo_profitable(ir::IRCode, nargs::Int) + for i = 1:nargs + t = unwrap_unionall(widenconst(ir.argtypes[i])) + t <: IO && return false # bail out IO-related functions + is_ipo_profitable_type(t) && return true + end + return false +end +function is_ipo_profitable_type(@nospecialize t) + if isa(t, Union) + return is_ipo_profitable_type(t.a) && is_ipo_profitable_type(t.b) + end + (t === String || t === Symbol || t === Module || t === SimpleVector) && return false + return ismutabletype(t) +end + +abstract type Change end +struct EscapeChange <: Change + xidx::Int + xinfo::EscapeInfo +end +struct AliasChange <: Change + xidx::Int + yidx::Int +end +struct ArgAliasChange <: Change + xidx::Int + yidx::Int +end +struct LivenessChange <: Change + xidx::Int + livepc::Int +end +const Changes = Vector{Change} + +struct AnalysisState{T<:Callable} + ir::IRCode + estate::EscapeState + changes::Changes + get_escape_cache::T +end + +function getinst(ir::IRCode, idx::Int) + nstmts = length(ir.stmts) + if idx ≤ nstmts + return ir.stmts[idx] + else + return ir.new_nodes.stmts[idx - nstmts] + end +end + +""" + analyze_escapes(ir::IRCode, nargs::Int, call_resolved::Bool, get_escape_cache::Callable) + -> estate::EscapeState + +Analyzes escape information in `ir`: +- `nargs`: the number of actual arguments of the analyzed call +- `call_resolved`: if interprocedural calls are already resolved by `ssa_inlining_pass!` +- `get_escape_cache(::Union{InferenceResult,MethodInstance}) -> Union{Nothing,ArgEscapeCache}`: + retrieves cached argument escape information +""" +function analyze_escapes(ir::IRCode, nargs::Int, call_resolved::Bool, get_escape_cache::T) where T<:Callable + stmts = ir.stmts + nstmts = length(stmts) + length(ir.new_nodes.stmts) + + tryregions, arrayinfo, callinfo = compute_frameinfo(ir, call_resolved) + estate = EscapeState(nargs, nstmts, arrayinfo) + changes = Changes() # keeps changes that happen at current statement + astate = AnalysisState(ir, estate, changes, get_escape_cache) + + local debug_itr_counter = 0 + while true + local anyupdate = false + + for pc in nstmts:-1:1 + stmt = getinst(ir, pc)[:inst] + + # collect escape information + if isa(stmt, Expr) + head = stmt.head + if head === :call + if callinfo !== nothing + escape_call!(astate, pc, stmt.args, callinfo) + else + escape_call!(astate, pc, stmt.args) + end + elseif head === :invoke + escape_invoke!(astate, pc, stmt.args) + elseif head === :new || head === :splatnew + escape_new!(astate, pc, stmt.args) + elseif head === :(=) + lhs, rhs = stmt.args + if isa(lhs, GlobalRef) # global store + add_escape_change!(astate, rhs, ⊤) + else + unexpected_assignment!(ir, pc) + end + elseif head === :foreigncall + escape_foreigncall!(astate, pc, stmt.args) + elseif head === :throw_undef_if_not # XXX when is this expression inserted ? + add_escape_change!(astate, stmt.args[1], ThrownEscape(pc)) + elseif is_meta_expr_head(head) + # meta expressions doesn't account for any usages + continue + elseif head === :enter || head === :leave || head === :the_exception || head === :pop_exception + # ignore these expressions since escapes via exceptions are handled by `escape_exception!` + # `escape_exception!` conservatively propagates `AllEscape` anyway, + # and so escape information imposed on `:the_exception` isn't computed + continue + elseif head === :static_parameter || # this exists statically, not interested in its escape + head === :copyast || # XXX can this account for some escapes? + head === :undefcheck || # XXX can this account for some escapes? + head === :isdefined || # just returns `Bool`, nothing accounts for any escapes + head === :gc_preserve_begin || # `GC.@preserve` expressions themselves won't be used anywhere + head === :gc_preserve_end # `GC.@preserve` expressions themselves won't be used anywhere + continue + else + add_conservative_changes!(astate, pc, stmt.args) + end + elseif isa(stmt, ReturnNode) + if isdefined(stmt, :val) + add_escape_change!(astate, stmt.val, ReturnEscape(pc)) + end + elseif isa(stmt, PhiNode) + escape_edges!(astate, pc, stmt.values) + elseif isa(stmt, PiNode) + escape_val_ifdefined!(astate, pc, stmt) + elseif isa(stmt, PhiCNode) + escape_edges!(astate, pc, stmt.values) + elseif isa(stmt, UpsilonNode) + escape_val_ifdefined!(astate, pc, stmt) + elseif isa(stmt, GlobalRef) # global load + add_escape_change!(astate, SSAValue(pc), ⊤) + elseif isa(stmt, SSAValue) + escape_val!(astate, pc, stmt) + elseif isa(stmt, Argument) + escape_val!(astate, pc, stmt) + else # otherwise `stmt` can be GotoNode, GotoIfNot, and inlined values etc. + continue + end + + isempty(changes) && continue + + anyupdate |= propagate_changes!(estate, changes) + + empty!(changes) + end + + tryregions !== nothing && escape_exception!(astate, tryregions) + + debug_itr_counter += 1 + + anyupdate || break + end + + # if debug_itr_counter > 2 + # println("[EA] excessive iteration count found ", debug_itr_counter, " (", singleton_type(ir.argtypes[1]), ")") + # end + + return estate +end + +""" + compute_frameinfo(ir::IRCode, call_resolved::Bool) -> (tryregions, arrayinfo, callinfo) + +A preparatory linear scan before the escape analysis on `ir` to find: +- `tryregions::Union{Nothing,Vector{UnitRange{Int}}}`: regions in which potential `throw`s can be caught (used by `escape_exception!`) +- `arrayinfo::Union{Nothing,IdDict{Int,Vector{Int}}}`: array allocations whose dimensions are known precisely (with some very simple local analysis) +- `callinfo::`: when `!call_resolved`, `compute_frameinfo` additionally returns `callinfo::Vector{Union{MethodInstance,InferenceResult}}`, + which contains information about statically resolved callsites. + The inliner will use essentially equivalent interprocedural information to inline callees as well as resolve static callsites, + this additional information won't be required when analyzing post-inlining IR. + +!!! note + This array dimension analysis to compute `arrayinfo` is very local and doesn't account + for flow-sensitivity nor complex aliasing. + Ideally this dimension analysis should be done as a part of type inference that + propagates array dimenstions in a flow sensitive way. +""" +function compute_frameinfo(ir::IRCode, call_resolved::Bool) + nstmts, nnewnodes = length(ir.stmts), length(ir.new_nodes.stmts) + tryregions, arrayinfo = nothing, nothing + if !call_resolved + callinfo = Vector{Any}(undef, nstmts+nnewnodes) + else + callinfo = nothing + end + for idx in 1:nstmts+nnewnodes + inst = getinst(ir, idx) + stmt = inst[:inst] + if !call_resolved + # TODO don't call `check_effect_free!` in the inlinear + check_effect_free!(ir, idx, stmt, inst[:type]) + end + if callinfo !== nothing && isexpr(stmt, :call) + callinfo[idx] = resolve_call(ir, stmt, inst[:info]) + elseif isexpr(stmt, :enter) + @assert idx ≤ nstmts "try/catch inside new_nodes unsupported" + tryregions === nothing && (tryregions = UnitRange{Int}[]) + leave_block = stmt.args[1]::Int + leave_pc = first(ir.cfg.blocks[leave_block].stmts) + push!(tryregions, idx:leave_pc) + elseif isexpr(stmt, :foreigncall) + args = stmt.args + name = args[1] + nn = normalize(name) + isa(nn, Symbol) || @goto next_stmt + ndims = alloc_array_ndims(nn) + ndims === nothing && @goto next_stmt + if ndims ≠ 0 + length(args) ≥ ndims+6 || @goto next_stmt + dims = Int[] + for i in 1:ndims + dim = argextype(args[i+6], ir) + isa(dim, Const) || @goto next_stmt + dim = dim.val + isa(dim, Int) || @goto next_stmt + push!(dims, dim) + end + else + length(args) ≥ 7 || @goto next_stmt + dims = argextype(args[7], ir) + if isa(dims, Const) + dims = dims.val + isa(dims, Tuple{Vararg{Int}}) || @goto next_stmt + dims = collect(Int, dims) + else + dims === Tuple{} || @goto next_stmt + dims = Int[] + end + end + if arrayinfo === nothing + arrayinfo = ArrayInfo() + end + arrayinfo[idx] = dims + elseif arrayinfo !== nothing + # TODO this super limited alias analysis is able to handle only very simple cases + # this should be replaced with a proper forward dimension analysis + if isa(stmt, PhiNode) + values = stmt.values + local dims = nothing + for i = 1:length(values) + if isassigned(values, i) + val = values[i] + if isa(val, SSAValue) && haskey(arrayinfo, val.id) + if dims === nothing + dims = arrayinfo[val.id] + continue + elseif dims == arrayinfo[val.id] + continue + end + end + end + @goto next_stmt + end + if dims !== nothing + arrayinfo[idx] = dims + end + elseif isa(stmt, PiNode) + if isdefined(stmt, :val) + val = stmt.val + if isa(val, SSAValue) && haskey(arrayinfo, val.id) + arrayinfo[idx] = arrayinfo[val.id] + end + end + end + end + @label next_stmt + end + return tryregions, arrayinfo, callinfo +end + +# define resolve_call +if _TOP_MOD === Core.Compiler + include("compiler/ssair/EscapeAnalysis/interprocedural.jl") +else + include("interprocedural.jl") +end + +# propagate changes, and check convergence +function propagate_changes!(estate::EscapeState, changes::Changes) + local anychanged = false + for change in changes + if isa(change, EscapeChange) + anychanged |= propagate_escape_change!(estate, change) + elseif isa(change, LivenessChange) + anychanged |= propagate_liveness_change!(estate, change) + else + change = change::AliasChange + anychanged |= propagate_alias_change!(estate, change) + end + end + return anychanged +end + +@inline propagate_escape_change!(estate::EscapeState, change::EscapeChange) = + propagate_escape_change!(⊔ₑ, estate, change) + +# allows this to work as lattice join as well as lattice meet +@inline function propagate_escape_change!(@specialize(op), + estate::EscapeState, change::EscapeChange) + (; xidx, xinfo) = change + anychanged = _propagate_escape_change!(op, estate, xidx, xinfo) + # COMBAK is there a more efficient method of escape information equalization on aliasset? + aliases = getaliases(xidx, estate) + if aliases !== nothing + for aidx in aliases + anychanged |= _propagate_escape_change!(op, estate, aidx, xinfo) + end + end + return anychanged +end + +@inline function _propagate_escape_change!(@specialize(op), + estate::EscapeState, xidx::Int, info::EscapeInfo) + old = estate.escapes[xidx] + new = op(old, info) + if old ≠ new + estate.escapes[xidx] = new + return true + end + return false +end + +# propagate Liveness changes separately in order to avoid constructing too many LivenessSet +@inline function propagate_liveness_change!(estate::EscapeState, change::LivenessChange) + (; xidx, livepc) = change + info = estate.escapes[xidx] + Liveness = info.Liveness + Liveness === TOP_LIVENESS && return false + livepc in Liveness && return false + if Liveness === BOT_LIVENESS || Liveness === ARG_LIVENESS + # if this Liveness is a constant, we shouldn't modify it and propagate this change as a new EscapeInfo + Liveness = copy(Liveness) + push!(Liveness, livepc) + estate.escapes[xidx] = EscapeInfo(info; Liveness) + return true + else + # directly modify Liveness property in order to avoid excessive copies + push!(Liveness, livepc) + return true + end +end + +@inline function propagate_alias_change!(estate::EscapeState, change::AliasChange) + anychange = false + (; xidx, yidx) = change + aliasset = estate.aliasset + xroot = find_root!(aliasset, xidx) + yroot = find_root!(aliasset, yidx) + if xroot ≠ yroot + union!(aliasset, xroot, yroot) + return true + end + return false +end + +function add_escape_change!(astate::AnalysisState, @nospecialize(x), xinfo::EscapeInfo, + force::Bool = false) + xinfo === ⊥ && return nothing # performance optimization + xidx = iridx(x, astate.estate) + if xidx !== nothing + if force || !isbitstype(widenconst(argextype(x, astate.ir))) + push!(astate.changes, EscapeChange(xidx, xinfo)) + end + end + return nothing +end + +function add_liveness_change!(astate::AnalysisState, @nospecialize(x), livepc::Int) + xidx = iridx(x, astate.estate) + if xidx !== nothing + if !isbitstype(widenconst(argextype(x, astate.ir))) + push!(astate.changes, LivenessChange(xidx, livepc)) + end + end + return nothing +end + +function add_alias_change!(astate::AnalysisState, @nospecialize(x), @nospecialize(y)) + if isa(x, GlobalRef) + return add_escape_change!(astate, y, ⊤) + elseif isa(y, GlobalRef) + return add_escape_change!(astate, x, ⊤) + end + estate = astate.estate + xidx = iridx(x, estate) + yidx = iridx(y, estate) + if xidx !== nothing && yidx !== nothing + if !isaliased(xidx, yidx, astate.estate) + pushfirst!(astate.changes, AliasChange(xidx, yidx)) + end + # add new escape change here so that it's shared among the expanded `aliasset` in `propagate_escape_change!` + xinfo = estate.escapes[xidx] + yinfo = estate.escapes[yidx] + add_escape_change!(astate, x, xinfo ⊔ₑ yinfo, #=force=#true) + end + return nothing +end + +struct LocalDef + idx::Int +end +struct LocalUse + idx::Int +end + +function add_alias_escapes!(astate::AnalysisState, @nospecialize(v), ainfo::AInfo) + estate = astate.estate + for x in ainfo + isa(x, LocalUse) || continue # ignore def + x = SSAValue(x.idx) # obviously this won't be true once we implement interprocedural AliasInfo + add_alias_change!(astate, v, x) + end +end + +function add_thrown_escapes!(astate::AnalysisState, pc::Int, args::Vector{Any}, + first_idx::Int = 1, last_idx::Int = length(args)) + info = ThrownEscape(pc) + for i in first_idx:last_idx + add_escape_change!(astate, args[i], info) + end +end + +function add_liveness_changes!(astate::AnalysisState, pc::Int, args::Vector{Any}, + first_idx::Int = 1, last_idx::Int = length(args)) + for i in first_idx:last_idx + arg = args[i] + add_liveness_change!(astate, arg, pc) + end +end + +function add_fallback_changes!(astate::AnalysisState, pc::Int, args::Vector{Any}, + first_idx::Int = 1, last_idx::Int = length(args)) + info = ThrownEscape(pc) + for i in first_idx:last_idx + arg = args[i] + add_escape_change!(astate, arg, info) + add_liveness_change!(astate, arg, pc) + end +end + +function add_conservative_changes!(astate::AnalysisState, pc::Int, args::Vector{Any}, + first_idx::Int = 1, last_idx::Int = length(args)) + for i in first_idx:last_idx + add_escape_change!(astate, args[i], ⊤) + end + add_escape_change!(astate, SSAValue(pc), ⊤) # it may return GlobalRef etc. + return nothing +end + +function escape_edges!(astate::AnalysisState, pc::Int, edges::Vector{Any}) + ret = SSAValue(pc) + for i in 1:length(edges) + if isassigned(edges, i) + v = edges[i] + add_alias_change!(astate, ret, v) + end + end +end + +function escape_val_ifdefined!(astate::AnalysisState, pc::Int, x) + if isdefined(x, :val) + escape_val!(astate, pc, x.val) + end +end + +function escape_val!(astate::AnalysisState, pc::Int, @nospecialize(val)) + ret = SSAValue(pc) + add_alias_change!(astate, ret, val) +end + +function escape_unanalyzable_obj!(astate::AnalysisState, @nospecialize(obj), objinfo::EscapeInfo) + objinfo = EscapeInfo(objinfo, true) + add_escape_change!(astate, obj, objinfo) + return objinfo +end + +@noinline function unexpected_assignment!(ir::IRCode, pc::Int) + @eval Main (ir = $ir; pc = $pc) + error("unexpected assignment found: inspect `Main.pc` and `Main.pc`") +end + +is_effect_free(ir::IRCode, pc::Int) = getinst(ir, pc)[:flag] & IR_FLAG_EFFECT_FREE ≠ 0 + +# NOTE if we don't maintain the alias set that is separated from the lattice state, we can do +# something like below: it essentially incorporates forward escape propagation in our default +# backward propagation, and leads to inefficient convergence that requires more iterations +# # lhs = rhs: propagate escape information of `rhs` to `lhs` +# function escape_alias!(astate::AnalysisState, @nospecialize(lhs), @nospecialize(rhs)) +# if isa(rhs, SSAValue) || isa(rhs, Argument) +# vinfo = astate.estate[rhs] +# else +# return +# end +# add_escape_change!(astate, lhs, vinfo) +# end + +""" + escape_exception!(astate::AnalysisState, tryregions::Vector{UnitRange{Int}}) + +Propagates escapes via exceptions that can happen in `tryregions`. + +Naively it seems enough to propagate escape information imposed on `:the_exception` object, +but actually there are several other ways to access to the exception object such as +`Base.current_exceptions` and manual catch of `rethrow`n object. +For example, escape analysis needs to account for potential escape of the allocated object +via `rethrow_escape!()` call in the example below: +```julia +const Gx = Ref{Any}() +@noinline function rethrow_escape!() + try + rethrow() + catch err + Gx[] = err + end +end +unsafeget(x) = isassigned(x) ? x[] : throw(x) + +code_escapes() do + r = Ref{String}() + try + t = unsafeget(r) + catch err + t = typeof(err) # `err` (which `r` may alias to) doesn't escape here + rethrow_escape!() # `r` can escape here + end + return t +end +``` + +As indicated by the above example, it requires a global analysis in addition to a base escape +analysis to reason about all possible escapes via existing exception interfaces correctly. +For now we conservatively always propagate `AllEscape` to all potentially thrown objects, +since such an additional analysis might not be worthwhile to do given that exception handlings +and error paths usually don't need to be very performance sensitive, and optimizations of +error paths might be very ineffective anyway since they are sometimes "unoptimized" +intentionally for latency reasons. +""" +function escape_exception!(astate::AnalysisState, tryregions::Vector{UnitRange{Int}}) + estate = astate.estate + # NOTE if `:the_exception` is the only way to access the exception, we can do: + # exc = SSAValue(pc) + # excinfo = estate[exc] + excinfo = ⊤ + escapes = estate.escapes + for i in 1:length(escapes) + x = escapes[i] + xt = x.ThrownEscape + xt === TOP_THROWN_ESCAPE && @goto propagate_exception_escape # fast pass + for pc in xt + for region in tryregions + pc in region && @goto propagate_exception_escape # early break because of AllEscape + end + end + continue + @label propagate_exception_escape + xval = irval(i, estate) + add_escape_change!(astate, xval, excinfo) + end +end + +# escape statically-resolved call, i.e. `Expr(:invoke, ::MethodInstance, ...)` +escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}) = + escape_invoke!(astate, pc, args, first(args)::MethodInstance, 2) + +function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}, + linfo::Linfo, first_idx::Int, last_idx::Int = length(args)) + if isa(linfo, InferenceResult) + cache = astate.get_escape_cache(linfo) + linfo = linfo.linfo + else + cache = astate.get_escape_cache(linfo) + end + if cache === nothing + return add_conservative_changes!(astate, pc, args, 2) + else + cache = cache::ArgEscapeCache + end + ret = SSAValue(pc) + retinfo = astate.estate[ret] # escape information imposed on the call statement + method = linfo.def::Method + nargs = Int(method.nargs) + for (i, argidx) in enumerate(first_idx:last_idx) + arg = args[argidx] + if i > nargs + # handle isva signature + # COMBAK will this be invalid once we take alias information into account? + i = nargs + end + arginfo = cache.argescapes[i] + info = from_interprocedural(arginfo, pc) + if has_return_escape(arginfo) + # if this argument can be "returned", in addition to propagating + # the escape information imposed on this call argument within the callee, + # we should also account for possible aliasing of this argument and the returned value + add_escape_change!(astate, arg, info) + add_alias_change!(astate, ret, arg) + else + # if this is simply passed as the call argument, we can just propagate + # the escape information imposed on this call argument within the callee + add_escape_change!(astate, arg, info) + end + end + for (; aidx, bidx) in cache.argaliases + add_alias_change!(astate, args[aidx-(first_idx-1)], args[bidx-(first_idx-1)]) + end + # we should disable the alias analysis on this newly introduced object + add_escape_change!(astate, ret, EscapeInfo(retinfo, true)) +end + +""" + from_interprocedural(arginfo::ArgEscapeInfo, pc::Int) -> x::EscapeInfo + +Reinterprets the escape information imposed on the call argument which is cached as `arginfo` +in the context of the caller frame, where `pc` is the SSA statement number of the return value. +""" +function from_interprocedural(arginfo::ArgEscapeInfo, pc::Int) + has_all_escape(arginfo) && return ⊤ + + ThrownEscape = has_thrown_escape(arginfo) ? LivenessSet(pc) : BOT_THROWN_ESCAPE + + return EscapeInfo( + #=Analyzed=#true, #=ReturnEscape=#false, ThrownEscape, + # FIXME implement interprocedural memory effect-analysis + # currently, this essentially disables the entire field analysis + # it might be okay from the SROA point of view, since we can't remove the allocation + # as far as it's passed to a callee anyway, but still we may want some field analysis + # for e.g. stack allocation or some other IPO optimizations + #=AliasInfo=#true, #=Liveness=#LivenessSet(pc)) +end + +# escape every argument `(args[6:length(args[3])])` and the name `args[1]` +# TODO: we can apply a similar strategy like builtin calls to specialize some foreigncalls +function escape_foreigncall!(astate::AnalysisState, pc::Int, args::Vector{Any}) + nargs = length(args) + if nargs < 6 + # invalid foreigncall, just escape everything + add_conservative_changes!(astate, pc, args) + return + end + argtypes = args[3]::SimpleVector + nargs = length(argtypes) + name = args[1] + nn = normalize(name) + if isa(nn, Symbol) + boundserror_ninds = array_resize_info(nn) + if boundserror_ninds !== nothing + boundserror, ninds = boundserror_ninds + escape_array_resize!(boundserror, ninds, astate, pc, args) + return + end + if is_array_copy(nn) + escape_array_copy!(astate, pc, args) + return + elseif is_array_isassigned(nn) + escape_array_isassigned!(astate, pc, args) + return + end + # if nn === :jl_gc_add_finalizer_th + # # TODO add `FinalizerEscape` ? + # end + end + # NOTE array allocations might have been proven as nothrow (https://github.com/JuliaLang/julia/pull/43565) + nothrow = is_effect_free(astate.ir, pc) + name_info = nothrow ? ⊥ : ThrownEscape(pc) + add_escape_change!(astate, name, name_info) + add_liveness_change!(astate, name, pc) + for i = 1:nargs + # we should escape this argument if it is directly called, + # otherwise just impose ThrownEscape if not nothrow + if argtypes[i] === Any + arg_info = ⊤ + else + arg_info = nothrow ? ⊥ : ThrownEscape(pc) + end + add_escape_change!(astate, args[5+i], arg_info) + add_liveness_change!(astate, args[5+i], pc) + end + for i = (5+nargs):length(args) + arg = args[i] + add_escape_change!(astate, arg, ⊥) + add_liveness_change!(astate, arg, pc) + end +end + +normalize(@nospecialize x) = isa(x, QuoteNode) ? x.value : x + +function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any}, callinfo::Vector{Any}) + info = callinfo[pc] + if isa(info, Bool) + info && return # known to be no escape + # now cascade to the builtin handling + escape_call!(astate, pc, args) + return + elseif isa(info, CallInfo) + for linfo in info.linfos + escape_invoke!(astate, pc, args, linfo, 1) + end + # accounts for a potential escape via MethodError + info.nothrow || add_thrown_escapes!(astate, pc, args) + return + else + @assert info === missing + # if this call couldn't be analyzed, escape it conservatively + add_conservative_changes!(astate, pc, args) + end +end + +function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any}) + ir = astate.ir + ft = argextype(first(args), ir, ir.sptypes, ir.argtypes) + f = singleton_type(ft) + if isa(f, Core.IntrinsicFunction) + # XXX somehow `:call` expression can creep in here, ideally we should be able to do: + # argtypes = Any[argextype(args[i], astate.ir) for i = 2:length(args)] + argtypes = Any[] + for i = 2:length(args) + arg = args[i] + push!(argtypes, isexpr(arg, :call) ? Any : argextype(arg, ir)) + end + if intrinsic_nothrow(f, argtypes) + add_liveness_changes!(astate, pc, args, 2) + else + add_fallback_changes!(astate, pc, args, 2) + end + return # TODO accounts for pointer operations? + end + result = escape_builtin!(f, astate, pc, args) + if result === missing + # if this call hasn't been handled by any of pre-defined handlers, escape it conservatively + add_conservative_changes!(astate, pc, args) + return + elseif result === true + add_liveness_changes!(astate, pc, args, 2) + return # ThrownEscape is already checked + else + # we escape statements with the `ThrownEscape` property using the effect-freeness + # computed by `stmt_effect_free` invoked within inlining + # TODO throwness ≠ "effect-free-ness" + if is_effect_free(astate.ir, pc) + add_liveness_changes!(astate, pc, args, 2) + else + add_fallback_changes!(astate, pc, args, 2) + end + return + end +end + +escape_builtin!(@nospecialize(f), _...) = return missing + +# safe builtins +escape_builtin!(::typeof(isa), _...) = return false +escape_builtin!(::typeof(typeof), _...) = return false +escape_builtin!(::typeof(sizeof), _...) = return false +escape_builtin!(::typeof(===), _...) = return false +# not really safe, but `ThrownEscape` will be imposed later +escape_builtin!(::typeof(isdefined), _...) = return false +escape_builtin!(::typeof(throw), _...) = return false + +function escape_builtin!(::typeof(ifelse), astate::AnalysisState, pc::Int, args::Vector{Any}) + length(args) == 4 || return false + f, cond, th, el = args + ret = SSAValue(pc) + condt = argextype(cond, astate.ir) + if isa(condt, Const) && (cond = condt.val; isa(cond, Bool)) + if cond + add_alias_change!(astate, th, ret) + else + add_alias_change!(astate, el, ret) + end + else + add_alias_change!(astate, th, ret) + add_alias_change!(astate, el, ret) + end + return false +end + +function escape_builtin!(::typeof(typeassert), astate::AnalysisState, pc::Int, args::Vector{Any}) + length(args) == 3 || return false + f, obj, typ = args + ret = SSAValue(pc) + add_alias_change!(astate, ret, obj) + return false +end + +function escape_new!(astate::AnalysisState, pc::Int, args::Vector{Any}) + obj = SSAValue(pc) + objinfo = astate.estate[obj] + AliasInfo = objinfo.AliasInfo + nargs = length(args) + if isa(AliasInfo, Bool) + AliasInfo && @goto conservative_propagation + # AliasInfo of this object hasn't been analyzed yet: set AliasInfo now + typ = widenconst(argextype(obj, astate.ir)) + nflds = fieldcount_noerror(typ) + if nflds === nothing + AliasInfo = Unindexable() + @goto escape_unindexable_def + else + AliasInfo = IndexableFields(nflds) + @goto escape_indexable_def + end + elseif isa(AliasInfo, IndexableFields) + @label escape_indexable_def + # fields are known precisely: propagate escape information imposed on recorded possibilities to the exact field values + infos = AliasInfo.infos + nf = length(infos) + objinfo′ = ignore_aliasinfo(objinfo) + for i in 2:nargs + i-1 > nf && break # may happen when e.g. ϕ-node merges values with different types + arg = args[i] + add_alias_escapes!(astate, arg, infos[i-1]) + push!(infos[i-1], LocalDef(pc)) + # propagate the escape information of this object ignoring field information + add_escape_change!(astate, arg, objinfo′) + add_liveness_change!(astate, arg, pc) + end + add_escape_change!(astate, obj, EscapeInfo(objinfo, AliasInfo)) # update with new AliasInfo + elseif isa(AliasInfo, Unindexable) + @label escape_unindexable_def + # fields are known partially: propagate escape information imposed on recorded possibilities to all fields values + info = AliasInfo.info + objinfo′ = ignore_aliasinfo(objinfo) + for i in 2:nargs + arg = args[i] + add_alias_escapes!(astate, arg, info) + push!(info, LocalDef(pc)) + # propagate the escape information of this object ignoring field information + add_escape_change!(astate, arg, objinfo′) + add_liveness_change!(astate, arg, pc) + end + add_escape_change!(astate, obj, EscapeInfo(objinfo, AliasInfo)) # update with new AliasInfo + else + # this object has been used as array, but it is allocated as struct here (i.e. should throw) + # update obj's field information and just handle this case conservatively + objinfo = escape_unanalyzable_obj!(astate, obj, objinfo) + @label conservative_propagation + # the fields couldn't be analyzed precisely: propagate the entire escape information + # of this object to all its fields as the most conservative propagation + for i in 2:nargs + arg = args[i] + add_escape_change!(astate, arg, objinfo) + add_liveness_change!(astate, arg, pc) + end + end + if !is_effect_free(astate.ir, pc) + add_thrown_escapes!(astate, pc, args) + end +end + +function escape_builtin!(::typeof(tuple), astate::AnalysisState, pc::Int, args::Vector{Any}) + escape_new!(astate, pc, args) + return false +end + +function analyze_fields(ir::IRCode, @nospecialize(typ), @nospecialize(fld)) + nflds = fieldcount_noerror(typ) + if nflds === nothing + return Unindexable(), 0 + end + if isa(typ, DataType) + fldval = try_compute_field(ir, fld) + fidx = try_compute_fieldidx(typ, fldval) + else + fidx = nothing + end + if fidx === nothing + return Unindexable(), 0 + end + return IndexableFields(nflds), fidx +end + +function reanalyze_fields(ir::IRCode, AliasInfo::IndexableFields, @nospecialize(typ), @nospecialize(fld)) + nflds = fieldcount_noerror(typ) + if nflds === nothing + return merge_to_unindexable(AliasInfo), 0 + end + if isa(typ, DataType) + fldval = try_compute_field(ir, fld) + fidx = try_compute_fieldidx(typ, fldval) + else + fidx = nothing + end + if fidx === nothing + return merge_to_unindexable(AliasInfo), 0 + end + infos = AliasInfo.infos + ninfos = length(infos) + if nflds > ninfos + for _ in 1:(nflds-ninfos) + push!(infos, AInfo()) + end + end + return AliasInfo, fidx +end + +function escape_builtin!(::typeof(getfield), astate::AnalysisState, pc::Int, args::Vector{Any}) + length(args) ≥ 3 || return false + ir, estate = astate.ir, astate.estate + obj = args[2] + typ = widenconst(argextype(obj, ir)) + if hasintersect(typ, Module) # global load + add_escape_change!(astate, SSAValue(pc), ⊤) + end + if isa(obj, SSAValue) || isa(obj, Argument) + objinfo = estate[obj] + else + return false + end + AliasInfo = objinfo.AliasInfo + if isa(AliasInfo, Bool) + AliasInfo && @goto conservative_propagation + # AliasInfo of this object hasn't been analyzed yet: set AliasInfo now + AliasInfo, fidx = analyze_fields(ir, typ, args[3]) + if isa(AliasInfo, IndexableFields) + @goto record_indexable_use + else + @goto record_unindexable_use + end + elseif isa(AliasInfo, IndexableFields) + AliasInfo, fidx = reanalyze_fields(ir, AliasInfo, typ, args[3]) + isa(AliasInfo, Unindexable) && @goto record_unindexable_use + @label record_indexable_use + push!(AliasInfo.infos[fidx], LocalUse(pc)) + add_escape_change!(astate, obj, EscapeInfo(objinfo, AliasInfo)) # update with new AliasInfo + elseif isa(AliasInfo, Unindexable) + @label record_unindexable_use + push!(AliasInfo.info, LocalUse(pc)) + add_escape_change!(astate, obj, EscapeInfo(objinfo, AliasInfo)) # update with new AliasInfo + else + # this object has been used as array, but it is used as struct here (i.e. should throw) + # update obj's field information and just handle this case conservatively + objinfo = escape_unanalyzable_obj!(astate, obj, objinfo) + @label conservative_propagation + # at the extreme case, a field of `obj` may point to `obj` itself + # so add the alias change here as the most conservative propagation + add_alias_change!(astate, obj, SSAValue(pc)) + end + return false +end + +function escape_builtin!(::typeof(setfield!), astate::AnalysisState, pc::Int, args::Vector{Any}) + length(args) ≥ 4 || return false + ir, estate = astate.ir, astate.estate + obj = args[2] + val = args[4] + if isa(obj, SSAValue) || isa(obj, Argument) + objinfo = estate[obj] + else + # unanalyzable object (e.g. obj::GlobalRef): escape field value conservatively + add_escape_change!(astate, val, ⊤) + @goto add_thrown_escapes + end + AliasInfo = objinfo.AliasInfo + if isa(AliasInfo, Bool) + AliasInfo && @goto conservative_propagation + # AliasInfo of this object hasn't been analyzed yet: set AliasInfo now + typ = widenconst(argextype(obj, ir)) + AliasInfo, fidx = analyze_fields(ir, typ, args[3]) + if isa(AliasInfo, IndexableFields) + @goto escape_indexable_def + else + @goto escape_unindexable_def + end + elseif isa(AliasInfo, IndexableFields) + typ = widenconst(argextype(obj, ir)) + AliasInfo, fidx = reanalyze_fields(ir, AliasInfo, typ, args[3]) + isa(AliasInfo, Unindexable) && @goto escape_unindexable_def + @label escape_indexable_def + add_alias_escapes!(astate, val, AliasInfo.infos[fidx]) + push!(AliasInfo.infos[fidx], LocalDef(pc)) + objinfo = EscapeInfo(objinfo, AliasInfo) + add_escape_change!(astate, obj, objinfo) # update with new AliasInfo + # propagate the escape information of this object ignoring field information + add_escape_change!(astate, val, ignore_aliasinfo(objinfo)) + elseif isa(AliasInfo, Unindexable) + info = AliasInfo.info + @label escape_unindexable_def + add_alias_escapes!(astate, val, AliasInfo.info) + push!(AliasInfo.info, LocalDef(pc)) + objinfo = EscapeInfo(objinfo, AliasInfo) + add_escape_change!(astate, obj, objinfo) # update with new AliasInfo + # propagate the escape information of this object ignoring field information + add_escape_change!(astate, val, ignore_aliasinfo(objinfo)) + else + # this object has been used as array, but it is used as struct here (i.e. should throw) + # update obj's field information and just handle this case conservatively + objinfo = escape_unanalyzable_obj!(astate, obj, objinfo) + @label conservative_propagation + # the field couldn't be analyzed: alias this object to the value being assigned + # as the most conservative propagation (as required for ArgAliasing) + add_alias_change!(astate, val, obj) + end + # also propagate escape information imposed on the return value of this `setfield!` + ssainfo = estate[SSAValue(pc)] + add_escape_change!(astate, val, ssainfo) + # compute the throwness of this setfield! call here since builtin_nothrow doesn't account for that + @label add_thrown_escapes + argtypes = Any[] + for i = 2:length(args) + push!(argtypes, argextype(args[i], ir)) + end + setfield!_nothrow(argtypes) || add_thrown_escapes!(astate, pc, args, 2) + return true +end + +function escape_builtin!(::typeof(arrayref), astate::AnalysisState, pc::Int, args::Vector{Any}) + length(args) ≥ 4 || return false + # check potential thrown escapes from this arrayref call + argtypes = Any[argextype(args[i], astate.ir) for i in 2:length(args)] + boundcheckt = argtypes[1] + aryt = argtypes[2] + if !array_builtin_common_typecheck(boundcheckt, aryt, argtypes, 3) + add_thrown_escapes!(astate, pc, args, 2) + end + ary = args[3] + inbounds = isa(boundcheckt, Const) && !boundcheckt.val::Bool + inbounds || add_escape_change!(astate, ary, ThrownEscape(pc)) + # we don't track precise index information about this array and thus don't know what values + # can be referenced here: directly propagate the escape information imposed on the return + # value of this `arrayref` call to the array itself as the most conservative propagation + # but also with updated index information + estate = astate.estate + if isa(ary, SSAValue) || isa(ary, Argument) + aryinfo = estate[ary] + else + return true + end + AliasInfo = aryinfo.AliasInfo + if isa(AliasInfo, Bool) + AliasInfo && @goto conservative_propagation + # AliasInfo of this array hasn't been analyzed yet: set AliasInfo now + idx = array_nd_index(astate, ary, args[4:end]) + if isa(idx, Int) + AliasInfo = IndexableElements(IdDict{Int,AInfo}()) + @goto record_indexable_use + end + AliasInfo = Unindexable() + @goto record_unindexable_use + elseif isa(AliasInfo, IndexableElements) + idx = array_nd_index(astate, ary, args[4:end]) + if !isa(idx, Int) + AliasInfo = merge_to_unindexable(AliasInfo) + @goto record_unindexable_use + end + @label record_indexable_use + info = get!(()->AInfo(), AliasInfo.infos, idx) + push!(info, LocalUse(pc)) + add_escape_change!(astate, ary, EscapeInfo(aryinfo, AliasInfo)) # update with new AliasInfo + elseif isa(AliasInfo, Unindexable) + @label record_unindexable_use + push!(AliasInfo.info, LocalUse(pc)) + add_escape_change!(astate, ary, EscapeInfo(aryinfo, AliasInfo)) # update with new AliasInfo + else + # this object has been used as struct, but it is used as array here (thus should throw) + # update ary's element information and just handle this case conservatively + aryinfo = escape_unanalyzable_obj!(astate, ary, aryinfo) + @label conservative_propagation + # at the extreme case, an element of `ary` may point to `ary` itself + # so add the alias change here as the most conservative propagation + add_alias_change!(astate, ary, SSAValue(pc)) + end + return true +end + +function escape_builtin!(::typeof(arrayset), astate::AnalysisState, pc::Int, args::Vector{Any}) + length(args) ≥ 5 || return false + # check potential escapes from this arrayset call + # NOTE here we essentially only need to account for TypeError, assuming that + # UndefRefError or BoundsError don't capture any of the arguments here + argtypes = Any[argextype(args[i], astate.ir) for i in 2:length(args)] + boundcheckt = argtypes[1] + aryt = argtypes[2] + valt = argtypes[3] + if !(array_builtin_common_typecheck(boundcheckt, aryt, argtypes, 4) && + arrayset_typecheck(aryt, valt)) + add_thrown_escapes!(astate, pc, args, 2) + end + ary = args[3] + val = args[4] + inbounds = isa(boundcheckt, Const) && !boundcheckt.val::Bool + inbounds || add_escape_change!(astate, ary, ThrownEscape(pc)) + # we don't track precise index information about this array and won't record what value + # is being assigned here: directly propagate the escape information of this array to + # the value being assigned as the most conservative propagation + estate = astate.estate + if isa(ary, SSAValue) || isa(ary, Argument) + aryinfo = estate[ary] + else + # unanalyzable object (e.g. obj::GlobalRef): escape field value conservatively + add_escape_change!(astate, val, ⊤) + return true + end + AliasInfo = aryinfo.AliasInfo + if isa(AliasInfo, Bool) + AliasInfo && @goto conservative_propagation + # AliasInfo of this array hasn't been analyzed yet: set AliasInfo now + idx = array_nd_index(astate, ary, args[5:end]) + if isa(idx, Int) + AliasInfo = IndexableElements(IdDict{Int,AInfo}()) + @goto escape_indexable_def + end + AliasInfo = Unindexable() + @goto escape_unindexable_def + elseif isa(AliasInfo, IndexableElements) + idx = array_nd_index(astate, ary, args[5:end]) + if !isa(idx, Int) + AliasInfo = merge_to_unindexable(AliasInfo) + @goto escape_unindexable_def + end + @label escape_indexable_def + info = get!(()->AInfo(), AliasInfo.infos, idx) + add_alias_escapes!(astate, val, info) + push!(info, LocalDef(pc)) + add_escape_change!(astate, ary, EscapeInfo(aryinfo, AliasInfo)) # update with new AliasInfo + # propagate the escape information of this array ignoring elements information + add_escape_change!(astate, val, ignore_aliasinfo(aryinfo)) + elseif isa(AliasInfo, Unindexable) + @label escape_unindexable_def + add_alias_escapes!(astate, val, AliasInfo.info) + push!(AliasInfo.info, LocalDef(pc)) + add_escape_change!(astate, ary, EscapeInfo(aryinfo, AliasInfo)) # update with new AliasInfo + # propagate the escape information of this array ignoring elements information + add_escape_change!(astate, val, ignore_aliasinfo(aryinfo)) + else + # this object has been used as struct, but it is used as array here (thus should throw) + # update ary's element information and just handle this case conservatively + aryinfo = escape_unanalyzable_obj!(astate, ary, aryinfo) + @label conservative_propagation + add_alias_change!(astate, val, ary) + end + # also propagate escape information imposed on the return value of this `arrayset` + ssainfo = estate[SSAValue(pc)] + add_escape_change!(astate, ary, ssainfo) + return true +end + +# NOTE this function models and thus should be synced with the implementation of: +# size_t array_nd_index(jl_array_t *a, jl_value_t **args, size_t nidxs, ...) +function array_nd_index(astate::AnalysisState, @nospecialize(ary), args::Vector{Any}, nidxs::Int = length(args)) + isa(ary, SSAValue) || return nothing + aryid = ary.id + arrayinfo = astate.estate.arrayinfo + isa(arrayinfo, ArrayInfo) || return nothing + haskey(arrayinfo, aryid) || return nothing + dims = arrayinfo[aryid] + local i = 0 + local k, stride = 0, 1 + local nd = length(dims) + while k < nidxs + arg = args[k+1] + argval = argextype(arg, astate.ir) + isa(argval, Const) || return nothing + argval = argval.val + isa(argval, Int) || return nothing + ii = argval - 1 + i += ii * stride + d = k ≥ nd ? 1 : dims[k+1] + k < nidxs - 1 && ii ≥ d && return nothing # BoundsError + stride *= d + k += 1 + end + while k < nd + stride *= dims[k+1] + k += 1 + end + i ≥ stride && return nothing # BoundsError + return i +end + +function escape_builtin!(::typeof(arraysize), astate::AnalysisState, pc::Int, args::Vector{Any}) + length(args) == 3 || return false + ary = args[2] + dim = args[3] + if !arraysize_typecheck(ary, dim, astate.ir) + add_escape_change!(astate, ary, ThrownEscape(pc)) + add_escape_change!(astate, dim, ThrownEscape(pc)) + end + # NOTE we may still see "arraysize: dimension out of range", but it doesn't capture anything + return true +end + +function arraysize_typecheck(@nospecialize(ary), @nospecialize(dim), ir::IRCode) + aryt = argextype(ary, ir) + aryt ⊑ Array || return false + dimt = argextype(dim, ir) + dimt ⊑ Int || return false + return true +end + +# returns nothing if this isn't array resizing operation, +# otherwise returns true if it can throw BoundsError and false if not +function array_resize_info(name::Symbol) + if name === :jl_array_grow_beg || name === :jl_array_grow_end + return false, 1 + elseif name === :jl_array_del_beg || name === :jl_array_del_end + return true, 1 + elseif name === :jl_array_grow_at || name === :jl_array_del_at + return true, 2 + else + return nothing + end +end + +# NOTE may potentially throw "cannot resize array with shared data" error, +# but just ignore it since it doesn't capture anything +function escape_array_resize!(boundserror::Bool, ninds::Int, + astate::AnalysisState, pc::Int, args::Vector{Any}) + length(args) ≥ 6+ninds || return add_fallback_changes!(astate, pc, args) + ary = args[6] + aryt = argextype(ary, astate.ir) + aryt ⊑ Array || return add_fallback_changes!(astate, pc, args) + for i in 1:ninds + ind = args[i+6] + indt = argextype(ind, astate.ir) + indt ⊑ Integer || return add_fallback_changes!(astate, pc, args) + end + if boundserror + # this array resizing can potentially throw `BoundsError`, impose it now + add_escape_change!(astate, ary, ThrownEscape(pc)) + end + # give up indexing analysis whenever we see array resizing + # (since we track array dimensions only globally) + mark_unindexable!(astate, ary) + add_liveness_changes!(astate, pc, args, 6) +end + +function mark_unindexable!(astate::AnalysisState, @nospecialize(ary)) + isa(ary, SSAValue) || return + aryinfo = astate.estate[ary] + AliasInfo = aryinfo.AliasInfo + isa(AliasInfo, IndexableElements) || return + AliasInfo = merge_to_unindexable(AliasInfo) + add_escape_change!(astate, ary, EscapeInfo(aryinfo, AliasInfo)) +end + +is_array_copy(name::Symbol) = name === :jl_array_copy + +# FIXME this implementation is very conservative, improve the accuracy and solve broken test cases +function escape_array_copy!(astate::AnalysisState, pc::Int, args::Vector{Any}) + length(args) ≥ 6 || return add_fallback_changes!(astate, pc, args) + ary = args[6] + aryt = argextype(ary, astate.ir) + aryt ⊑ Array || return add_fallback_changes!(astate, pc, args) + if isa(ary, SSAValue) || isa(ary, Argument) + newary = SSAValue(pc) + aryinfo = astate.estate[ary] + newaryinfo = astate.estate[newary] + add_escape_change!(astate, newary, aryinfo) + add_escape_change!(astate, ary, newaryinfo) + end + add_liveness_changes!(astate, pc, args, 6) +end + +is_array_isassigned(name::Symbol) = name === :jl_array_isassigned + +function escape_array_isassigned!(astate::AnalysisState, pc::Int, args::Vector{Any}) + if !array_isassigned_nothrow(args, astate.ir) + add_thrown_escapes!(astate, pc, args) + end + add_liveness_changes!(astate, pc, args, 6) +end + +function array_isassigned_nothrow(args::Vector{Any}, src::IRCode) + # if !validate_foreigncall_args(args, + # :jl_array_isassigned, Cint, svec(Any,Csize_t), 0, :ccall) + # return false + # end + length(args) ≥ 7 || return false + arytype = argextype(args[6], src) + arytype ⊑ Array || return false + idxtype = argextype(args[7], src) + idxtype ⊑ Csize_t || return false + return true +end + +# # COMBAK do we want to enable this (and also backport this to Base for array allocations?) +# import Core.Compiler: Cint, svec +# function validate_foreigncall_args(args::Vector{Any}, +# name::Symbol, @nospecialize(rt), argtypes::SimpleVector, nreq::Int, convension::Symbol) +# length(args) ≥ 5 || return false +# normalize(args[1]) === name || return false +# args[2] === rt || return false +# args[3] === argtypes || return false +# args[4] === vararg || return false +# normalize(args[5]) === convension || return false +# return true +# end + +if isdefined(Core, :ImmutableArray) + +import Core: ImmutableArray, arrayfreeze, mutating_arrayfreeze, arraythaw + +escape_builtin!(::typeof(arrayfreeze), astate::AnalysisState, pc::Int, args::Vector{Any}) = + is_safe_immutable_array_op(Array, astate, args) +escape_builtin!(::typeof(mutating_arrayfreeze), astate::AnalysisState, pc::Int, args::Vector{Any}) = + is_safe_immutable_array_op(Array, astate, args) +escape_builtin!(::typeof(arraythaw), astate::AnalysisState, pc::Int, args::Vector{Any}) = + is_safe_immutable_array_op(ImmutableArray, astate, args) +function is_safe_immutable_array_op(@nospecialize(arytype), astate::AnalysisState, args::Vector{Any}) + length(args) == 2 || return false + argextype(args[2], astate.ir) ⊑ arytype || return false + return true +end + +end # if isdefined(Core, :ImmutableArray) + +if _TOP_MOD !== Core.Compiler + # NOTE define fancy package utilities when developing EA as an external package + include("EAUtils.jl") + using .EAUtils + export code_escapes, @code_escapes, __clear_cache! +end + +end # baremodule EscapeAnalysis diff --git a/base/compiler/ssair/EscapeAnalysis/disjoint_set.jl b/base/compiler/ssair/EscapeAnalysis/disjoint_set.jl new file mode 100644 index 00000000000000..915bc214d7c3ce --- /dev/null +++ b/base/compiler/ssair/EscapeAnalysis/disjoint_set.jl @@ -0,0 +1,143 @@ +# A disjoint set implementation adapted from +# https://github.com/JuliaCollections/DataStructures.jl/blob/f57330a3b46f779b261e6c07f199c88936f28839/src/disjoint_set.jl +# under the MIT license: https://github.com/JuliaCollections/DataStructures.jl/blob/master/License.md + +# imports +import ._TOP_MOD: + length, + eltype, + union!, + push! +# usings +import ._TOP_MOD: + OneTo, collect, zero, zeros, one, typemax + +# Disjoint-Set + +############################################################ +# +# A forest of disjoint sets of integers +# +# Since each element is an integer, we can use arrays +# instead of dictionary (for efficiency) +# +# Disjoint sets over other key types can be implemented +# based on an IntDisjointSet through a map from the key +# to an integer index +# +############################################################ + +_intdisjointset_bounds_err_msg(T) = "the maximum number of elements in IntDisjointSet{$T} is $(typemax(T))" + +""" + IntDisjointSet{T<:Integer}(n::Integer) + +A forest of disjoint sets of integers, which is a data structure +(also called a union–find data structure or merge–find set) +that tracks a set of elements partitioned +into a number of disjoint (non-overlapping) subsets. +""" +mutable struct IntDisjointSet{T<:Integer} + parents::Vector{T} + ranks::Vector{T} + ngroups::T +end + +IntDisjointSet(n::T) where {T<:Integer} = IntDisjointSet{T}(collect(OneTo(n)), zeros(T, n), n) +IntDisjointSet{T}(n::Integer) where {T<:Integer} = IntDisjointSet{T}(collect(OneTo(T(n))), zeros(T, T(n)), T(n)) +length(s::IntDisjointSet) = length(s.parents) + +""" + num_groups(s::IntDisjointSet) + +Get a number of groups. +""" +num_groups(s::IntDisjointSet) = s.ngroups +eltype(::Type{IntDisjointSet{T}}) where {T<:Integer} = T + +# find the root element of the subset that contains x +# path compression is implemented here +function find_root_impl!(parents::Vector{T}, x::Integer) where {T<:Integer} + p = parents[x] + @inbounds if parents[p] != p + parents[x] = p = _find_root_impl!(parents, p) + end + return p +end + +# unsafe version of the above +function _find_root_impl!(parents::Vector{T}, x::Integer) where {T<:Integer} + @inbounds p = parents[x] + @inbounds if parents[p] != p + parents[x] = p = _find_root_impl!(parents, p) + end + return p +end + +""" + find_root!(s::IntDisjointSet{T}, x::T) + +Find the root element of the subset that contains an member `x`. +Path compression happens here. +""" +find_root!(s::IntDisjointSet{T}, x::T) where {T<:Integer} = find_root_impl!(s.parents, x) + +""" + in_same_set(s::IntDisjointSet{T}, x::T, y::T) + +Returns `true` if `x` and `y` belong to the same subset in `s`, and `false` otherwise. +""" +in_same_set(s::IntDisjointSet{T}, x::T, y::T) where {T<:Integer} = find_root!(s, x) == find_root!(s, y) + +""" + union!(s::IntDisjointSet{T}, x::T, y::T) + +Merge the subset containing `x` and that containing `y` into one +and return the root of the new set. +""" +function union!(s::IntDisjointSet{T}, x::T, y::T) where {T<:Integer} + parents = s.parents + xroot = find_root_impl!(parents, x) + yroot = find_root_impl!(parents, y) + return xroot != yroot ? root_union!(s, xroot, yroot) : xroot +end + +""" + root_union!(s::IntDisjointSet{T}, x::T, y::T) + +Form a new set that is the union of the two sets whose root elements are +`x` and `y` and return the root of the new set. +Assume `x ≠ y` (unsafe). +""" +function root_union!(s::IntDisjointSet{T}, x::T, y::T) where {T<:Integer} + parents = s.parents + rks = s.ranks + @inbounds xrank = rks[x] + @inbounds yrank = rks[y] + + if xrank < yrank + x, y = y, x + elseif xrank == yrank + rks[x] += one(T) + end + @inbounds parents[y] = x + s.ngroups -= one(T) + return x +end + +""" + push!(s::IntDisjointSet{T}) + +Make a new subset with an automatically chosen new element `x`. +Returns the new element. Throw an `ArgumentError` if the +capacity of the set would be exceeded. +""" +function push!(s::IntDisjointSet{T}) where {T<:Integer} + l = length(s) + l < typemax(T) || throw(ArgumentError(_intdisjointset_bounds_err_msg(T))) + x = l + one(T) + push!(s.parents, x) + push!(s.ranks, zero(T)) + s.ngroups += one(T) + return x +end diff --git a/base/compiler/ssair/EscapeAnalysis/interprocedural.jl b/base/compiler/ssair/EscapeAnalysis/interprocedural.jl new file mode 100644 index 00000000000000..5d75db990e6f4b --- /dev/null +++ b/base/compiler/ssair/EscapeAnalysis/interprocedural.jl @@ -0,0 +1,151 @@ +# TODO this file contains many duplications with the inlining analysis code, factor them out + +import Core.Compiler: + MethodInstance, InferenceResult, Signature, ConstPropResult, ConcreteResult, + MethodResultPure, MethodMatchInfo, UnionSplitInfo, ConstCallInfo, InvokeCallInfo, + call_sig, argtypes_to_type, is_builtin, is_return_type, istopfunction, validate_sparams, + specialize_method, invoke_rewrite + +const Linfo = Union{MethodInstance,InferenceResult} +struct CallInfo + linfos::Vector{Linfo} + nothrow::Bool +end + +function resolve_call(ir::IRCode, stmt::Expr, @nospecialize(info)) + sig = call_sig(ir, stmt) + if sig === nothing + return missing + end + # TODO handle _apply_iterate + if is_builtin(sig) && sig.f !== invoke + return false + end + # handling corresponding to late_inline_special_case! + (; f, argtypes) = sig + if length(argtypes) == 3 && istopfunction(f, :!==) + return true + elseif length(argtypes) == 3 && istopfunction(f, :(>:)) + return true + elseif f === TypeVar && 2 ≤ length(argtypes) ≤ 4 && (argtypes[2] ⊑ Symbol) + return true + elseif f === UnionAll && length(argtypes) == 3 && (argtypes[2] ⊑ TypeVar) + return true + elseif is_return_type(f) + return true + end + if info isa MethodResultPure + return true + elseif info === false + return missing + end + # TODO handle OpaqueClosureCallInfo + if sig.f === invoke + isa(info, InvokeCallInfo) || return missing + return analyze_invoke_call(sig, info) + elseif isa(info, ConstCallInfo) + return analyze_const_call(sig, info) + elseif isa(info, MethodMatchInfo) + infos = MethodMatchInfo[info] + elseif isa(info, UnionSplitInfo) + infos = info.matches + else # isa(info, ReturnTypeCallInfo), etc. + return missing + end + return analyze_call(sig, infos) +end + +function analyze_invoke_call(sig::Signature, info::InvokeCallInfo) + match = info.match + if !match.fully_covers + # TODO: We could union split out the signature check and continue on + return missing + end + result = info.result + if isa(result, ConstPropResult) + return CallInfo(Linfo[result.result], true) + else + argtypes = invoke_rewrite(sig.argtypes) + mi = analyze_match(match, length(argtypes)) + mi === nothing && return missing + return CallInfo(Linfo[mi], true) + end +end + +function analyze_const_call(sig::Signature, cinfo::ConstCallInfo) + linfos = Linfo[] + (; call, results) = cinfo + infos = isa(call, MethodMatchInfo) ? MethodMatchInfo[call] : call.matches + local nothrow = true # required to account for potential escape via MethodError + local j = 0 + for i in 1:length(infos) + meth = infos[i].results + nothrow &= !meth.ambig + nmatch = Core.Compiler.length(meth) + if nmatch == 0 # No applicable methods + # mark this call may potentially throw, and the try next union split + nothrow = false + continue + end + for i = 1:nmatch + j += 1 + result = results[j] + match = Core.Compiler.getindex(meth, i) + if result === nothing + mi = analyze_match(match, length(sig.argtypes)) + mi === nothing && return missing + push!(linfos, mi) + elseif isa(result, ConcreteResult) + # TODO we may want to feedback information that this call always throws if !isdefined(result, :result) + push!(linfos, result.mi) + elseif isa(result, ConstPropResult) + push!(linfos, result.result) + end + nothrow &= match.fully_covers + end + end + return CallInfo(linfos, nothrow) +end + +function analyze_call(sig::Signature, infos::Vector{MethodMatchInfo}) + linfos = Linfo[] + local nothrow = true # required to account for potential escape via MethodError + for i in 1:length(infos) + meth = infos[i].results + nothrow &= !meth.ambig + nmatch = Core.Compiler.length(meth) + if nmatch == 0 # No applicable methods + # mark this call may potentially throw, and the try next union split + nothrow = false + continue + end + for i = 1:nmatch + match = Core.Compiler.getindex(meth, i) + mi = analyze_match(match, length(sig.argtypes)) + mi === nothing && return missing + push!(linfos, mi) + nothrow &= match.fully_covers + end + end + return CallInfo(linfos, nothrow) +end + +function analyze_match(match::MethodMatch, npassedargs::Int) + method = match.method + na = Int(method.nargs) + if na != npassedargs && !(na > 0 && method.isva) + # we have a method match only because an earlier + # inference step shortened our call args list, even + # though we have too many arguments to actually + # call this function + return nothing + end + + # Bail out if any static parameters are left as TypeVar + # COMBAK is this needed for escape analysis? + validate_sparams(match.sparams) || return nothing + + # See if there exists a specialization for this method signature + mi = specialize_method(match; preexisting=true) # Union{Nothing, MethodInstance} + return mi +end diff --git a/base/compiler/ssair/basicblock.jl b/base/compiler/ssair/basicblock.jl new file mode 100644 index 00000000000000..427aae707e6645 --- /dev/null +++ b/base/compiler/ssair/basicblock.jl @@ -0,0 +1,32 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +""" +Like UnitRange{Int}, but can handle the `last` field, being temporarily +< first (this can happen during compacting) +""" +struct StmtRange <: AbstractUnitRange{Int} + start::Int + stop::Int +end + +first(r::StmtRange) = r.start +last(r::StmtRange) = r.stop +iterate(r::StmtRange, state=0) = (last(r) - first(r) < state) ? nothing : (first(r) + state, state + 1) + +StmtRange(range::UnitRange{Int}) = StmtRange(first(range), last(range)) + +struct BasicBlock + stmts::StmtRange + preds::Vector{Int} + succs::Vector{Int} +end + +function BasicBlock(stmts::StmtRange) + return BasicBlock(stmts, Int[], Int[]) +end + +function BasicBlock(old_bb, stmts) + return BasicBlock(stmts, old_bb.preds, old_bb.succs) +end + +copy(bb::BasicBlock) = BasicBlock(bb.stmts, copy(bb.preds), copy(bb.succs)) diff --git a/base/compiler/ssair/domtree.jl b/base/compiler/ssair/domtree.jl index f9b407f9ddb3e5..fd49a7e118eb76 100644 --- a/base/compiler/ssair/domtree.jl +++ b/base/compiler/ssair/domtree.jl @@ -1,63 +1,573 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license +# This file implements the Semi-NCA (SNCA) dominator tree construction +# described in Georgiadis' PhD thesis [LG05], which itself is a simplification +# of the Simple Lenguare-Tarjan (SLT) algorithm [LG79]. This algorithm matches +# the algorithm choice in LLVM and seems to be a sweet spot in implementation +# simplicity and efficiency. +# +# This file also implements an extension of SNCA that supports updating the +# dominator tree with insertion and deletion of edges in the control flow +# graph, described in [GI16] as Dynamic SNCA. DSNCA was chosen over DBS, a +# different algorithm which achieves the best overall performance in [GI16], +# because it is simpler to understand and implement, performs well with edge +# deletions, and is of similar performance overall. +# +# SNCA works by first computing semidominators, then computing immediate +# dominators from them. The semidominator of a node is the node with minimum +# preorder number such that there is a semidominator path from it to the node. +# A semidominator path is a path in which the preorder numbers of all nodes not +# at the endpoints are greater than the preorder number of the last node. +# Intuitively, the semidominator approximates the immediate dominator of a node +# by taking the path (in the CFG) that gets as close to the root as possible +# while avoiding ancestors of the node in the DFS tree. +# +# In computing the semidominators, SNCA performs "path compression" whenever a +# node has a nontrivial semidominator (i.e. a semidominator that is not just +# its parent in the DFS tree). Path compression propagates the "label" of a +# node, which represents a possible semidominator with associated semidominator +# path passing through that node. +# +# For example, path compression will be performed for the following CFG, where +# the edge not in the DFS tree is marked with asterisks. Note that nodes are +# labeled with their preorder numbers, and all edges point downward. +# +# 1 +# |\ +# | \ +# | 4 +# | | +# 2 5 +# | | +# | 6 +# | * +# |* +# 3 +# +# There is a nontrivial semidominator path from 1 to 3, passing through 4, 5, +# and 6. Stepping through the whole algorithm on paper with an example like +# this is very helpful for understanding how it works. +# +# DSNCA runs the whole algorithm from scratch if the DFS tree is invalidated by +# the insertion or deletion, but otherwise recomputes a subset of the +# semidominators (all immediate dominators then need to be recomputed). +# +# [LG05] Linear-Time Algorithms for Dominators and Related Problems +# Loukas Georgiadis, Princeton University, November 2005, pp. 21-23: +# ftp://ftp.cs.princeton.edu/reports/2005/737.pdf +# +# [LT79] A fast algorithm for finding dominators in a flowgraph +# Thomas Lengauer, Robert Endre Tarjan, July 1979, ACM TOPLAS 1-1 +# http://www.dtic.mil/dtic/tr/fulltext/u2/a054144.pdf +# +# [GI16] An Experimental Study of Dynamic Dominators +# Loukas Georgiadis, Giuseppe F. Italiano, Luigi Laura, Federico +# Santaroni, April 2016 +# https://arxiv.org/abs/1604.02711 + +# We could make these real structs, but probably not worth the extra +# overhead. Still, give them names for documentary purposes. +const BBNumber = Int +const PreNumber = Int +const PostNumber = Int + +struct DFSTree + # These map between BB number and pre- or postorder numbers + to_pre::Vector{PreNumber} + from_pre::Vector{BBNumber} + to_post::Vector{PostNumber} + from_post::Vector{BBNumber} + + # Records parent relationships in the DFS tree + # (preorder number -> preorder number) + # Storing it this way saves a few lookups in the snca_compress! algorithm + to_parent_pre::Vector{PreNumber} +end + +function DFSTree(n_blocks::Int) + return DFSTree(zeros(PreNumber, n_blocks), + Vector{BBNumber}(undef, n_blocks), + zeros(PostNumber, n_blocks), + Vector{BBNumber}(undef, n_blocks), + zeros(PreNumber, n_blocks)) +end + +copy(D::DFSTree) = DFSTree(copy(D.to_pre), + copy(D.from_pre), + copy(D.to_post), + copy(D.from_post), + copy(D.to_parent_pre)) + +function copy!(dst::DFSTree, src::DFSTree) + copy!(dst.to_pre, src.to_pre) + copy!(dst.from_pre, src.from_pre) + copy!(dst.to_post, src.to_post) + copy!(dst.from_post, src.from_post) + copy!(dst.to_parent_pre, src.to_parent_pre) + return dst +end + +length(D::DFSTree) = length(D.from_pre) + +function DFS!(D::DFSTree, blocks::Vector{BasicBlock}) + copy!(D, DFSTree(length(blocks))) + to_visit = Tuple{BBNumber, PreNumber, Bool}[(1, 0, false)] + pre_num = 1 + post_num = 1 + while !isempty(to_visit) + # Because we want the postorder number as well as the preorder number, + # we don't pop the current node from the stack until we're moving up + # the tree + (current_node_bb, parent_pre, pushed_children) = to_visit[end] + + if pushed_children + # Going up the DFS tree, so all we need to do is record the + # postorder number, then move on + D.to_post[current_node_bb] = post_num + D.from_post[post_num] = current_node_bb + post_num += 1 + pop!(to_visit) + + elseif D.to_pre[current_node_bb] != 0 + # Node has already been visited, move on + pop!(to_visit) + continue + else + # Going down the DFS tree + + # Record preorder number + D.to_pre[current_node_bb] = pre_num + D.from_pre[pre_num] = current_node_bb + D.to_parent_pre[pre_num] = parent_pre + + # Record that children (will) have been pushed + to_visit[end] = (current_node_bb, parent_pre, true) + + # Push children to the stack + for succ_bb in blocks[current_node_bb].succs + push!(to_visit, (succ_bb, pre_num, false)) + end + + pre_num += 1 + end + end + + # If all blocks are reachable, this is a no-op, otherwise, we shrink these + # arrays. + resize!(D.from_pre, pre_num - 1) + resize!(D.from_post, post_num - 1) # should be same size as pre_num - 1 + resize!(D.to_parent_pre, pre_num - 1) + + return D +end + +DFS(blocks::Vector{BasicBlock}) = DFS!(DFSTree(0), blocks) + +""" +Keeps the per-BB state of the Semi NCA algorithm. In the original formulation, +there are three separate length `n` arrays, `label`, `semi` and `ancestor`. +Instead, for efficiency, we use one array in a array-of-structs style setup. +""" +struct SNCAData + semi::PreNumber + label::PreNumber +end + "Represents a Basic Block, in the DomTree" struct DomTreeNode # How deep we are in the DomTree level::Int # The BB indices in the CFG for all Basic Blocks we immediately dominate - children::Vector{Int} + children::Vector{BBNumber} end -DomTreeNode() = DomTreeNode(1, Vector{Int}()) + +DomTreeNode() = DomTreeNode(1, Vector{BBNumber}()) "Data structure that encodes which basic block dominates which." struct DomTree - # Which basic block immediately dominates each basic block (ordered by BB indices) - # Note: this is the inverse of the nodes, children field - idoms::Vector{Int} + # These can be reused when updating domtree dynamically + dfs_tree::DFSTree + snca_state::Vector{SNCAData} + + # Which basic block immediately dominates each basic block, using BB indices + idoms_bb::Vector{BBNumber} # The nodes in the tree (ordered by BB indices) nodes::Vector{DomTreeNode} end +function DomTree() + return DomTree(DFSTree(0), SNCAData[], BBNumber[], DomTreeNode[]) +end + +function construct_domtree(blocks::Vector{BasicBlock}) + return update_domtree!(blocks, DomTree(), true, 0) +end + +function update_domtree!(blocks::Vector{BasicBlock}, domtree::DomTree, + recompute_dfs::Bool, max_pre::PreNumber) + if recompute_dfs + DFS!(domtree.dfs_tree, blocks) + end + + if max_pre == 0 + max_pre = length(domtree.dfs_tree) + end + + SNCA!(domtree, blocks, max_pre) + compute_domtree_nodes!(domtree) + return domtree +end + +function compute_domtree_nodes!(domtree::DomTree) + # Compute children + copy!(domtree.nodes, + DomTreeNode[DomTreeNode() for _ in 1:length(domtree.idoms_bb)]) + for (idx, idom) in Iterators.enumerate(domtree.idoms_bb) + (idx == 1 || idom == 0) && continue + push!(domtree.nodes[idom].children, idx) + end + # Recursively set level + update_level!(domtree.nodes, 1, 1) + return domtree.nodes +end + +function update_level!(nodes::Vector{DomTreeNode}, node::BBNumber, level::Int) + worklist = Tuple{BBNumber, Int}[(node, level)] + while !isempty(worklist) + (node, level) = pop!(worklist) + nodes[node] = DomTreeNode(level, nodes[node].children) + foreach(nodes[node].children) do child + push!(worklist, (child, level+1)) + end + end +end + +""" +The main Semi-NCA algorithm. Matches Figure 2.8 in [LG05]. Note that the +pseudocode in [LG05] is not entirely accurate. The best way to understand +what's happening is to read [LT79], then the description of SLT in [LG05] +(warning: inconsistent notation), then the description of Semi-NCA. +""" +function SNCA!(domtree::DomTree, blocks::Vector{BasicBlock}, max_pre::PreNumber) + D = domtree.dfs_tree + state = domtree.snca_state + # There may be more blocks than are reachable in the DFS / dominator tree + n_blocks = length(blocks) + n_nodes = length(D) + + # `label` is initialized to the identity mapping (though the paper doesn't + # make that clear). The rationale for this is Lemma 2.4 in [LG05] (i.e. + # Theorem 4 in [LT79]). Note however, that we don't ever look at `semi` + # until it is fully initialized, so we could leave it uninitialized here if + # we wanted to. + resize!(state, n_nodes) + for w in 1:max_pre + # Only reset semidominators for nodes we want to recompute + state[w] = SNCAData(typemax(PreNumber), w) + end + + # If we are only recomputing some of the semidominators, the remaining + # labels should be reset, because they may have become inapplicable to the + # node/semidominator we are currently processing/recomputing. They can + # become inapplicable because of path compressions that were triggered by + # nodes that should only be processed after the current one (but were + # processed the last time `SNCA!` was run). + # + # So, for every node that is not being reprocessed, we reset its label to + # its semidominator, which is the value that its label assumes once its + # semidominator is computed. If this was too conservative, i.e. if the + # label would have been updated before we process the current node in a + # situation where all semidominators were recomputed, then path compression + # will produce the correct label. + for w in max_pre+1:n_nodes + semi = state[w].semi + state[w] = SNCAData(semi, semi) + end + + # Calculate semidominators, but only for blocks with preorder number up to + # max_pre + ancestors = copy(D.to_parent_pre) + for w::PreNumber in reverse(2:max_pre) + # LLVM initializes this to the parent, the paper initializes this to + # `w`, but it doesn't really matter (the parent is a predecessor, so at + # worst we'll discover it below). Save a memory reference here. + semi_w = typemax(PreNumber) + last_linked = PreNumber(w + 1) + for v ∈ blocks[D.from_pre[w]].preds + # For the purpose of the domtree, ignore virtual predecessors into + # catch blocks. + v == 0 && continue + + v_pre = D.to_pre[v] + + # Ignore unreachable predecessors + v_pre == 0 && continue + + # N.B.: This conditional is missing from the pseudocode in figure + # 2.8 of [LG05]. It corresponds to the `ancestor[v] != 0` check in + # the `eval` implementation in figure 2.6 + if v_pre >= last_linked + # `v` has already been processed, so perform path compression + + # For performance, if the number of ancestors is small avoid + # the extra allocation of the worklist. + if length(ancestors) <= 32 + snca_compress!(state, ancestors, v_pre, last_linked) + else + snca_compress_worklist!(state, ancestors, v_pre, last_linked) + end + end + + # The (preorder number of the) semidominator of a block is the + # minimum over the labels of its predecessors + semi_w = min(semi_w, state[v_pre].label) + end + state[w] = SNCAData(semi_w, semi_w) + end + + # Compute immediate dominators, which for a node must be the nearest common + # ancestor in the (immediate) dominator tree between its semidominator and + # its parent (see Lemma 2.6 in [LG05]). + idoms_pre = copy(D.to_parent_pre) + for v in 2:n_nodes + idom = idoms_pre[v] + vsemi = state[v].semi + while idom > vsemi + idom = idoms_pre[idom] + end + idoms_pre[v] = idom + end + + # Express idoms in BB indexing + resize!(domtree.idoms_bb, n_blocks) + for i::BBNumber in 1:n_blocks + if i == 1 || D.to_pre[i] == 0 + domtree.idoms_bb[i] = 0 + else + domtree.idoms_bb[i] = D.from_pre[idoms_pre[D.to_pre[i]]] + end + end +end + """ - Checks if bb1 dominates bb2. - bb1 and bb2 are indexes into the CFG blocks. - bb1 dominates bb2 if the only way to enter bb2 is via bb1. - (Other blocks may be in between, e.g bb1->bbX->bb2). +Matches the snca_compress algorithm in Figure 2.8 of [LG05], with the +modification suggested in the paper to use `last_linked` to determine whether +an ancestor has been processed rather than storing `0` in the ancestor array. """ -function dominates(domtree::DomTree, bb1::Int, bb2::Int) +function snca_compress!(state::Vector{SNCAData}, ancestors::Vector{PreNumber}, + v::PreNumber, last_linked::PreNumber) + u = ancestors[v] + @assert u < v + if u >= last_linked + snca_compress!(state, ancestors, u, last_linked) + if state[u].label < state[v].label + state[v] = SNCAData(state[v].semi, state[u].label) + end + ancestors[v] = ancestors[u] + end + nothing +end + +function snca_compress_worklist!( + state::Vector{SNCAData}, ancestors::Vector{PreNumber}, + v::PreNumber, last_linked::PreNumber) + # TODO: There is a smarter way to do this + u = ancestors[v] + worklist = Tuple{PreNumber, PreNumber}[(u,v)] + @assert u < v + while !isempty(worklist) + u, v = last(worklist) + if u >= last_linked + if ancestors[u] >= last_linked + push!(worklist, (ancestors[u], u)) + continue + end + if state[u].label < state[v].label + state[v] = SNCAData(state[v].semi, state[u].label) + end + ancestors[v] = ancestors[u] + end + pop!(worklist) + end +end + +"Given updated blocks, update the given dominator tree with an inserted edge." +function domtree_insert_edge!(domtree::DomTree, blocks::Vector{BasicBlock}, + from::BBNumber, to::BBNumber) + # `from` is unreachable, so `from` and `to` aren't in domtree + if bb_unreachable(domtree, from) + return domtree + end + + # Implements Section 3.1 of [GI16] + dt = domtree.dfs_tree + from_pre = dt.to_pre[from] + to_pre = dt.to_pre[to] + from_post = dt.to_post[from] + to_post = dt.to_post[to] + if to_pre == 0 || (from_pre < to_pre && from_post < to_post) + # The DFS tree is invalidated by the edge insertion, so run from + # scratch + update_domtree!(blocks, domtree, true, 0) + else + # DFS tree is still valid, so update only affected nodes + update_domtree!(blocks, domtree, false, to_pre) + end + + return domtree +end + +"Given updated blocks, update the given dominator tree with a deleted edge." +function domtree_delete_edge!(domtree::DomTree, blocks::Vector{BasicBlock}, + from::BBNumber, to::BBNumber) + # `from` is unreachable, so `from` and `to` aren't in domtree + if bb_unreachable(domtree, from) + return domtree + end + + # Implements Section 3.1 of [GI16] + if is_parent(domtree.dfs_tree, from, to) + # The `from` block is the parent of the `to` block in the DFS tree, so + # deleting the edge invalidates the DFS tree, so start from scratch + update_domtree!(blocks, domtree, true, 0) + elseif on_semidominator_path(domtree, from, to) + # Recompute semidominators for blocks with preorder number up to that + # of `to` block. Semidominators for blocks with preorder number greater + # than that of `to` aren't affected because no semidominator path to + # the block can pass through the `to` block (the preorder number of + # `to` would be lower than those of these blocks, and `to` is not their + # parent in the DFS tree). + to_pre = domtree.dfs_tree.to_pre[to] + update_domtree!(blocks, domtree, false, to_pre) + end + # Otherwise, dominator tree is not affected + + return domtree +end + +"Check if x is the parent of y in the given DFS tree." +function is_parent(dfs_tree::DFSTree, x::BBNumber, y::BBNumber) + x_pre = dfs_tree.to_pre[x] + y_pre = dfs_tree.to_pre[y] + return x_pre == dfs_tree.to_parent_pre[y_pre] +end + +""" +Check if x is on some semidominator path from the semidominator of y to y, +assuming there is an edge from x to y. +""" +function on_semidominator_path(domtree::DomTree, x::BBNumber, y::BBNumber) + x_pre = domtree.dfs_tree.to_pre[x] + y_pre = domtree.dfs_tree.to_pre[y] + + semi_y = domtree.snca_state[y_pre].semi + current_block = x_pre + + # Follow the semidominators of `x` up the DFS tree to see if we ever reach + # the semidominator of `y`. If so, `x` is on a semidominator path between + # `y` and its semidominator. We can stop if the preorder number of the + # semidominators becomes less than that of the semidominator of `y`, + # because it can only decrease further. + while current_block >= semi_y + if semi_y == current_block + return true + end + current_block = domtree.snca_state[current_block].semi + end + return false +end + +""" +Rename basic block numbers in a dominator tree, removing the block if it is +renamed to -1. +""" +function rename_nodes!(domtree::DomTree, rename_bb::Vector{BBNumber}) + # Rename DFS tree + rename_nodes!(domtree.dfs_tree, rename_bb) + + # `snca_state` is indexed by preorder number, so should be unchanged + + # Rename `idoms_bb` and `nodes` + old_idoms_bb = copy(domtree.idoms_bb) + old_nodes = copy(domtree.nodes) + for (old_bb, new_bb) in enumerate(rename_bb) + if new_bb != -1 + domtree.idoms_bb[new_bb] = (new_bb == 1) ? + 0 : rename_bb[old_idoms_bb[old_bb]] + domtree.nodes[new_bb] = old_nodes[old_bb] + map!(i -> rename_bb[i], + domtree.nodes[new_bb].children, + domtree.nodes[new_bb].children) + end + end + + # length of `to_pre` after renaming DFS tree is new number of basic blocks + resize!(domtree.idoms_bb, length(domtree.dfs_tree.to_pre)) + resize!(domtree.nodes, length(domtree.dfs_tree.to_pre)) + return domtree +end + +""" +Rename basic block numbers in a DFS tree, removing the block if it is renamed +to -1. +""" +function rename_nodes!(D::DFSTree, rename_bb::Vector{BBNumber}) + n_blocks = length(D.to_pre) + n_reachable_blocks = length(D.from_pre) + + old_to_pre = copy(D.to_pre) + old_from_pre = copy(D.from_pre) + old_to_post = copy(D.to_post) + old_from_post = copy(D.from_post) + max_new_bb = 0 + for (old_bb, new_bb) in enumerate(rename_bb) + if new_bb != -1 + D.to_pre[new_bb] = old_to_pre[old_bb] + D.from_pre[old_to_pre[old_bb]] = new_bb + D.to_post[new_bb] = old_to_post[old_bb] + D.from_post[old_to_post[old_bb]] = new_bb + + # Keep track of highest BB number to resize arrays with + if new_bb > max_new_bb + max_new_bb = new_bb + end + end + end + resize!(D.to_pre, max_new_bb) + resize!(D.to_post, max_new_bb) + # `to_parent_pre` should be unchanged + return D +end + +""" +Checks if bb1 dominates bb2. +bb1 and bb2 are indexes into the CFG blocks. +bb1 dominates bb2 if the only way to enter bb2 is via bb1. +(Other blocks may be in between, e.g bb1->bbX->bb2). +""" +function dominates(domtree::DomTree, bb1::BBNumber, bb2::BBNumber) bb1 == bb2 && return true target_level = domtree.nodes[bb1].level source_level = domtree.nodes[bb2].level source_level < target_level && return false for _ in (source_level - 1):-1:target_level - bb2 = domtree.idoms[bb2] + bb2 = domtree.idoms_bb[bb2] end return bb1 == bb2 end -bb_unreachable(domtree::DomTree, bb::Int) = bb != 1 && domtree.nodes[bb].level == 1 - -function update_level!(domtree::Vector{DomTreeNode}, node::Int, level::Int) - worklist = Tuple{Int, Int}[(node, level)] - while !isempty(worklist) - (node, level) = pop!(worklist) - domtree[node] = DomTreeNode(level, domtree[node].children) - foreach(domtree[node].children) do child - push!(worklist, (child, level+1)) - end - end -end +bb_unreachable(domtree::DomTree, bb::BBNumber) = bb != 1 && domtree.dfs_tree.to_pre[bb] == 0 "Iterable data structure that walks though all dominated blocks" struct DominatedBlocks domtree::DomTree - worklist::Vector{Int} + worklist::Vector{BBNumber} end "Returns an iterator that walks through all blocks dominated by the basic block at index `root`" -function dominated(domtree::DomTree, root::Int) - doms = DominatedBlocks(domtree, Vector{Int}()) +function dominated(domtree::DomTree, root::BBNumber) + doms = DominatedBlocks(domtree, Vector{BBNumber}()) push!(doms.worklist, root) doms end @@ -71,8 +581,8 @@ function iterate(doms::DominatedBlocks, state::Nothing=nothing) return (bb, nothing) end -function naive_idoms(cfg::CFG) - nblocks = length(cfg.blocks) +function naive_idoms(blocks::Vector{BasicBlock}) + nblocks = length(blocks) # The extra +1 helps us detect unreachable blocks below dom_all = BitSet(1:nblocks+1) dominators = BitSet[n == 1 ? BitSet(1) : copy(dom_all) for n = 1:nblocks] @@ -80,10 +590,10 @@ function naive_idoms(cfg::CFG) while changed changed = false for n = 2:nblocks - if isempty(cfg.blocks[n].preds) + if isempty(blocks[n].preds) continue end - firstp, rest = Iterators.peel(Iterators.filter(p->p != 0, cfg.blocks[n].preds)) + firstp, rest = Iterators.peel(Iterators.filter(p->p != 0, blocks[n].preds))::NTuple{2,Any} new_doms = copy(dominators[firstp]) for p in rest intersect!(new_doms, dominators[p]) @@ -115,213 +625,3 @@ function naive_idoms(cfg::CFG) end idoms end - -# Construct Dom Tree -function construct_domtree(cfg::CFG) - idoms = SNCA(cfg) - # Compute children - nblocks = length(cfg.blocks) - domtree = DomTreeNode[DomTreeNode() for _ = 1:nblocks] - for (idx, idom) in Iterators.enumerate(idoms) - (idx == 1 || idom == 0) && continue - push!(domtree[idom].children, idx) - end - # Recursively set level - update_level!(domtree, 1, 1) - DomTree(idoms, domtree) -end - -#================================ [SNCA] ======================================# -# -# This section implements the Semi-NCA (SNCA) dominator tree construction from -# described in Georgiadis' PhD thesis [LG05], which itself is a simplification -# of the Simple Lenguare-Tarjan (SLT) algorithm [LG79]. This algorithm matches -# the algorithm choice in LLVM and seems to be a sweet spot in implementation -# simplicity and efficiency. -# -# [LG05] Linear-Time Algorithms for Dominators and Related Problems -# Loukas Georgiadis, Princeton University, November 2005, pp. 21-23: -# ftp://ftp.cs.princeton.edu/reports/2005/737.pdf -# -# [LT79] A fast algorithm for finding dominators in a flowgraph -# Thomas Lengauer, Robert Endre Tarjan, July 1979, ACM TOPLAS 1-1 -# http://www.dtic.mil/dtic/tr/fulltext/u2/a054144.pdf -# -begin - # We could make these real structs, but probably not worth the extra - # overhead. Still, give them names for documentary purposes. - const BBNumber = UInt - const DFSNumber = UInt - - """ - Keeps the per-BB state of the Semi NCA algorithm. In the original - formulation, there are three separate length `n` arrays, `label`, `semi` and - `ancestor`. Instead, for efficiency, we use one array in a array-of-structs - style setup. - """ - struct Node - semi::DFSNumber - label::DFSNumber - end - - struct DFSTree - # Maps DFS number to BB number - numbering::Vector{BBNumber} - # Maps BB number to DFS number - reverse::Vector{DFSNumber} - # Records parent relationships in the DFS tree (DFS number -> DFS number) - # Storing it this way saves a few lookups in the snca_compress! algorithm - parents::Vector{DFSNumber} - end - length(D::DFSTree) = length(D.numbering) - preorder(D::DFSTree) = OneTo(length(D)) - _drop(xs::AbstractUnitRange, n::Integer) = (first(xs)+n):last(xs) - - function DFSTree(nblocks::Int) - DFSTree( - Vector{BBNumber}(undef, nblocks), - zeros(DFSNumber, nblocks), - Vector{DFSNumber}(undef, nblocks)) - end - - function DFS(cfg::CFG, current_node::BBNumber)::DFSTree - dfs = DFSTree(length(cfg.blocks)) - # TODO: We could reuse the storage in DFSTree for our worklist. We're - # guaranteed for the worklist to be smaller than the remaining space in - # DFSTree - worklist = Tuple{DFSNumber, BBNumber}[(0, current_node)] - dfs_num = 1 - parent = 0 - while !isempty(worklist) - (parent, current_node) = pop!(worklist) - dfs.reverse[current_node] != 0 && continue - dfs.reverse[current_node] = dfs_num - dfs.numbering[dfs_num] = current_node - dfs.parents[dfs_num] = parent - for succ in cfg.blocks[current_node].succs - push!(worklist, (dfs_num, succ)) - end - dfs_num += 1 - end - # If all blocks are reachable, this is a no-op, otherwise, - # we shrink these arrays. - resize!(dfs.numbering, dfs_num - 1) - resize!(dfs.parents, dfs_num - 1) - dfs - end - - """ - Matches the snca_compress algorithm in Figure 2.8 of [LG05], with the - modification suggested in the paper to use `last_linked` to determine - whether an ancestor has been processed rather than storing `0` in the - ancestor array. - """ - function snca_compress!(state::Vector{Node}, ancestors::Vector{DFSNumber}, - v::DFSNumber, last_linked::DFSNumber) - u = ancestors[v] - @assert u < v - if u >= last_linked - snca_compress!(state, ancestors, u, last_linked) - if state[u].label < state[v].label - state[v] = Node(state[v].semi, state[u].label) - end - ancestors[v] = ancestors[u] - end - nothing - end - - function snca_compress_worklist!( - state::Vector{Node}, ancestors::Vector{DFSNumber}, - v::DFSNumber, last_linked::DFSNumber) - # TODO: There is a smarter way to do this - u = ancestors[v] - worklist = Tuple{DFSNumber, DFSNumber}[(u,v)] - @assert u < v - while !isempty(worklist) - u, v = last(worklist) - if u >= last_linked - if ancestors[u] >= last_linked - push!(worklist, (ancestors[u], u)) - continue - end - if state[u].label < state[v].label - state[v] = Node(state[v].semi, state[u].label) - end - ancestors[v] = ancestors[u] - end - pop!(worklist) - end - end - - """ - SNCA(cfg::CFG) - - Determines a map from basic blocks to the block which immediately dominate them. - Expressed as indexes into `cfg.blocks`. - - The main Semi-NCA algrithm. Matches Figure 2.8 in [LG05]. - Note that the pseudocode in [LG05] is not entirely accurate. - The best way to understand what's happening is to read [LT79], then the - description of SLT in [LG05] (warning: inconsistent notation), then - the description of Semi-NCA. - """ - function SNCA(cfg::CFG) - D = DFS(cfg, BBNumber(1)) - # `label` is initialized to the identity mapping (though - # the paper doesn't make that clear). The rational for this is Lemma - # 2.4 in [LG05] (i.e. Theorem 4 in ). Note however, that we don't - # ever look at `semi` until it is fully initialized, so we could leave - # it uninitialized here if we wanted to. - state = Node[ Node(typemax(DFSNumber), w) for w in preorder(D) ] - # Initialize idoms to parents. Note that while idoms are eventually - # BB indexed, we keep it DFS indexed until a final post-processing - # pass to avoid extra memory references during the O(N^2) phase below. - idoms_dfs = copy(D.parents) - # We abuse the parents array as the ancestors array. - # Semi-NCA does not look at the parents array at all. - # SLT would, but never simultaneously, so we could still - # do this. - ancestors = D.parents - for w::DFSNumber ∈ reverse(_drop(preorder(D), 1)) - # LLVM initializes this to the parent, the paper initializes this to - # `w`, but it doesn't really matter (the parent is a predecessor, - # so at worst we'll discover it below). Save a memory reference here. - semi_w = typemax(DFSNumber) - for v ∈ cfg.blocks[D.numbering[w]].preds - # For the purpose of the domtree, ignore virtual predecessors - # into catch blocks. - v == 0 && continue - vdfs = D.reverse[v] - # Ignore unreachable predecessors - vdfs == 0 && continue - last_linked = DFSNumber(w + 1) - # N.B.: This conditional is missing from the psuedocode - # in figure 2.8 of [LG05]. It corresponds to the - # `ancestor[v] != 0` check in the `eval` implementation in - # figure 2.6 - if vdfs >= last_linked - # For performance, if the number of ancestors is small - # avoid the extra allocation of the worklist. - if length(ancestors) <= 32 - snca_compress!(state, ancestors, vdfs, last_linked) - else - snca_compress_worklist!(state, ancestors, vdfs, last_linked) - end - end - semi_w = min(semi_w, state[vdfs].label) - end - state[w] = Node(semi_w, semi_w) - end - for v ∈ _drop(preorder(D), 1) - idom = idoms_dfs[v] - vsemi = state[v].semi - while idom > vsemi - idom = idoms_dfs[idom] - end - idoms_dfs[v] = idom - end - # Reexpress the idom relationship in BB indexing - idoms_bb = Int[ (i == 1 || D.reverse[i] == 0) ? 0 : D.numbering[idoms_dfs[D.reverse[i]]] for i = 1:length(cfg.blocks) ] - idoms_bb - end -end diff --git a/base/compiler/ssair/driver.jl b/base/compiler/ssair/driver.jl index 465102e82e1556..7759d8d80b9cc8 100644 --- a/base/compiler/ssair/driver.jl +++ b/base/compiler/ssair/driver.jl @@ -1,7 +1,5 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -using Core: LineInfoNode - if false import Base: Base, @show else @@ -10,134 +8,17 @@ else end end -include("compiler/ssair/ir.jl") +function argextype end # imported by EscapeAnalysis +function stmt_effect_free end # imported by EscapeAnalysis +function alloc_array_ndims end # imported by EscapeAnalysis +function try_compute_field end # imported by EscapeAnalysis + +include("compiler/ssair/basicblock.jl") include("compiler/ssair/domtree.jl") +include("compiler/ssair/ir.jl") include("compiler/ssair/slot2ssa.jl") -include("compiler/ssair/queries.jl") -include("compiler/ssair/passes.jl") include("compiler/ssair/inlining.jl") include("compiler/ssair/verify.jl") include("compiler/ssair/legacy.jl") -#@isdefined(Base) && include("compiler/ssair/show.jl") - -function normalize(@nospecialize(stmt), meta::Vector{Any}) - if isa(stmt, Expr) - if stmt.head === :meta - args = stmt.args - if length(args) > 0 - push!(meta, stmt) - end - return nothing - end - end - return stmt -end - -function convert_to_ircode(ci::CodeInfo, code::Vector{Any}, coverage::Bool, nargs::Int, sv::OptimizationState) - # Go through and add an unreachable node after every - # Union{} call. Then reindex labels. - idx = 1 - oldidx = 1 - changemap = fill(0, length(code)) - labelmap = coverage ? fill(0, length(code)) : changemap - prevloc = zero(eltype(ci.codelocs)) - stmtinfo = sv.stmt_info - while idx <= length(code) - codeloc = ci.codelocs[idx] - if coverage && codeloc != prevloc && codeloc != 0 - # insert a side-effect instruction before the current instruction in the same basic block - insert!(code, idx, Expr(:code_coverage_effect)) - insert!(ci.codelocs, idx, codeloc) - insert!(ci.ssavaluetypes, idx, Nothing) - insert!(stmtinfo, idx, nothing) - changemap[oldidx] += 1 - if oldidx < length(labelmap) - labelmap[oldidx + 1] += 1 - end - idx += 1 - prevloc = codeloc - end - if code[idx] isa Expr && ci.ssavaluetypes[idx] === Union{} - if !(idx < length(code) && isa(code[idx + 1], ReturnNode) && !isdefined((code[idx + 1]::ReturnNode), :val)) - # insert unreachable in the same basic block after the current instruction (splitting it) - insert!(code, idx + 1, ReturnNode()) - insert!(ci.codelocs, idx + 1, ci.codelocs[idx]) - insert!(ci.ssavaluetypes, idx + 1, Union{}) - insert!(stmtinfo, idx + 1, nothing) - if oldidx < length(changemap) - changemap[oldidx + 1] += 1 - coverage && (labelmap[oldidx + 1] += 1) - end - idx += 1 - end - end - idx += 1 - oldidx += 1 - end - renumber_ir_elements!(code, changemap, labelmap) - - inbounds_depth = 0 # Number of stacked inbounds - meta = Any[] - flags = fill(0x00, length(code)) - for i = 1:length(code) - stmt = code[i] - if isexpr(stmt, :inbounds) - arg1 = stmt.args[1] - if arg1 === true # push - inbounds_depth += 1 - elseif arg1 === false # clear - inbounds_depth = 0 - elseif inbounds_depth > 0 # pop - inbounds_depth -= 1 - end - stmt = nothing - else - stmt = normalize(stmt, meta) - end - code[i] = stmt - if !(stmt === nothing) - if inbounds_depth > 0 - flags[i] |= IR_FLAG_INBOUNDS - end - end - end - strip_trailing_junk!(ci, code, stmtinfo, flags) - cfg = compute_basic_blocks(code) - types = Any[] - stmts = InstructionStream(code, types, stmtinfo, ci.codelocs, flags) - ir = IRCode(stmts, cfg, collect(LineInfoNode, ci.linetable), sv.slottypes, meta, sv.sptypes) - return ir -end - -function slot2reg(ir::IRCode, ci::CodeInfo, nargs::Int, sv::OptimizationState) - # need `ci` for the slot metadata, IR for the code - @timeit "domtree 1" domtree = construct_domtree(ir.cfg) - defuse_insts = scan_slot_def_use(nargs, ci, ir.stmts.inst) - @timeit "construct_ssa" ir = construct_ssa!(ci, ir, domtree, defuse_insts, nargs, sv.sptypes, sv.slottypes) # consumes `ir` - return ir -end - -function run_passes(ci::CodeInfo, nargs::Int, sv::OptimizationState) - preserve_coverage = coverage_enabled(sv.mod) - ir = convert_to_ircode(ci, copy_exprargs(ci.code), preserve_coverage, nargs, sv) - ir = slot2reg(ir, ci, nargs, sv) - #@Base.show ("after_construct", ir) - # TODO: Domsorting can produce an updated domtree - no need to recompute here - @timeit "compact 1" ir = compact!(ir) - @timeit "Inlining" ir = ssa_inlining_pass!(ir, ir.linetable, sv.inlining, ci.propagate_inbounds) - #@timeit "verify 2" verify_ir(ir) - ir = compact!(ir) - #@Base.show ("before_sroa", ir) - @timeit "SROA" ir = getfield_elim_pass!(ir) - #@Base.show ir.new_nodes - #@Base.show ("after_sroa", ir) - ir = adce_pass!(ir) - #@Base.show ("after_adce", ir) - @timeit "type lift" ir = type_lift_pass!(ir) - @timeit "compact 3" ir = compact!(ir) - #@Base.show ir - if JLOptions().debug_level == 2 - @timeit "verify 3" (verify_ir(ir); verify_linetable(ir.linetable)) - end - return ir -end +include("compiler/ssair/EscapeAnalysis/EscapeAnalysis.jl") +include("compiler/ssair/passes.jl") diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 4a8e5f5e0a622f..1738c05678211b 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -2,22 +2,12 @@ @nospecialize -struct InvokeData - entry::Method - types0 - min_valid::UInt - max_valid::UInt -end - struct Signature f::Any ft::Any - atypes::Vector{Any} - atype::Type - Signature(f, ft, atypes) = new(f, ft, atypes) - Signature(f, ft, atypes, atype) = new(f, ft, atypes, atype) + argtypes::Vector{Any} + Signature(@nospecialize(f), @nospecialize(ft), argtypes::Vector{Any}) = new(f, ft, argtypes) end -with_atype(sig::Signature) = Signature(sig.f, sig.ft, sig.atypes, argtypes_to_type(sig.atypes)) struct ResolvedInliningSpec # The LineTable and IR of the inlinee @@ -25,17 +15,18 @@ struct ResolvedInliningSpec # If the function being inlined is a single basic block we can use a # simpler inlining algorithm. This flag determines whether that's allowed linear_inline_eligible::Bool + # Effects of the call statement + effects::Effects end """ - Represents a callsite that our analysis has determined is legal to inline, - but did not resolve during the analysis step to allow the outer inlining - pass to apply its own inlining policy decisions. +Represents a callsite that our analysis has determined is legal to inline, +but did not resolve during the analysis step to allow the outer inlining +pass to apply its own inlining policy decisions. """ struct DelayedInliningSpec - match::MethodMatch - atypes::Vector{Any} - stmttype::Any + match::Union{MethodMatch, InferenceResult} + argtypes::Vector{Any} end struct InliningTodo @@ -44,19 +35,42 @@ struct InliningTodo spec::Union{ResolvedInliningSpec, DelayedInliningSpec} end -InliningTodo(mi::MethodInstance, match::MethodMatch, atypes::Vector{Any}, @nospecialize(stmttype)) = InliningTodo(mi, DelayedInliningSpec(match, atypes, stmttype)) +InliningTodo(mi::MethodInstance, match::MethodMatch, argtypes::Vector{Any}) = + InliningTodo(mi, DelayedInliningSpec(match, argtypes)) + +InliningTodo(result::InferenceResult, argtypes::Vector{Any}) = + InliningTodo(result.linfo, DelayedInliningSpec(result, argtypes)) struct ConstantCase val::Any ConstantCase(val) = new(val) end +struct SomeCase + val::Any + SomeCase(val) = new(val) +end + +struct InvokeCase + invoke::MethodInstance + effects::Effects +end + +struct InliningCase + sig # Type + item # Union{InliningTodo, MethodInstance, ConstantCase} + function InliningCase(@nospecialize(sig), @nospecialize(item)) + @assert isa(item, Union{InliningTodo, InvokeCase, ConstantCase}) "invalid inlining item" + return new(sig, item) + end +end + struct UnionSplit fully_covered::Bool - atype # ::Type - cases::Vector{Pair{Any, Any}} + atype::DataType + cases::Vector{InliningCase} bbs::Vector{Int} - UnionSplit(fully_covered::Bool, atype, cases::Vector{Pair{Any, Any}}) = + UnionSplit(fully_covered::Bool, atype::DataType, cases::Vector{InliningCase}) = new(fully_covered, atype, cases, Int[]) end @@ -68,13 +82,12 @@ function ssa_inlining_pass!(ir::IRCode, linetable::Vector{LineInfoNode}, state:: @timeit "analysis" todo = assemble_inline_todo!(ir, state) isempty(todo) && return ir # Do the actual inlining for every call we identified - @timeit "execution" ir = batch_inline!(todo, ir, linetable, propagate_inbounds) + @timeit "execution" ir = batch_inline!(todo, ir, linetable, propagate_inbounds, state.params) return ir end mutable struct CFGInliningState new_cfg_blocks::Vector{BasicBlock} - inserted_block_ranges::Vector{UnitRange{Int}} todo_bbs::Vector{Tuple{Int, Int}} first_bb::Int bb_rename::Vector{Int} @@ -87,7 +100,6 @@ end function CFGInliningState(ir::IRCode) CFGInliningState( BasicBlock[], - UnitRange{Int}[], Tuple{Int, Int}[], 0, zeros(Int, length(ir.cfg.blocks)), @@ -99,25 +111,25 @@ function CFGInliningState(ir::IRCode) end # Tells the inliner that we're now inlining into block `block`, meaning -# all previous blocks have been proceesed and can be added to the new cfg +# all previous blocks have been processed and can be added to the new cfg function inline_into_block!(state::CFGInliningState, block::Int) if state.first_bb != block new_range = state.first_bb+1:block l = length(state.new_cfg_blocks) state.bb_rename[new_range] = (l+1:l+length(new_range)) - append!(state.new_cfg_blocks, map(copy, state.cfg.blocks[new_range])) + append!(state.new_cfg_blocks, (copy(block) for block in state.cfg.blocks[new_range])) push!(state.merged_orig_blocks, last(new_range)) end state.first_bb = block return end -function cfg_inline_item!(idx::Int, spec::ResolvedInliningSpec, state::CFGInliningState, from_unionsplit::Bool=false) +function cfg_inline_item!(ir::IRCode, idx::Int, spec::ResolvedInliningSpec, state::CFGInliningState, from_unionsplit::Bool=false) inlinee_cfg = spec.ir.cfg # Figure out if we need to split the BB need_split_before = false need_split = true - block = block_for_inst(state.cfg, idx) + block = block_for_inst(ir, idx) inline_into_block!(state, block) if !isempty(inlinee_cfg.blocks[1].preds) @@ -127,20 +139,19 @@ function cfg_inline_item!(idx::Int, spec::ResolvedInliningSpec, state::CFGInlini last_block_idx = last(state.cfg.blocks[block].stmts) if false # TODO: ((idx+1) == last_block_idx && isa(ir[SSAValue(last_block_idx)], GotoNode)) need_split = false - post_bb_id = -ir[SSAValue(last_block_idx)].label + post_bb_id = -ir[SSAValue(last_block_idx)][:inst].label else post_bb_id = length(state.new_cfg_blocks) + length(inlinee_cfg.blocks) + (need_split_before ? 1 : 0) need_split = true #!(idx == last_block_idx) end - if !need_split - delete!(state.merged_orig_blocks, last(new_range)) - end + need_split || delete!(state.merged_orig_blocks, last(new_range)) push!(state.todo_bbs, (length(state.new_cfg_blocks) - 1 + (need_split_before ? 1 : 0), post_bb_id)) from_unionsplit || delete!(state.split_targets, length(state.new_cfg_blocks)) - orig_succs = copy(state.new_cfg_blocks[end].succs) + local orig_succs + need_split && (orig_succs = copy(state.new_cfg_blocks[end].succs)) empty!(state.new_cfg_blocks[end].succs) if need_split_before l = length(state.new_cfg_blocks) @@ -160,7 +171,6 @@ function cfg_inline_item!(idx::Int, spec::ResolvedInliningSpec, state::CFGInlini from_unionsplit || push!(state.split_targets, length(state.new_cfg_blocks)) end new_block_range = (length(state.new_cfg_blocks)-length(inlinee_cfg.blocks)+1):length(state.new_cfg_blocks) - push!(state.inserted_block_ranges, new_block_range) # Fixup the edges of the newely added blocks for (old_block, new_block) in enumerate(bb_rename_range) @@ -190,7 +200,7 @@ function cfg_inline_item!(idx::Int, spec::ResolvedInliningSpec, state::CFGInlini for (old_block, new_block) in enumerate(bb_rename_range) if (length(state.new_cfg_blocks[new_block].succs) == 0) terminator_idx = last(inlinee_cfg.blocks[old_block].stmts) - terminator = spec.ir[SSAValue(terminator_idx)] + terminator = spec.ir[SSAValue(terminator_idx)][:inst] if isa(terminator, ReturnNode) && isdefined(terminator, :val) any_edges = true push!(state.new_cfg_blocks[new_block].succs, post_bb_id) @@ -200,53 +210,52 @@ function cfg_inline_item!(idx::Int, spec::ResolvedInliningSpec, state::CFGInlini end end end + any_edges || push!(state.dead_blocks, post_bb_id) - if !any_edges - push!(state.dead_blocks, post_bb_id) - end + return nothing end -function cfg_inline_unionsplit!(idx::Int, item::UnionSplit, state::CFGInliningState) - block = block_for_inst(state.cfg, idx) - inline_into_block!(state, block) +function cfg_inline_unionsplit!(ir::IRCode, idx::Int, + (; fully_covered, #=atype,=# cases, bbs)::UnionSplit, + state::CFGInliningState, + params::OptimizationParams) + inline_into_block!(state, block_for_inst(ir, idx)) from_bbs = Int[] delete!(state.split_targets, length(state.new_cfg_blocks)) orig_succs = copy(state.new_cfg_blocks[end].succs) empty!(state.new_cfg_blocks[end].succs) - for (i, (_, case)) in enumerate(item.cases) + for i in 1:length(cases) # The condition gets sunk into the previous block # Add a block for the union-split body push!(state.new_cfg_blocks, BasicBlock(StmtRange(idx, idx))) cond_bb = length(state.new_cfg_blocks)-1 push!(state.new_cfg_blocks[end].preds, cond_bb) push!(state.new_cfg_blocks[cond_bb].succs, cond_bb+1) + case = cases[i].item if isa(case, InliningTodo) spec = case.spec::ResolvedInliningSpec if !spec.linear_inline_eligible - cfg_inline_item!(idx, spec, state, true) + cfg_inline_item!(ir, idx, spec, state, true) end end - bb = length(state.new_cfg_blocks) - push!(from_bbs, bb) + push!(from_bbs, length(state.new_cfg_blocks)) # TODO: Right now we unconditionally generate a fallback block # in case of subtyping errors - This is probably unnecessary. - if true # i != length(item.cases) || !item.fully_covered + if i != length(cases) || (!fully_covered || (!params.trust_inference)) # This block will have the next condition or the final else case push!(state.new_cfg_blocks, BasicBlock(StmtRange(idx, idx))) push!(state.new_cfg_blocks[cond_bb].succs, length(state.new_cfg_blocks)) push!(state.new_cfg_blocks[end].preds, cond_bb) - push!(item.bbs, length(state.new_cfg_blocks)) + push!(bbs, length(state.new_cfg_blocks)) end end # The edge from the fallback block. - if !item.fully_covered - push!(from_bbs, length(state.new_cfg_blocks)) - end + fully_covered || push!(from_bbs, length(state.new_cfg_blocks)) # This block will be the block everyone returns to push!(state.new_cfg_blocks, BasicBlock(StmtRange(idx, idx), from_bbs, orig_succs)) join_bb = length(state.new_cfg_blocks) push!(state.split_targets, join_bb) - push!(item.bbs, join_bb) + push!(bbs, join_bb) for bb in from_bbs push!(state.new_cfg_blocks[bb].succs, join_bb) end @@ -254,8 +263,10 @@ end function finish_cfg_inline!(state::CFGInliningState) new_range = (state.first_bb + 1):length(state.cfg.blocks) - l = length(state.new_cfg_blocks) - state.bb_rename[new_range] = (l+1:l+length(new_range)) + state.bb_rename[new_range] = let + l = length(state.new_cfg_blocks) + l+1:l+length(new_range) + end append!(state.new_cfg_blocks, state.cfg.blocks[new_range]) # Rename edges original bbs @@ -300,34 +311,60 @@ function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector boundscheck::Symbol, todo_bbs::Vector{Tuple{Int, Int}}) # Ok, do the inlining here spec = item.spec::ResolvedInliningSpec - inline_cfg = spec.ir.cfg - stmt = compact.result[idx][:inst] + sparam_vals = item.mi.sparam_vals + def = item.mi.def::Method linetable_offset::Int32 = length(linetable) # Append the linetable of the inlined function to our line table - inlined_at = Int(compact.result[idx][:line]) - for entry in spec.ir.linetable - push!(linetable, LineInfoNode(entry.module, entry.method, entry.file, entry.line, - (entry.inlined_at > 0 ? entry.inlined_at + linetable_offset : inlined_at))) - end - nargs_def = item.mi.def.nargs - isva = nargs_def > 0 && item.mi.def.isva - if isva - vararg = mk_tuplecall!(compact, argexprs[nargs_def:end], compact.result[idx][:line]) - argexprs = Any[argexprs[1:(nargs_def - 1)]..., vararg] - end - flag = compact.result[idx][:flag] - boundscheck_idx = boundscheck - if boundscheck_idx === :default || boundscheck_idx === :propagate - if (flag & IR_FLAG_INBOUNDS) != 0 - boundscheck_idx = :off + inlined_at = compact.result[idx][:line] + topline::Int32 = linetable_offset + Int32(1) + coverage = coverage_enabled(def.module) + coverage_by_path = JLOptions().code_coverage == 3 + push!(linetable, LineInfoNode(def.module, def.name, def.file, def.line, inlined_at)) + oldlinetable = spec.ir.linetable + for oldline in 1:length(oldlinetable) + entry = oldlinetable[oldline] + if !coverage && coverage_by_path && is_file_tracked(entry.file) + # include topline coverage entry if in path-specific coverage mode, and any file falls under path + coverage = true + end + newentry = LineInfoNode(entry.module, entry.method, entry.file, entry.line, + (entry.inlined_at > 0 ? entry.inlined_at + linetable_offset + (oldline == 1) : inlined_at)) + if oldline == 1 + # check for a duplicate on the first iteration (likely true) + if newentry === linetable[topline] + continue + else + linetable_offset += 1 + end + end + push!(linetable, newentry) + end + if coverage && spec.ir.stmts[1][:line] + linetable_offset != topline + insert_node_here!(compact, NewInstruction(Expr(:code_coverage_effect), Nothing, topline)) + end + if def.isva + nargs_def = Int(def.nargs::Int32) + if nargs_def > 0 + argexprs = fix_va_argexprs!(compact, argexprs, nargs_def, topline) + end + end + if def.is_for_opaque_closure + # Replace the first argument by a load of the capture environment + argexprs[1] = insert_node_here!(compact, + NewInstruction(Expr(:call, GlobalRef(Core, :getfield), argexprs[1], QuoteNode(:captures)), + spec.ir.argtypes[1], topline)) + end + if boundscheck === :default || boundscheck === :propagate + if (compact.result[idx][:flag] & IR_FLAG_INBOUNDS) != 0 + boundscheck = :off end end # If the iterator already moved on to the next basic block, # temporarily re-open in again. local return_value + sig = def.sig # Special case inlining that maintains the current basic block if there's only one BB in the target if spec.linear_inline_eligible - terminator = spec.ir[SSAValue(last(inline_cfg.blocks[1].stmts))] #compact[idx] = nothing inline_compact = IncrementalCompact(compact, spec.ir, compact.result_idx) for ((_, idx′), stmt′) in inline_compact @@ -335,15 +372,13 @@ function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector # face of rename_arguments! mutating in place - should figure out # something better eventually. inline_compact[idx′] = nothing - stmt′ = ssa_substitute!(idx′, stmt′, argexprs, item.mi.def.sig, item.mi.sparam_vals, linetable_offset, boundscheck_idx, compact) + stmt′ = ssa_substitute!(idx′, stmt′, argexprs, sig, sparam_vals, linetable_offset, boundscheck, compact) if isa(stmt′, ReturnNode) - isa(stmt′.val, SSAValue) && (compact.used_ssas[stmt′.val.id] += 1) - return_value = SSAValue(idx′) - inline_compact[idx′] = stmt′.val val = stmt′.val - inline_compact.result[idx′][:type] = (isa(val, Argument) || isa(val, Expr)) ? - compact_exprtype(compact, stmt′.val) : - compact_exprtype(inline_compact, stmt′.val) + return_value = SSAValue(idx′) + inline_compact[idx′] = val + inline_compact.result[idx′][:type] = + argextype(val, isa(val, Argument) || isa(val, Expr) ? compact : inline_compact) break end inline_compact[idx′] = stmt′ @@ -362,7 +397,7 @@ function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector inline_compact = IncrementalCompact(compact, spec.ir, compact.result_idx) for ((_, idx′), stmt′) in inline_compact inline_compact[idx′] = nothing - stmt′ = ssa_substitute!(idx′, stmt′, argexprs, item.mi.def.sig, item.mi.sparam_vals, linetable_offset, boundscheck_idx, compact) + stmt′ = ssa_substitute!(idx′, stmt′, argexprs, sig, sparam_vals, linetable_offset, boundscheck, compact) if isa(stmt′, ReturnNode) if isdefined(stmt′, :val) val = stmt′.val @@ -371,23 +406,21 @@ function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector push!(pn.edges, inline_compact.active_result_bb-1) if isa(val, GlobalRef) || isa(val, Expr) stmt′ = val - inline_compact.result[idx′][:type] = (isa(val, Argument) || isa(val, Expr)) ? - compact_exprtype(compact, val) : - compact_exprtype(inline_compact, val) - insert_node_here!(inline_compact, GotoNode(post_bb_id), - Any, compact.result[idx′][:line], + inline_compact.result[idx′][:type] = + argextype(val, isa(val, Expr) ? compact : inline_compact) + insert_node_here!(inline_compact, NewInstruction(GotoNode(post_bb_id), + Any, compact.result[idx′][:line]), true) push!(pn.values, SSAValue(idx′)) else push!(pn.values, val) stmt′ = GotoNode(post_bb_id) end - end elseif isa(stmt′, GotoNode) stmt′ = GotoNode(stmt′.label + bb_offset) elseif isa(stmt′, Expr) && stmt′.head === :enter - stmt′ = Expr(:enter, stmt′.args[1] + bb_offset) + stmt′ = Expr(:enter, stmt′.args[1]::Int + bb_offset) elseif isa(stmt′, GotoIfNot) stmt′ = GotoIfNot(stmt′.cond, stmt′.dest + bb_offset) elseif isa(stmt′, PhiNode) @@ -398,71 +431,151 @@ function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector just_fixup!(inline_compact) compact.result_idx = inline_compact.result_idx compact.active_result_bb = inline_compact.active_result_bb - for i = 1:length(pn.values) - isassigned(pn.values, i) || continue - if isa(pn.values[i], SSAValue) - compact.used_ssas[pn.values[i].id] += 1 - end - end if length(pn.edges) == 1 return_value = pn.values[1] else - return_value = insert_node_here!(compact, pn, compact_exprtype(compact, SSAValue(idx)), compact.result[idx][:line]) + return_value = insert_node_here!(compact, + NewInstruction(pn, argextype(SSAValue(idx), compact), compact.result[idx][:line])) end end return_value end -const fatal_type_bound_error = ErrorException("fatal error in type inference (type bound)") +function fix_va_argexprs!(compact::IncrementalCompact, + argexprs::Vector{Any}, nargs_def::Int, line_idx::Int32) + newargexprs = argexprs[1:(nargs_def-1)] + tuple_call = Expr(:call, TOP_TUPLE) + tuple_typs = Any[] + for i in nargs_def:length(argexprs) + arg = argexprs[i] + push!(tuple_call.args, arg) + push!(tuple_typs, argextype(arg, compact)) + end + tuple_typ = tuple_tfunc(tuple_typs) + push!(newargexprs, insert_node_here!(compact, NewInstruction(tuple_call, tuple_typ, line_idx))) + return newargexprs +end + +const FATAL_TYPE_BOUND_ERROR = ErrorException("fatal error in type inference (type bound)") + +""" + ir_inline_unionsplit! + +The core idea of this function is to simulate the dispatch semantics by generating +(flat) `isa`-checks corresponding to the signatures of union-split dispatch candidates, +and then inline their bodies into each `isa`-conditional block. +This `isa`-based virtual dispatch requires few pre-conditions to hold in order to simulate +the actual semantics correctly. + +The first one is that these dispatch candidates need to be processed in order of their specificity, +and the corresponding `isa`-checks should reflect the method specificities, since now their +signatures are not necessarily concrete. +For example, given the following definitions: + + f(x::Int) = ... + f(x::Number) = ... + f(x::Any) = ... + +and a callsite: + + f(x::Any) + +then a correct `isa`-based virtual dispatch would be: + + if isa(x, Int) + [inlined/resolved f(x::Int)] + elseif isa(x, Number) + [inlined/resolved f(x::Number)] + else # implies `isa(x, Any)`, which fully covers this call signature, + # otherwise we need to insert a fallback dynamic dispatch case also + [inlined/resolved f(x::Any)] + end +Fortunately, `ml_matches` should already sorted them in that way, except cases when there is +any ambiguity, from which we already bail out at this point. + +Another consideration is type equality constraint from type variables: the `isa`-checks are +not enough to simulate the dispatch semantics in cases like: +Given a definition: + + g(x::T, y::T) where T<:Integer = ... + +transform a callsite: + + g(x::Any, y::Any) + +into the optimized form: + + if isa(x, Integer) && isa(y, Integer) + [inlined/resolved g(x::Integer, y::Integer)] + else + g(x, y) # fallback dynamic dispatch + end + +But again, we should already bail out from such cases at this point, essentially by +excluding cases where `case.sig::UnionAll`. + +In short, here we can process the dispatch candidates in order, assuming we haven't changed +their order somehow somewhere up to this point. +""" function ir_inline_unionsplit!(compact::IncrementalCompact, idx::Int, argexprs::Vector{Any}, linetable::Vector{LineInfoNode}, - item::UnionSplit, boundscheck::Symbol, todo_bbs::Vector{Tuple{Int, Int}}) + (; fully_covered, atype, cases, bbs)::UnionSplit, + boundscheck::Symbol, todo_bbs::Vector{Tuple{Int, Int}}, + params::OptimizationParams) stmt, typ, line = compact.result[idx][:inst], compact.result[idx][:type], compact.result[idx][:line] - atype = item.atype - generic_bb = item.bbs[end-1] - join_bb = item.bbs[end] - bb = compact.active_result_bb + join_bb = bbs[end] pn = PhiNode() - has_generic = false - @assert length(item.bbs) > length(item.cases) - for ((metharg, case), next_cond_bb) in zip(item.cases, item.bbs) - @assert !isa(metharg, UnionAll) + local bb = compact.active_result_bb + ncases = length(cases) + @assert length(bbs) >= ncases + for i = 1:ncases + ithcase = cases[i] + mtype = ithcase.sig::DataType # checked within `handle_cases!` + case = ithcase.item + next_cond_bb = bbs[i] cond = true - @assert length(atype.parameters) == length(metharg.parameters) - for i in 1:length(atype.parameters) - a, m = atype.parameters[i], metharg.parameters[i] - # If this is always true, we don't need to check for it - a <: m && continue - # Generate isa check - isa_expr = Expr(:call, isa, argexprs[i], m) - ssa = insert_node_here!(compact, isa_expr, Bool, line) - if cond === true - cond = ssa - else - and_expr = Expr(:call, and_int, cond, ssa) - cond = insert_node_here!(compact, and_expr, Bool, line) + nparams = fieldcount(atype) + @assert nparams == fieldcount(mtype) + if i != ncases || !fully_covered || !params.trust_inference + for i = 1:nparams + a, m = fieldtype(atype, i), fieldtype(mtype, i) + # If this is always true, we don't need to check for it + a <: m && continue + # Generate isa check + isa_expr = Expr(:call, isa, argexprs[i], m) + ssa = insert_node_here!(compact, NewInstruction(isa_expr, Bool, line)) + if cond === true + cond = ssa + else + and_expr = Expr(:call, and_int, cond, ssa) + cond = insert_node_here!(compact, NewInstruction(and_expr, Bool, line)) + end end + insert_node_here!(compact, NewInstruction(GotoIfNot(cond, next_cond_bb), Union{}, line)) end - insert_node_here!(compact, GotoIfNot(cond, next_cond_bb), Union{}, line) bb = next_cond_bb - 1 finish_current_bb!(compact, 0) argexprs′ = argexprs if !isa(case, ConstantCase) argexprs′ = copy(argexprs) - for i = 1:length(metharg.parameters) - a, m = atype.parameters[i], metharg.parameters[i] - (isa(argexprs[i], SSAValue) || isa(argexprs[i], Argument)) || continue + for i = 1:nparams + argex = argexprs[i] + (isa(argex, SSAValue) || isa(argex, Argument)) || continue + a, m = fieldtype(atype, i), fieldtype(mtype, i) if !(a <: m) - argexprs′[i] = insert_node_here!(compact, PiNode(argexprs′[i], m), - m, line) + argexprs′[i] = insert_node_here!(compact, + NewInstruction(PiNode(argex, m), m, line)) end end end if isa(case, InliningTodo) val = ir_inline_item!(compact, idx, argexprs′, linetable, case, boundscheck, todo_bbs) - elseif isa(case, MethodInstance) - val = insert_node_here!(compact, Expr(:invoke, case, argexprs′...), typ, line) + elseif isa(case, InvokeCase) + effect_free = is_removable_if_unused(case.effects) + val = insert_node_here!(compact, + NewInstruction(Expr(:invoke, case.invoke, argexprs′...), typ, nothing, + line, effect_free ? IR_FLAG_EFFECT_FREE : IR_FLAG_NULL, effect_free)) else case = case::ConstantCase val = case.val @@ -470,44 +583,47 @@ function ir_inline_unionsplit!(compact::IncrementalCompact, idx::Int, if !isempty(compact.result_bbs[bb].preds) push!(pn.edges, bb) push!(pn.values, val) - insert_node_here!(compact, GotoNode(join_bb), Union{}, line) + insert_node_here!(compact, + NewInstruction(GotoNode(join_bb), Union{}, line)) else - insert_node_here!(compact, ReturnNode(), Union{}, line) + insert_node_here!(compact, + NewInstruction(ReturnNode(), Union{}, line)) end finish_current_bb!(compact, 0) end bb += 1 # We're now in the fall through block, decide what to do - if item.fully_covered - e = Expr(:call, GlobalRef(Core, :throw), fatal_type_bound_error) - insert_node_here!(compact, e, Union{}, line) - insert_node_here!(compact, ReturnNode(), Union{}, line) - finish_current_bb!(compact, 0) + if fully_covered + if !params.trust_inference + e = Expr(:call, GlobalRef(Core, :throw), FATAL_TYPE_BOUND_ERROR) + insert_node_here!(compact, NewInstruction(e, Union{}, line)) + insert_node_here!(compact, NewInstruction(ReturnNode(), Union{}, line)) + finish_current_bb!(compact, 0) + end else - ssa = insert_node_here!(compact, stmt, typ, line) + ssa = insert_node_here!(compact, NewInstruction(stmt, typ, line)) push!(pn.edges, bb) push!(pn.values, ssa) - insert_node_here!(compact, GotoNode(join_bb), Union{}, line) + insert_node_here!(compact, NewInstruction(GotoNode(join_bb), Union{}, line)) finish_current_bb!(compact, 0) end # We're now in the join block. - compact.ssa_rename[compact.idx-1] = insert_node_here!(compact, pn, typ, line) - nothing + return insert_node_here!(compact, NewInstruction(pn, typ, line)) end -function batch_inline!(todo::Vector{Pair{Int, Any}}, ir::IRCode, linetable::Vector{LineInfoNode}, propagate_inbounds::Bool) +function batch_inline!(todo::Vector{Pair{Int, Any}}, ir::IRCode, linetable::Vector{LineInfoNode}, propagate_inbounds::Bool, params::OptimizationParams) # Compute the new CFG first (modulo statement ranges, which will be computed below) state = CFGInliningState(ir) for (idx, item) in todo if isa(item, UnionSplit) - cfg_inline_unionsplit!(idx, item::UnionSplit, state) + cfg_inline_unionsplit!(ir, idx, item, state, params) else item = item::InliningTodo spec = item.spec::ResolvedInliningSpec # A linear inline does not modify the CFG spec.linear_inline_eligible && continue - cfg_inline_item!(idx, spec, state, false) + cfg_inline_item!(ir, idx, spec, state, false) end end finish_cfg_inline!(state) @@ -532,23 +648,29 @@ function batch_inline!(todo::Vector{Pair{Int, Any}}, ir::IRCode, linetable::Vect (inline_idx, item) = popfirst!(todo) for ((old_idx, idx), stmt) in compact if old_idx == inline_idx + stmt = stmt::Expr argexprs = copy(stmt.args) refinish = false if compact.result_idx == first(compact.result_bbs[compact.active_result_bb].stmts) compact.active_result_bb -= 1 refinish = true end - # At the moment we will allow globalrefs in argument position, turn those into ssa values + # It is possible for GlobalRefs and Exprs to be in argument position + # at this point in the IR, though in that case they are required + # to be effect-free. However, we must still move them out of argument + # position, since `Argument` is allowed in PhiNodes, but `GlobalRef` + # and `Expr` are not, so a substitution could anger the verifier. for aidx in 1:length(argexprs) aexpr = argexprs[aidx] - if isa(aexpr, GlobalRef) || isa(aexpr, Expr) - argexprs[aidx] = insert_node_here!(compact, aexpr, compact_exprtype(compact, aexpr), compact.result[idx][:line]) + if isa(aexpr, Expr) || isa(aexpr, GlobalRef) + ninst = effect_free(NewInstruction(aexpr, argextype(aexpr, compact), compact.result[idx][:line])) + argexprs[aidx] = insert_node_here!(compact, ninst) end end if isa(item, InliningTodo) compact.ssa_rename[old_idx] = ir_inline_item!(compact, idx, argexprs, linetable, item, boundscheck, state.todo_bbs) elseif isa(item, UnionSplit) - ir_inline_unionsplit!(compact, idx, argexprs, linetable, item, boundscheck, state.todo_bbs) + compact.ssa_rename[old_idx] = ir_inline_unionsplit!(compact, idx, argexprs, linetable, item, boundscheck, state.todo_bbs, params) end compact[idx] = nothing refinish && finish_current_bb!(compact, 0) @@ -560,7 +682,7 @@ function batch_inline!(todo::Vector{Pair{Int, Any}}, ir::IRCode, linetable::Vect elseif isa(stmt, GotoNode) compact[idx] = GotoNode(state.bb_rename[stmt.label]) elseif isa(stmt, Expr) && stmt.head === :enter - compact[idx] = Expr(:enter, state.bb_rename[stmt.args[1]]) + compact[idx] = Expr(:enter, state.bb_rename[stmt.args[1]::Int]) elseif isa(stmt, GotoIfNot) compact[idx] = GotoIfNot(stmt.cond, state.bb_rename[stmt.dest]) elseif isa(stmt, PhiNode) @@ -573,33 +695,33 @@ function batch_inline!(todo::Vector{Pair{Int, Any}}, ir::IRCode, linetable::Vect return ir end -# This assumes the caller has verified that all arguments to the _apply call are Tuples. -function rewrite_apply_exprargs!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx::Int, - argexprs::Vector{Any}, atypes::Vector{Any}, arginfos::Vector{Any}, - arg_start::Int, et::Union{EdgeTracker, Nothing}, caches::Union{InferenceCaches, Nothing}, - params::OptimizationParams) - +# This assumes the caller has verified that all arguments to the _apply_iterate call are Tuples. +function rewrite_apply_exprargs!( + ir::IRCode, idx::Int, stmt::Expr, argtypes::Vector{Any}, + arginfos::Vector{MaybeAbstractIterationInfo}, arg_start::Int, istate::InliningState, todo::Vector{Pair{Int, Any}}) + flag = ir.stmts[idx][:flag] + argexprs = stmt.args new_argexprs = Any[argexprs[arg_start]] - new_atypes = Any[atypes[arg_start]] + new_argtypes = Any[argtypes[arg_start]] # loop over original arguments and flatten any known iterators for i in (arg_start+1):length(argexprs) def = argexprs[i] - def_type = atypes[i] + def_type = argtypes[i] thisarginfo = arginfos[i-arg_start] if thisarginfo === nothing if def_type isa PartialStruct # def_type.typ <: Tuple is assumed - def_atypes = def_type.fields + def_argtypes = def_type.fields else - def_atypes = Any[] + def_argtypes = Any[] if isa(def_type, Const) # && isa(def_type.val, Union{Tuple, SimpleVector}) is implied for p in def_type.val - push!(def_atypes, Const(p)) + push!(def_argtypes, Const(p)) end else - ti = widenconst(def_type) + ti = widenconst(def_type)::DataType # checked by `is_valid_type_for_apply_rewrite` if ti.name === NamedTuple_typename - ti = ti.parameters[2] + ti = ti.parameters[2]::DataType # checked by `is_valid_type_for_apply_rewrite` end for p in ti.parameters if isa(p, DataType) && isdefined(p, :instance) @@ -608,125 +730,157 @@ function rewrite_apply_exprargs!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx:: elseif isconstType(p) p = Const(p.parameters[1]) end - push!(def_atypes, p) + push!(def_argtypes, p) end end end - # now push flattened types into new_atypes and getfield exprs into new_argexprs - for j in 1:length(def_atypes) - def_atype = def_atypes[j] + # now push flattened types into new_argtypes and getfield exprs into new_argexprs + for j in 1:length(def_argtypes) + def_atype = def_argtypes[j] if isa(def_atype, Const) && is_inlineable_constant(def_atype.val) new_argexpr = quoted(def_atype.val) else new_call = Expr(:call, GlobalRef(Core, :getfield), def, j) - new_argexpr = insert_node!(ir, idx, def_atype, new_call) + new_argexpr = insert_node!(ir, idx, NewInstruction(new_call, def_atype)) end push!(new_argexprs, new_argexpr) - push!(new_atypes, def_atype) + push!(new_argtypes, def_atype) end else state = Core.svec() for i = 1:length(thisarginfo.each) call = thisarginfo.each[i] new_stmt = Expr(:call, argexprs[2], def, state...) - state1 = insert_node!(ir, idx, call.rt, new_stmt) - new_sig = with_atype(call_sig(ir, new_stmt)) - if isa(call.info, MethodMatchInfo) || isa(call.info, UnionSplitInfo) - info = isa(call.info, MethodMatchInfo) ? - MethodMatchInfo[call.info] : call.info.matches + state1 = insert_node!(ir, idx, NewInstruction(new_stmt, call.rt)) + new_sig = call_sig(ir, new_stmt)::Signature + new_info = call.info + if isa(new_info, ConstCallInfo) + handle_const_call!( + ir, state1.id, new_stmt, new_info, flag, + new_sig, istate, todo) + elseif isa(new_info, MethodMatchInfo) || isa(new_info, UnionSplitInfo) + new_infos = isa(new_info, MethodMatchInfo) ? MethodMatchInfo[new_info] : new_info.matches # See if we can inline this call to `iterate` - analyze_single_call!(ir, todo, state1.id, new_stmt, - new_sig, call.rt, info, et, caches, params) + analyze_single_call!( + ir, state1.id, new_stmt, new_infos, flag, + new_sig, istate, todo) end if i != length(thisarginfo.each) valT = getfield_tfunc(call.rt, Const(1)) - val_extracted = insert_node!(ir, idx, valT, - Expr(:call, GlobalRef(Core, :getfield), state1, 1)) + val_extracted = insert_node!(ir, idx, NewInstruction( + Expr(:call, GlobalRef(Core, :getfield), state1, 1), + valT)) push!(new_argexprs, val_extracted) - push!(new_atypes, valT) - state_extracted = insert_node!(ir, idx, getfield_tfunc(call.rt, Const(2)), - Expr(:call, GlobalRef(Core, :getfield), state1, 2)) + push!(new_argtypes, valT) + state_extracted = insert_node!(ir, idx, NewInstruction( + Expr(:call, GlobalRef(Core, :getfield), state1, 2), + getfield_tfunc(call.rt, Const(2)))) state = Core.svec(state_extracted) end end end end - return new_argexprs, new_atypes + stmt.args = new_argexprs + return new_argtypes end -function rewrite_invoke_exprargs!(argexprs::Vector{Any}) - argexpr0 = argexprs[2] - argexprs = argexprs[4:end] - pushfirst!(argexprs, argexpr0) - return argexprs +function compileable_specialization(et::Union{EdgeTracker, Nothing}, match::MethodMatch, effects::Effects) + mi = specialize_method(match; compilesig=true) + mi !== nothing && et !== nothing && push!(et, mi::MethodInstance) + mi === nothing && return nothing + return InvokeCase(mi, effects) end -function singleton_type(@nospecialize(ft)) - if isa(ft, Const) - return ft.val - elseif ft isa DataType && isdefined(ft, :instance) - return ft.instance - end - return nothing +function compileable_specialization(et::Union{EdgeTracker, Nothing}, linfo::MethodInstance, effects::Effects) + mi = specialize_method(linfo.def::Method, linfo.specTypes, linfo.sparam_vals; compilesig=true) + mi !== nothing && et !== nothing && push!(et, mi::MethodInstance) + mi === nothing && return nothing + return InvokeCase(mi, effects) end -function compileable_specialization(et::Union{EdgeTracker, Nothing}, match::MethodMatch) - mi = specialize_method(match, false, true) - mi !== nothing && et !== nothing && push!(et, mi::MethodInstance) - return mi +function compileable_specialization(et::Union{EdgeTracker, Nothing}, (; linfo)::InferenceResult, effects::Effects) + return compileable_specialization(et, linfo, effects) end -function resolve_todo(todo::InliningTodo, et::Union{EdgeTracker, Nothing}, caches::InferenceCaches) - spec = todo.spec::DelayedInliningSpec - isconst, src = find_inferred(todo.mi, spec.atypes, caches, spec.stmttype) +function resolve_todo(todo::InliningTodo, state::InliningState, flag::UInt8) + mi = todo.mi + (; match, argtypes) = todo.spec::DelayedInliningSpec + et = state.et + + #XXX: update_valid_age!(min_valid[1], max_valid[1], sv) + if isa(match, InferenceResult) + inferred_src = match.src + if isa(inferred_src, ConstAPI) + # use constant calling convention + et !== nothing && push!(et, mi) + return ConstantCase(quoted(inferred_src.val)) + else + src = inferred_src + end + effects = match.ipo_effects + else + code = get(state.mi_cache, mi, nothing) + if code isa CodeInstance + if use_const_api(code) + # in this case function can be inlined to a constant + et !== nothing && push!(et, mi) + return ConstantCase(quoted(code.rettype_const)) + else + src = code.inferred + end + effects = decode_effects(code.ipo_purity_bits) + else + effects = Effects() + src = code + end + end - if isconst - push!(et, todo.mi) - return ConstantCase(src) + # the duplicated check might have been done already within `analyze_method!`, but still + # we need it here too since we may come here directly using a constant-prop' result + if !state.params.inlining || is_stmt_noinline(flag) + return compileable_specialization(et, match, effects) end + src = inlining_policy(state.interp, src, flag, mi, argtypes) + if src === nothing - return compileable_specialization(et, spec.match) + return compileable_specialization(et, match, effects) end - if isa(src, CodeInfo) || isa(src, Vector{UInt8}) - src_inferred = ccall(:jl_ir_flag_inferred, Bool, (Any,), src) - src_inlineable = ccall(:jl_ir_flag_inlineable, Bool, (Any,), src) - - if !(src_inferred && src_inlineable) - return compileable_specialization(et, spec.match) - end - elseif isa(src, IRCode) + if isa(src, IRCode) src = copy(src) end - et !== nothing && push!(et, todo.mi) - return InliningTodo(todo.mi, src) + et !== nothing && push!(et, mi) + return InliningTodo(mi, src, effects) end -function resolve_todo(todo::UnionSplit, et::Union{EdgeTracker, Nothing}, caches::InferenceCaches) - UnionSplit(todo.fully_covered, todo.atype, - Pair{Any,Any}[sig=>resolve_todo(item, et, caches) for (sig, item) in todo.cases]) +function resolve_todo((; fully_covered, atype, cases, #=bbs=#)::UnionSplit, state::InliningState, flag::UInt8) + ncases = length(cases) + newcases = Vector{InliningCase}(undef, ncases) + for i in 1:ncases + (; sig, item) = cases[i] + newitem = resolve_todo(item, state, flag) + push!(newcases, InliningCase(sig, newitem)) + end + return UnionSplit(fully_covered, atype, newcases) end -function resolve_todo!(todo::Vector{Pair{Int, Any}}, et::Union{EdgeTracker, Nothing}, caches::InferenceCaches) - for i = 1:length(todo) - idx, item = todo[i] - todo[i] = idx=>resolve_todo(item, et, caches) +function validate_sparams(sparams::SimpleVector) + for i = 1:length(sparams) + (isa(sparams[i], TypeVar) || isvarargtype(sparams[i])) && return false end - todo + return true end -function analyze_method!(match::MethodMatch, atypes::Vector{Any}, - et::Union{EdgeTracker, Nothing}, - caches::Union{InferenceCaches, Nothing}, - params::OptimizationParams, @nospecialize(stmttyp)) +function analyze_method!(match::MethodMatch, argtypes::Vector{Any}, + flag::UInt8, state::InliningState) method = match.method - methsig = method.sig + spec_types = match.spec_types - # Check that we habe the correct number of arguments + # Check that we have the correct number of arguments na = Int(method.nargs) - npassedargs = length(atypes) + npassedargs = length(argtypes) if na != npassedargs && !(na > 0 && method.isva) # we have a method match only because an earlier # inference step shortened our call args list, even @@ -734,113 +888,72 @@ function analyze_method!(match::MethodMatch, atypes::Vector{Any}, # call this function return nothing end + if !match.fully_covers + # type-intersection was not able to give us a simple list of types, so + # ir_inline_unionsplit won't be able to deal with inlining this + if !(spec_types isa DataType && length(spec_types.parameters) == length(argtypes) && !isvarargtype(spec_types.parameters[end])) + return nothing + end + end # Bail out if any static parameters are left as TypeVar - ok = true - for i = 1:length(match.sparams) - isa(match.sparams[i], TypeVar) && return nothing - end + validate_sparams(match.sparams) || return nothing - if !params.inlining - return compileable_specialization(et, match) - end + et = state.et # See if there exists a specialization for this method signature - mi = specialize_method(match, true) # Union{Nothing, MethodInstance} - if !isa(mi, MethodInstance) - return compileable_specialization(et, match) - end + mi = specialize_method(match; preexisting=true) # Union{Nothing, MethodInstance} + isa(mi, MethodInstance) || return compileable_specialization(et, match, Effects()) - todo = InliningTodo(mi, match, atypes, stmttyp) + todo = InliningTodo(mi, match, argtypes) # If we don't have caches here, delay resolving this MethodInstance # until the batch inlining step (or an external post-processing pass) - caches === nothing && return todo - return resolve_todo(todo, et, caches) + state.mi_cache === nothing && return todo + return resolve_todo(todo, state, flag) end -function InliningTodo(mi::MethodInstance, ir::IRCode) - return InliningTodo(mi, ResolvedInliningSpec(ir, linear_inline_eligible(ir))) +function InliningTodo(mi::MethodInstance, ir::IRCode, effects::Effects) + return InliningTodo(mi, ResolvedInliningSpec(ir, linear_inline_eligible(ir), effects)) end -function InliningTodo(mi::MethodInstance, src::Union{CodeInfo, Array{UInt8, 1}}) +function InliningTodo(mi::MethodInstance, src::Union{CodeInfo, Array{UInt8, 1}}, effects::Effects) if !isa(src, CodeInfo) src = ccall(:jl_uncompress_ir, Any, (Any, Ptr{Cvoid}, Any), mi.def, C_NULL, src::Vector{UInt8})::CodeInfo end - @timeit "inline IR inflation" begin - return InliningTodo(mi, inflate_ir(src, mi)::IRCode) + @timeit "inline IR inflation" begin; + return InliningTodo(mi, inflate_ir(src, mi)::IRCode, effects) end end -# Neither the product iterator not CartesianIndices are available -# here, so use this poor man's version -struct SimpleCartesian - ranges::Vector{UnitRange{Int}} -end -function iterate(s::SimpleCartesian, state::Vector{Int}=Int[1 for _ in 1:length(s.ranges)]) - state[end] > last(s.ranges[end]) && return nothing - vals = copy(state) - any = false - for i = 1:length(s.ranges) - if state[i] < last(s.ranges[i]) - for j = 1:(i-1) - state[j] = first(s.ranges[j]) - end - state[i] += 1 - any = true - break - end - end - if !any - state[end] += 1 - end - (vals, state) -end - -# Given a signure, iterate over the signatures to union split over -struct UnionSplitSignature - it::SimpleCartesian - typs::Vector{Any} -end - -function UnionSplitSignature(atypes::Vector{Any}) - typs = Any[uniontypes(widenconst(atypes[i])) for i = 1:length(atypes)] - ranges = UnitRange{Int}[1:length(typs[i]) for i = 1:length(typs)] - return UnionSplitSignature(SimpleCartesian(ranges), typs) -end - -function iterate(split::UnionSplitSignature, state::Vector{Int}...) - y = iterate(split.it, state...) - y === nothing && return nothing - idxs, state = y - sig = Any[split.typs[i][j] for (i, j) in enumerate(idxs)] - return (sig, state) -end - -function handle_single_case!(ir::IRCode, stmt::Expr, idx::Int, @nospecialize(case), isinvoke::Bool, todo::Vector{Pair{Int, Any}}) +function handle_single_case!( + ir::IRCode, idx::Int, stmt::Expr, + @nospecialize(case), todo::Vector{Pair{Int, Any}}, params::OptimizationParams, isinvoke::Bool = false) if isa(case, ConstantCase) - ir[SSAValue(idx)] = case.val - elseif isa(case, MethodInstance) - if isinvoke - stmt.args = rewrite_invoke_exprargs!(stmt.args) - end + ir[SSAValue(idx)][:inst] = case.val + elseif isa(case, InvokeCase) + is_total(case.effects) && inline_const_if_inlineable!(ir[SSAValue(idx)]) && return nothing + isinvoke && rewrite_invoke_exprargs!(stmt) stmt.head = :invoke - pushfirst!(stmt.args, case) + pushfirst!(stmt.args, case.invoke) + if is_removable_if_unused(case.effects) + ir[SSAValue(idx)][:flag] |= IR_FLAG_EFFECT_FREE + end elseif case === nothing # Do, well, nothing else - if isinvoke - stmt.args = rewrite_invoke_exprargs!(stmt.args) - end + isinvoke && rewrite_invoke_exprargs!(stmt) push!(todo, idx=>(case::InliningTodo)) end nothing end +rewrite_invoke_exprargs!(expr::Expr) = (expr.args = invoke_rewrite(expr.args); expr) + function is_valid_type_for_apply_rewrite(@nospecialize(typ), params::OptimizationParams) - if isa(typ, Const) && isa(typ.val, SimpleVector) - length(typ.val) > params.MAX_TUPLE_SPLAT && return false - for p in typ.val + if isa(typ, Const) && (v = typ.val; isa(v, SimpleVector)) + length(v) > params.MAX_TUPLE_SPLAT && return false + for p in v is_inlineable_constant(p) || return false end return true @@ -848,9 +961,7 @@ function is_valid_type_for_apply_rewrite(@nospecialize(typ), params::Optimizatio typ = widenconst(typ) if isa(typ, DataType) && typ.name === NamedTuple_typename typ = typ.parameters[2] - while isa(typ, TypeVar) - typ = typ.ub - end + typ = unwraptv(typ) end isa(typ, DataType) || return false if typ.name === Tuple.name @@ -860,24 +971,22 @@ function is_valid_type_for_apply_rewrite(@nospecialize(typ), params::Optimizatio end end -function inline_splatnew!(ir::IRCode, idx::Int) - stmt = ir.stmts[idx][:inst] - ty = ir.stmts[idx][:type] - nf = nfields_tfunc(ty) +function inline_splatnew!(ir::IRCode, idx::Int, stmt::Expr, @nospecialize(rt)) + nf = nfields_tfunc(rt) if nf isa Const eargs = stmt.args tup = eargs[2] - tt = argextype(tup, ir, ir.sptypes) + tt = argextype(tup, ir) tnf = nfields_tfunc(tt) # TODO: hoisting this tnf.val === nf.val check into codegen # would enable us to almost always do this transform if tnf isa Const && tnf.val === nf.val - n = tnf.val + n = tnf.val::Int new_argexprs = Any[eargs[1]] for j = 1:n atype = getfield_tfunc(tt, Const(j)) new_call = Expr(:call, Core.getfield, tup, j) - new_argexpr = insert_node!(ir, idx, atype, new_call) + new_argexpr = insert_node!(ir, idx, NewInstruction(new_call, atype)) push!(new_argexprs, new_argexpr) end stmt.head = :new @@ -889,52 +998,50 @@ end function call_sig(ir::IRCode, stmt::Expr) isempty(stmt.args) && return nothing - ft = argextype(stmt.args[1], ir, ir.sptypes) + ft = argextype(stmt.args[1], ir) has_free_typevars(ft) && return nothing f = singleton_type(ft) f === Core.Intrinsics.llvmcall && return nothing f === Core.Intrinsics.cglobal && return nothing - atypes = Vector{Any}(undef, length(stmt.args)) - atypes[1] = ft - ok = true + argtypes = Vector{Any}(undef, length(stmt.args)) + argtypes[1] = ft for i = 2:length(stmt.args) - a = argextype(stmt.args[i], ir, ir.sptypes) + a = argextype(stmt.args[i], ir) (a === Bottom || isvarargtype(a)) && return nothing - atypes[i] = a + argtypes[i] = a end - - Signature(f, ft, atypes) + return Signature(f, ft, argtypes) end -function inline_apply!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx::Int, sig::Signature, - et, caches, params::OptimizationParams) - stmt = ir.stmts[idx][:inst] - while sig.f === Core._apply || sig.f === Core._apply_iterate +function inline_apply!( + ir::IRCode, idx::Int, stmt::Expr, sig::Signature, + state::InliningState, todo::Vector{Pair{Int, Any}}) + while sig.f === Core._apply_iterate info = ir.stmts[idx][:info] if isa(info, UnionSplitApplyCallInfo) if length(info.infos) != 1 # TODO: Handle union split applies? - new_info = info = nothing + new_info = info = false else info = info.infos[1] new_info = info.call end else @assert info === nothing || info === false - new_info = info = nothing + new_info = info = false end - arg_start = sig.f === Core._apply ? 2 : 3 - atypes = sig.atypes - if arg_start > length(atypes) + arg_start = 3 + argtypes = sig.argtypes + if arg_start > length(argtypes) return nothing end - ft = atypes[arg_start] + ft = argtypes[arg_start] if ft isa Const && ft.val === Core.tuple # if one argument is a tuple already, and the rest are empty, we can just return it # e.g. rewrite `((t::Tuple)...,)` to `t` nonempty_idx = 0 - for i = (arg_start + 1):length(atypes) - ti = atypes[i] + for i = (arg_start + 1):length(argtypes) + ti = argtypes[i] ti ⊑ Tuple{} && continue if ti ⊑ Tuple && nonempty_idx == 0 nonempty_idx = i @@ -950,25 +1057,27 @@ function inline_apply!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx::Int, sig:: end # Try to figure out the signature of the function being called # and if rewrite_apply_exprargs can deal with this form - infos = Any[] - for i = (arg_start + 1):length(atypes) + arginfos = MaybeAbstractIterationInfo[] + for i = (arg_start + 1):length(argtypes) thisarginfo = nothing - if !is_valid_type_for_apply_rewrite(atypes[i], params) + if !is_valid_type_for_apply_rewrite(argtypes[i], state.params) if isa(info, ApplyCallInfo) && info.arginfo[i-arg_start] !== nothing thisarginfo = info.arginfo[i-arg_start] else return nothing end end - push!(infos, thisarginfo) + push!(arginfos, thisarginfo) end # Independent of whether we can inline, the above analysis allows us to rewrite # this apply call to a regular call - stmt.args, atypes = rewrite_apply_exprargs!(ir, todo, idx, stmt.args, atypes, infos, arg_start, et, caches, params) + argtypes = rewrite_apply_exprargs!( + ir, idx, stmt, argtypes, + arginfos, arg_start, state, todo) ir.stmts[idx][:info] = new_info has_free_typevars(ft) && return nothing f = singleton_type(ft) - sig = Signature(f, ft, atypes) + sig = Signature(f, ft, argtypes) end sig end @@ -980,277 +1089,379 @@ is_builtin(s::Signature) = isa(s.f, Builtin) || s.ft ⊑ Builtin -function inline_invoke!(ir::IRCode, idx::Int, sig::Signature, invoke_data::InvokeData, state::InliningState, todo::Vector{Pair{Int, Any}}) - stmt = ir.stmts[idx][:inst] - calltype = ir.stmts[idx][:type] - method = invoke_data.entry - (metharg, methsp) = ccall(:jl_type_intersection_with_env, Any, (Any, Any), - sig.atype, method.sig)::SimpleVector - methsp = methsp::SimpleVector - match = MethodMatch(metharg, methsp, method, true) - result = analyze_method!(match, sig.atypes, state.et, state.caches, state.params, calltype) - handle_single_case!(ir, stmt, idx, result, true, todo) - intersect!(state.et, WorldRange(invoke_data.min_valid, invoke_data.max_valid)) +function inline_invoke!( + ir::IRCode, idx::Int, stmt::Expr, info::InvokeCallInfo, flag::UInt8, + sig::Signature, state::InliningState, todo::Vector{Pair{Int, Any}}) + match = info.match + if !match.fully_covers + # TODO: We could union split out the signature check and continue on + return nothing + end + result = info.result + if isa(result, ConcreteResult) + item = concrete_result_item(result, state) + else + argtypes = invoke_rewrite(sig.argtypes) + if isa(result, ConstPropResult) + (; mi) = item = InliningTodo(result.result, argtypes) + validate_sparams(mi.sparam_vals) || return nothing + if argtypes_to_type(argtypes) <: mi.def.sig + state.mi_cache !== nothing && (item = resolve_todo(item, state, flag)) + handle_single_case!(ir, idx, stmt, item, todo, state.params, true) + return nothing + end + end + item = analyze_method!(match, argtypes, flag, state) + end + handle_single_case!(ir, idx, stmt, item, todo, state.params, true) return nothing end +function narrow_opaque_closure!(ir::IRCode, stmt::Expr, @nospecialize(info), state::InliningState) + if isa(info, OpaqueClosureCreateInfo) + lbt = argextype(stmt.args[2], ir) + lb, exact = instanceof_tfunc(lbt) + exact || return + ubt = argextype(stmt.args[3], ir) + ub, exact = instanceof_tfunc(ubt) + exact || return + # Narrow opaque closure type + newT = widenconst(tmeet(tmerge(lb, info.unspec.rt), ub)) + if newT != ub + # N.B.: Narrowing the ub requires a backdge on the mi whose type + # information we're using, since a change in that function may + # invalidate ub result. + stmt.args[3] = newT + end + end +end + +# As a matter of convenience, this pass also computes effect-freenes. +# For primitives, we do that right here. For proper calls, we will +# discover this when we consult the caches. +function check_effect_free!(ir::IRCode, idx::Int, @nospecialize(stmt), @nospecialize(rt)) + if stmt_effect_free(stmt, rt, ir) + ir.stmts[idx][:flag] |= IR_FLAG_EFFECT_FREE + return true + end + return false +end + # Handles all analysis and inlining of intrinsics and builtins. In particular, # this method does not access the method table or otherwise process generic # functions. -function process_simple!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx::Int, state::InliningState) +function process_simple!(ir::IRCode, idx::Int, state::InliningState, todo::Vector{Pair{Int, Any}}) stmt = ir.stmts[idx][:inst] - stmt isa Expr || return nothing - if stmt.head === :splatnew - inline_splatnew!(ir, idx) + rt = ir.stmts[idx][:type] + if !(stmt isa Expr) + check_effect_free!(ir, idx, stmt, rt) + return nothing + end + head = stmt.head + if head !== :call + if head === :splatnew + inline_splatnew!(ir, idx, stmt, rt) + elseif head === :new_opaque_closure + narrow_opaque_closure!(ir, stmt, ir.stmts[idx][:info], state) + end + check_effect_free!(ir, idx, stmt, rt) return nothing end - - stmt.head === :call || return nothing sig = call_sig(ir, stmt) sig === nothing && return nothing - # Handle _apply - sig = inline_apply!(ir, todo, idx, sig, state.et, state.caches, state.params) + # Handle _apply_iterate + sig = inline_apply!(ir, idx, stmt, sig, state, todo) sig === nothing && return nothing # Check if we match any of the early inliners - calltype = ir.stmts[idx][:type] - res = early_inline_special_case(ir, sig, stmt, state.params, calltype) - if res !== nothing - ir.stmts[idx][:inst] = res + earlyres = early_inline_special_case(ir, stmt, rt, sig, state.params) + if isa(earlyres, SomeCase) + ir.stmts[idx][:inst] = earlyres.val return nothing end - - # Handle invoke - invoke_data = nothing - if sig.f === Core.invoke && length(sig.atypes) >= 3 - res = compute_invoke_data(sig.atypes, state.method_table) - res === nothing && return nothing - (sig, invoke_data) = res - elseif is_builtin(sig) - # No inlining for builtins (other than what was previously handled) + if (sig.f === modifyfield! || sig.ft ⊑ typeof(modifyfield!)) && 5 <= length(stmt.args) <= 6 + let info = ir.stmts[idx][:info] + info isa MethodResultPure && (info = info.info) + info isa ConstCallInfo && (info = info.call) + info isa MethodMatchInfo || return nothing + length(info.results) == 1 || return nothing + match = info.results[1]::MethodMatch + match.fully_covers || return nothing + case = compileable_specialization(state.et, match, Effects()) + case === nothing && return nothing + stmt.head = :invoke_modify + pushfirst!(stmt.args, case.invoke) + ir.stmts[idx][:inst] = stmt + end return nothing end - sig = with_atype(sig) + if check_effect_free!(ir, idx, stmt, rt) + if sig.f === typeassert || sig.ft ⊑ typeof(typeassert) + # typeassert is a no-op if effect free + ir.stmts[idx][:inst] = stmt.args[2] + return nothing + end + end - # In :invoke, make sure that the arguments we're passing are a subtype of the - # signature we're invoking. - (invoke_data === nothing || sig.atype <: invoke_data.types0) || return nothing + if sig.f !== Core.invoke && is_builtin(sig) + # No inlining for builtins (other invoke/apply/typeassert) + return nothing + end # Special case inliners for regular functions - if late_inline_special_case!(ir, sig, idx, stmt, state.params) || is_return_type(sig.f) + lateres = late_inline_special_case!(ir, idx, stmt, rt, sig, state.params) + if isa(lateres, SomeCase) + ir[SSAValue(idx)][:inst] = lateres.val + check_effect_free!(ir, idx, lateres.val, rt) + return nothing + elseif is_return_type(sig.f) + check_effect_free!(ir, idx, stmt, rt) return nothing end - return (sig, invoke_data) -end -# This is not currently called in the regular course, but may be needed -# if we ever want to re-run inlining again later in the pass pipeline after -# additional type information was discovered. -function recompute_method_matches(@nospecialize(atype), params::OptimizationParams, et::EdgeTracker, method_table::MethodTableView) - # Regular case: Retrieve matching methods from cache (or compute them) - # World age does not need to be taken into account in the cache - # because it is forwarded from type inference through `sv.params` - # in the case that the cache is nonempty, so it should be unchanged - # The max number of methods should be the same as in inference most - # of the time, and should not affect correctness otherwise. - results = findall(atype, method_table; limit=params.MAX_METHODS) - results !== missing && intersect!(et, results.valid_worlds) - MethodMatchInfo(results) + return stmt, sig end -function analyze_single_call!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx::Int, @nospecialize(stmt), - sig::Signature, @nospecialize(calltype), infos::Vector{MethodMatchInfo}, - et, caches, params) - cases = Pair{Any, Any}[] - signature_union = Union{} - only_method = nothing # keep track of whether there is one matching method - too_many = false - local meth - local fully_covered = true +# TODO inline non-`isdispatchtuple`, union-split callsites? +function analyze_single_call!( + ir::IRCode, idx::Int, stmt::Expr, infos::Vector{MethodMatchInfo}, flag::UInt8, + sig::Signature, state::InliningState, todo::Vector{Pair{Int, Any}}) + argtypes = sig.argtypes + cases = InliningCase[] + local any_fully_covered = false + local handled_all_cases = true for i in 1:length(infos) - info = infos[i] - meth = info.results - if meth === missing || meth.ambig + meth = infos[i].results + if meth.ambig # Too many applicable methods # Or there is a (partial?) ambiguity - too_many = true - break + return nothing elseif length(meth) == 0 # No applicable methods; try next union split + handled_all_cases = false continue - elseif length(meth) == 1 && only_method !== false - if only_method === nothing - only_method = meth[1].method - elseif only_method !== meth[1].method - only_method = false - end - else - only_method = false end for match in meth - signature_union = Union{signature_union, match.spec_types} - if !isdispatchtuple(match.spec_types) - fully_covered = false - continue - end - case = analyze_method!(match, sig.atypes, et, caches, params, calltype) - if case === nothing - fully_covered = false - continue - elseif _any(p->p[1] === match.spec_types, cases) - continue - end - push!(cases, Pair{Any,Any}(match.spec_types, case)) + handled_all_cases &= handle_match!(match, argtypes, flag, state, cases, true) + any_fully_covered |= match.fully_covers end end - too_many && return + if !handled_all_cases + # if we've not seen all candidates, union split is valid only for dispatch tuples + filter!(case::InliningCase->isdispatchtuple(case.sig), cases) + end - signature_fully_covered = sig.atype <: signature_union - # If we're fully covered and there's only one applicable method, - # we inline, even if the signature is not a dispatch tuple - if signature_fully_covered && length(cases) == 0 && only_method isa Method - if length(infos) > 1 - (metharg, methsp) = ccall(:jl_type_intersection_with_env, Any, (Any, Any), - sig.atype, only_method.sig)::SimpleVector - match = MethodMatch(metharg, methsp, only_method, true) - else - @assert length(meth) == 1 - match = meth[1] + handle_cases!(ir, idx, stmt, argtypes_to_type(argtypes), cases, + handled_all_cases & any_fully_covered, todo, state.params) +end + +# similar to `analyze_single_call!`, but with constant results +function handle_const_call!( + ir::IRCode, idx::Int, stmt::Expr, cinfo::ConstCallInfo, flag::UInt8, + sig::Signature, state::InliningState, todo::Vector{Pair{Int, Any}}) + argtypes = sig.argtypes + (; call, results) = cinfo + infos = isa(call, MethodMatchInfo) ? MethodMatchInfo[call] : call.matches + cases = InliningCase[] + local any_fully_covered = false + local handled_all_cases = true + local j = 0 + for i in 1:length(infos) + meth = infos[i].results + if meth.ambig + # Too many applicable methods + # Or there is a (partial?) ambiguity + return nothing + elseif length(meth) == 0 + # No applicable methods; try next union split + handled_all_cases = false + continue + end + for match in meth + j += 1 + result = results[j] + any_fully_covered |= match.fully_covers + if isa(result, ConcreteResult) + case = concrete_result_item(result, state) + push!(cases, InliningCase(result.mi.specTypes, case)) + elseif isa(result, ConstPropResult) + handled_all_cases &= handle_const_prop_result!(result, argtypes, flag, state, cases, true) + else + @assert result === nothing + handled_all_cases &= handle_match!(match, argtypes, flag, state, cases, true) + end end - fully_covered = true - case = analyze_method!(match, sig.atypes, et, caches, params, calltype) - case === nothing && return - push!(cases, Pair{Any,Any}(match.spec_types, case)) end - if !signature_fully_covered - fully_covered = false + + if !handled_all_cases + # if we've not seen all candidates, union split is valid only for dispatch tuples + filter!(case::InliningCase->isdispatchtuple(case.sig), cases) end + handle_cases!(ir, idx, stmt, argtypes_to_type(argtypes), cases, + handled_all_cases & any_fully_covered, todo, state.params) +end + +function handle_match!( + match::MethodMatch, argtypes::Vector{Any}, flag::UInt8, state::InliningState, + cases::Vector{InliningCase}, allow_abstract::Bool = false) + spec_types = match.spec_types + allow_abstract || isdispatchtuple(spec_types) || return false + # we may see duplicated dispatch signatures here when a signature gets widened + # during abstract interpretation: for the purpose of inlining, we can just skip + # processing this dispatch candidate + _any(case->case.sig === spec_types, cases) && return true + item = analyze_method!(match, argtypes, flag, state) + item === nothing && return false + push!(cases, InliningCase(spec_types, item)) + return true +end + +function handle_const_prop_result!( + result::ConstPropResult, argtypes::Vector{Any}, flag::UInt8, state::InliningState, + cases::Vector{InliningCase}, allow_abstract::Bool = false) + (; mi) = item = InliningTodo(result.result, argtypes) + spec_types = mi.specTypes + allow_abstract || isdispatchtuple(spec_types) || return false + validate_sparams(mi.sparam_vals) || return false + state.mi_cache !== nothing && (item = resolve_todo(item, state, flag)) + item === nothing && return false + push!(cases, InliningCase(spec_types, item)) + return true +end + +function concrete_result_item(result::ConcreteResult, state::InliningState) + if !isdefined(result, :result) || !is_inlineable_constant(result.result) + return compileable_specialization(state.et, result.mi, result.effects) + end + @assert result.effects === EFFECTS_TOTAL + return ConstantCase(quoted(result.result)) +end + +function handle_cases!(ir::IRCode, idx::Int, stmt::Expr, @nospecialize(atype), + cases::Vector{InliningCase}, fully_covered::Bool, todo::Vector{Pair{Int, Any}}, + params::OptimizationParams) # If we only have one case and that case is fully covered, we may either # be able to do the inlining now (for constant cases), or push it directly # onto the todo list if fully_covered && length(cases) == 1 - handle_single_case!(ir, stmt, idx, cases[1][2], false, todo) - return + handle_single_case!(ir, idx, stmt, cases[1].item, todo, params) + elseif length(cases) > 0 + isa(atype, DataType) || return nothing + for case in cases + isa(case.sig, DataType) || return nothing + end + push!(todo, idx=>UnionSplit(fully_covered, atype, cases)) end - length(cases) == 0 && return - push!(todo, idx=>UnionSplit(fully_covered, sig.atype, cases)) return nothing end +function handle_const_opaque_closure_call!( + ir::IRCode, idx::Int, stmt::Expr, result::ConstPropResult, flag::UInt8, + sig::Signature, state::InliningState, todo::Vector{Pair{Int, Any}}) + item = InliningTodo(result.result, sig.argtypes) + isdispatchtuple(item.mi.specTypes) || return + validate_sparams(item.mi.sparam_vals) || return + state.mi_cache !== nothing && (item = resolve_todo(item, state, flag)) + handle_single_case!(ir, idx, stmt, item, todo, state.params) + return nothing +end + +function inline_const_if_inlineable!(inst::Instruction) + rt = inst[:type] + if rt isa Const && is_inlineable_constant(rt.val) + inst[:inst] = quoted(rt.val) + return true + end + inst[:flag] |= IR_FLAG_EFFECT_FREE + return false +end + function assemble_inline_todo!(ir::IRCode, state::InliningState) # todo = (inline_idx, (isva, isinvoke, na), method, spvals, inline_linetable, inline_ir, lie) todo = Pair{Int, Any}[] - if state.params.unoptimize_throw_blocks - skip = find_throw_blocks(ir.stmts.inst, RefValue(ir)) - end + et = state.et + for idx in 1:length(ir.stmts) - state.params.unoptimize_throw_blocks && idx in skip && continue - r = process_simple!(ir, todo, idx, state) - r === nothing && continue + simpleres = process_simple!(ir, idx, state, todo) + simpleres === nothing && continue + stmt, sig = simpleres - stmt = ir.stmts[idx][:inst] - calltype = ir.stmts[idx][:type] info = ir.stmts[idx][:info] - # Inference determined this couldn't be analyzed. Don't question it. + + # Check whether this call was @pure and evaluates to a constant + if info isa MethodResultPure + inline_const_if_inlineable!(ir[SSAValue(idx)]) && continue + info = info.info + end if info === false + # Inference determined this couldn't be analyzed. Don't question it. continue end - (sig, invoke_data) = r + flag = ir.stmts[idx][:flag] - # Check whether this call was @pure and evaluates to a constant - if calltype isa Const && info isa MethodResultPure - if is_inlineable_constant(calltype.val) - ir.stmts[idx][:inst] = quoted(calltype.val) - continue + if isa(info, OpaqueClosureCallInfo) + result = info.result + if isa(result, ConstPropResult) + handle_const_opaque_closure_call!( + ir, idx, stmt, result, flag, + sig, state, todo) + else + if isa(result, ConcreteResult) + item = concrete_result_item(result, state) + else + item = analyze_method!(info.match, sig.argtypes, flag, state) + end + handle_single_case!(ir, idx, stmt, item, todo, state.params) + end + continue + end + + # Handle invoke + if sig.f === Core.invoke + if isa(info, InvokeCallInfo) + inline_invoke!(ir, idx, stmt, info, flag, sig, state, todo) end + continue end - # Ok, now figure out what method to call - if invoke_data !== nothing - inline_invoke!(ir, idx, sig, invoke_data, state, todo) + # if inference arrived here with constant-prop'ed result(s), + # we can perform a specialized analysis for just this case + if isa(info, ConstCallInfo) + handle_const_call!( + ir, idx, stmt, info, flag, + sig, state, todo) continue end - nu = countunionsplit(sig.atypes) - if nu == 1 || nu > state.params.MAX_UNION_SPLITTING - if !isa(info, MethodMatchInfo) - if state.method_table === nothing - continue - end - info = recompute_method_matches(sig.atype, state.params, state.et, state.method_table) - end + # Ok, now figure out what method to call + if isa(info, MethodMatchInfo) infos = MethodMatchInfo[info] + elseif isa(info, UnionSplitInfo) + infos = info.matches else - if !isa(info, UnionSplitInfo) - if state.method_table === nothing - continue - end - infos = MethodMatchInfo[] - for union_sig in UnionSplitSignature(sig.atypes) - push!(infos, recompute_method_matches(argtypes_to_type(union_sig), state.params, state.et, state.method_table)) - end - else - infos = info.matches - end + continue # isa(info, ReturnTypeCallInfo), etc. end - analyze_single_call!(ir, todo, idx, stmt, sig, calltype, infos, state.et, state.caches, state.params) + analyze_single_call!(ir, idx, stmt, infos, flag, sig, state, todo) end - todo -end -function mk_tuplecall!(compact::IncrementalCompact, args::Vector{Any}, line_idx::Int32) - e = Expr(:call, TOP_TUPLE, args...) - etyp = tuple_tfunc(Any[compact_exprtype(compact, args[i]) for i in 1:length(args)]) - return insert_node_here!(compact, e, etyp, line_idx) + return todo end function linear_inline_eligible(ir::IRCode) length(ir.cfg.blocks) == 1 || return false - terminator = ir[SSAValue(last(ir.cfg.blocks[1].stmts))] + terminator = ir[SSAValue(last(ir.cfg.blocks[1].stmts))][:inst] isa(terminator, ReturnNode) || return false isdefined(terminator, :val) || return false return true end -function compute_invoke_data(@nospecialize(atypes), method_table) - ft = widenconst(atypes[2]) - if !isdispatchelem(ft) || has_free_typevars(ft) || (ft <: Builtin) - # TODO: this can be rather aggressive at preventing inlining of closures - # but we need to check that `ft` can't have a subtype at runtime before using the supertype lookup below - return nothing - end - invoke_tt = widenconst(atypes[3]) - if !isType(invoke_tt) || has_free_typevars(invoke_tt) - return nothing - end - invoke_tt = invoke_tt.parameters[1] - if !(isa(unwrap_unionall(invoke_tt), DataType) && invoke_tt <: Tuple) - return nothing - end - if method_table === nothing - # TODO: These should be forwarded in stmt_info, just like regular - # method lookup results - return nothing - end - invoke_types = rewrap_unionall(Tuple{ft, unwrap_unionall(invoke_tt).parameters...}, invoke_tt) - invoke_entry = findsup(invoke_types, method_table) - invoke_entry === nothing && return nothing - method, valid_worlds = invoke_entry - invoke_data = InvokeData(method, invoke_types, first(valid_worlds), last(valid_worlds)) - atype0 = atypes[2] - atypes = atypes[4:end] - pushfirst!(atypes, atype0) - f = singleton_type(ft) - return (Signature(f, ft, atypes), invoke_data) -end - # Check for a number of functions known to be pure function ispuretopfunction(@nospecialize(f)) return istopfunction(f, :typejoin) || @@ -1259,76 +1470,78 @@ function ispuretopfunction(@nospecialize(f)) istopfunction(f, :promote_type) end -function early_inline_special_case(ir::IRCode, s::Signature, e::Expr, params::OptimizationParams, - @nospecialize(etype)) - f, ft, atypes = s.f, s.ft, s.atypes - if (f === typeassert || ft ⊑ typeof(typeassert)) && length(atypes) == 3 - # typeassert(x::S, T) => x, when S<:T - a3 = atypes[3] - if (isType(a3) && !has_free_typevars(a3) && atypes[2] ⊑ a3.parameters[1]) || - (isa(a3, Const) && isa(a3.val, Type) && atypes[2] ⊑ a3.val) - val = e.args[2] - val === nothing && return QuoteNode(val) - return val - end - end - - if params.inlining - if isa(etype, Const) # || isconstType(etype) - val = etype.val - is_inlineable_constant(val) || return nothing - if isa(f, IntrinsicFunction) - if is_pure_intrinsic_infer(f) && intrinsic_nothrow(f, atypes[2:end]) - return quoted(val) - end - elseif ispuretopfunction(f) || contains_is(_PURE_BUILTINS, f) - return quoted(val) - elseif contains_is(_PURE_OR_ERROR_BUILTINS, f) - if _builtin_nothrow(f, atypes[2:end], etype) - return quoted(val) - end +function early_inline_special_case( + ir::IRCode, stmt::Expr, @nospecialize(type), sig::Signature, + params::OptimizationParams) + params.inlining || return nothing + (; f, ft, argtypes) = sig + + if isa(type, Const) # || isconstType(type) + val = type.val + is_inlineable_constant(val) || return nothing + if isa(f, IntrinsicFunction) + if is_pure_intrinsic_infer(f) && intrinsic_nothrow(f, argtypes[2:end]) + return SomeCase(quoted(val)) + end + elseif ispuretopfunction(f) || contains_is(_PURE_BUILTINS, f) + return SomeCase(quoted(val)) + elseif contains_is(_EFFECT_FREE_BUILTINS, f) + if _builtin_nothrow(f, argtypes[2:end], type) + return SomeCase(quoted(val)) + end + elseif f === Core.get_binding_type + length(argtypes) == 3 || return nothing + if get_binding_type_effect_free(argtypes[2], argtypes[3]) + return SomeCase(quoted(val)) end end end - return nothing end -function late_inline_special_case!(ir::IRCode, sig::Signature, idx::Int, stmt::Expr, params::OptimizationParams) - f, ft, atypes = sig.f, sig.ft, sig.atypes - typ = ir.stmts[idx][:type] - if params.inlining && length(atypes) == 3 && istopfunction(f, :!==) +# special-case some regular method calls whose results are not folded within `abstract_call_known` +# (and thus `early_inline_special_case` doesn't handle them yet) +# NOTE we manually inline the method bodies, and so the logic here needs to precisely sync with their definitions +function late_inline_special_case!( + ir::IRCode, idx::Int, stmt::Expr, @nospecialize(type), sig::Signature, + params::OptimizationParams) + params.inlining || return nothing + (; f, ft, argtypes) = sig + if length(argtypes) == 3 && istopfunction(f, :!==) # special-case inliner for !== that precedes _methods_by_ftype union splitting # and that works, even though inference generally avoids inferring the `!==` Method - if isa(typ, Const) - ir[SSAValue(idx)] = quoted(typ.val) - return true + if isa(type, Const) + return SomeCase(quoted(type.val)) end cmp_call = Expr(:call, GlobalRef(Core, :(===)), stmt.args[2], stmt.args[3]) - cmp_call_ssa = insert_node!(ir, idx, Bool, cmp_call) + cmp_call_ssa = insert_node!(ir, idx, effect_free(NewInstruction(cmp_call, Bool))) not_call = Expr(:call, GlobalRef(Core.Intrinsics, :not_int), cmp_call_ssa) - ir[SSAValue(idx)] = not_call - return true - elseif params.inlining && length(atypes) == 3 && istopfunction(f, :(>:)) + return SomeCase(not_call) + elseif length(argtypes) == 3 && istopfunction(f, :(>:)) # special-case inliner for issupertype # that works, even though inference generally avoids inferring the `>:` Method - if isa(typ, Const) - ir[SSAValue(idx)] = quoted(typ.val) - return true + if isa(type, Const) && _builtin_nothrow(<:, Any[argtypes[3], argtypes[2]], type) + return SomeCase(quoted(type.val)) end subtype_call = Expr(:call, GlobalRef(Core, :(<:)), stmt.args[3], stmt.args[2]) - ir[SSAValue(idx)] = subtype_call - return true + return SomeCase(subtype_call) + elseif f === TypeVar && 2 <= length(argtypes) <= 4 && (argtypes[2] ⊑ Symbol) + typevar_call = Expr(:call, GlobalRef(Core, :_typevar), stmt.args[2], + length(stmt.args) < 4 ? Bottom : stmt.args[3], + length(stmt.args) == 2 ? Any : stmt.args[end]) + return SomeCase(typevar_call) + elseif f === UnionAll && length(argtypes) == 3 && (argtypes[2] ⊑ TypeVar) + unionall_call = Expr(:foreigncall, QuoteNode(:jl_type_unionall), Any, svec(Any, Any), + 0, QuoteNode(:ccall), stmt.args[2], stmt.args[3]) + return SomeCase(unionall_call) elseif is_return_type(f) - if isconstType(typ) - ir[SSAValue(idx)] = quoted(typ.parameters[1]) - return true - elseif isa(typ, Const) - ir[SSAValue(idx)] = quoted(typ.val) - return true + if isconstType(type) + return SomeCase(quoted(type.parameters[1])) + elseif isa(type, Const) + return SomeCase(quoted(type.val)) end end - return false + return nothing end function ssa_substitute!(idx::Int, @nospecialize(val), arg_replacements::Vector{Any}, @@ -1348,25 +1561,22 @@ function ssa_substitute_op!(@nospecialize(val), arg_replacements::Vector{Any}, e = val::Expr head = e.head if head === :static_parameter - return quoted(spvals[e.args[1]]) + return quoted(spvals[e.args[1]::Int]) elseif head === :cfunction @assert !isa(spsig, UnionAll) || !isempty(spvals) e.args[3] = ccall(:jl_instantiate_type_in_env, Any, (Any, Any, Ptr{Any}), e.args[3], spsig, spvals) e.args[4] = svec(Any[ ccall(:jl_instantiate_type_in_env, Any, (Any, Any, Ptr{Any}), argt, spsig, spvals) - for argt - in e.args[4] ]...) + for argt in e.args[4]::SimpleVector ]...) elseif head === :foreigncall @assert !isa(spsig, UnionAll) || !isempty(spvals) for i = 1:length(e.args) if i == 2 e.args[2] = ccall(:jl_instantiate_type_in_env, Any, (Any, Any, Ptr{Any}), e.args[2], spsig, spvals) elseif i == 3 - argtuple = Any[ + e.args[3] = svec(Any[ ccall(:jl_instantiate_type_in_env, Any, (Any, Any, Ptr{Any}), argt, spsig, spvals) - for argt - in e.args[3] ] - e.args[3] = svec(argtuple...) + for argt in e.args[3]::SimpleVector ]...) end end elseif head === :boundscheck @@ -1385,46 +1595,3 @@ function ssa_substitute_op!(@nospecialize(val), arg_replacements::Vector{Any}, end return urs[] end - -function find_inferred(mi::MethodInstance, atypes::Vector{Any}, caches::InferenceCaches, @nospecialize(rettype)) - if caches.inf_cache !== nothing - # see if the method has a InferenceResult in the current cache - # or an existing inferred code info store in `.inferred` - haveconst = false - for i in 1:length(atypes) - if has_nontrivial_const_info(atypes[i]) - # have new information from argtypes that wasn't available from the signature - haveconst = true - break - end - end - if haveconst || improvable_via_constant_propagation(rettype) - inf_result = cache_lookup(mi, atypes, caches.inf_cache) # Union{Nothing, InferenceResult} - else - inf_result = nothing - end - #XXX: update_valid_age!(min_valid[1], max_valid[1], sv) - if isa(inf_result, InferenceResult) - let inferred_src = inf_result.src - if isa(inferred_src, CodeInfo) - return svec(false, inferred_src) - end - if isa(inferred_src, Const) && is_inlineable_constant(inferred_src.val) - return svec(true, quoted(inferred_src.val),) - end - end - end - end - - linfo = get(caches.mi_cache, mi, nothing) - if linfo isa CodeInstance - if invoke_api(linfo) == 2 - # in this case function can be inlined to a constant - return svec(true, quoted(linfo.rettype_const)) - end - return svec(false, linfo.inferred) - else - # `linfo` may be `nothing` or an IRCode here - return svec(false, linfo) - end -end diff --git a/base/compiler/ssair/ir.jl b/base/compiler/ssair/ir.jl index e7003473e1cbd3..2f1359e4002aea 100644 --- a/base/compiler/ssair/ir.jl +++ b/base/compiler/ssair/ir.jl @@ -1,45 +1,41 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -@inline isexpr(@nospecialize(stmt), head::Symbol) = isa(stmt, Expr) && stmt.head === head Core.PhiNode() = Core.PhiNode(Int32[], Any[]) -""" -Like UnitRange{Int}, but can handle the `last` field, being temporarily -< first (this can happen during compacting) -""" -struct StmtRange <: AbstractUnitRange{Int} - start::Int - stop::Int -end -first(r::StmtRange) = r.start -last(r::StmtRange) = r.stop -iterate(r::StmtRange, state=0) = (last(r) - first(r) < state) ? nothing : (first(r) + state, state + 1) - -StmtRange(range::UnitRange{Int}) = StmtRange(first(range), last(range)) - -struct BasicBlock - stmts::StmtRange - preds::Vector{Int} - succs::Vector{Int} -end -function BasicBlock(stmts::StmtRange) - return BasicBlock(stmts, Int[], Int[]) -end -function BasicBlock(old_bb, stmts) - return BasicBlock(stmts, old_bb.preds, old_bb.succs) -end -copy(bb::BasicBlock) = BasicBlock(bb.stmts, copy(bb.preds), copy(bb.succs)) +isterminator(@nospecialize(stmt)) = isa(stmt, GotoNode) || isa(stmt, GotoIfNot) || isa(stmt, ReturnNode) struct CFG blocks::Vector{BasicBlock} index::Vector{Int} # map from instruction => basic-block number # TODO: make this O(1) instead of O(log(n_blocks))? end + copy(c::CFG) = CFG(BasicBlock[copy(b) for b in c.blocks], copy(c.index)) +function cfg_insert_edge!(cfg::CFG, from::Int, to::Int) + # Assumes that this edge does not already exist + push!(cfg.blocks[to].preds, from) + push!(cfg.blocks[from].succs, to) + nothing +end + +function cfg_delete_edge!(cfg::CFG, from::Int, to::Int) + preds = cfg.blocks[to].preds + succs = cfg.blocks[from].succs + # Assumes that blocks appear at most once in preds and succs + deleteat!(preds, findfirst(x->x === from, preds)::Int) + deleteat!(succs, findfirst(x->x === to, succs)::Int) + nothing +end + function block_for_inst(index::Vector{Int}, inst::Int) return searchsortedfirst(index, inst, lt=(<=)) end + +function block_for_inst(index::Vector{BasicBlock}, inst::Int) + return searchsortedfirst(index, BasicBlock(StmtRange(inst, inst)), by=x->first(x.stmts), lt=(<=))-1 +end + block_for_inst(cfg::CFG, inst::Int) = block_for_inst(cfg.index, inst) function basic_blocks_starts(stmts::Vector{Any}) @@ -92,7 +88,7 @@ function compute_basic_blocks(stmts::Vector{Any}) bb_starts = basic_blocks_starts(stmts) # Compute ranges pop!(bb_starts, 1) - basic_block_index = collect(bb_starts) + basic_block_index = sort!(collect(bb_starts); alg=QuickSort) blocks = BasicBlock[] sizehint!(blocks, length(basic_block_index)) let first = 1 @@ -129,9 +125,6 @@ function compute_basic_blocks(stmts::Vector{Any}) # :enter gets a virtual edge to the exception handler and # the exception handler gets a virtual edge from outside # the function. - # See the devdocs on exception handling in SSA form (or - # bug Keno to write them, if you're reading this and they - # don't exist) block′ = block_for_inst(basic_block_index, terminator.args[1]::Int) push!(blocks[block′].preds, num) push!(blocks[block′].preds, 0) @@ -147,6 +140,7 @@ function compute_basic_blocks(stmts::Vector{Any}) return CFG(blocks, basic_block_index) end +# this function assumes insert position exists function first_insert_for_bb(code, cfg::CFG, block::Int) for idx in cfg.blocks[block].stmts stmt = code[idx] @@ -154,9 +148,54 @@ function first_insert_for_bb(code, cfg::CFG, block::Int) return idx end end + error("any insert position isn't found") +end + +# SSA values that need renaming +struct OldSSAValue + id::Int +end + +# SSA values that are in `new_new_nodes` of an `IncrementalCompact` and are to +# be actually inserted next time (they become `new_nodes` next time) +struct NewSSAValue + id::Int end +const AnySSAValue = Union{SSAValue, OldSSAValue, NewSSAValue} + + # SSA-indexed nodes + +struct NewInstruction + stmt::Any + type::Any + info::Any + # If nothing, copy the line from previous statement + # in the insertion location + line::Union{Int32, Nothing} + flag::UInt8 + + ## Insertion options + + # The IR_FLAG_EFFECT_FREE flag has already been computed (or forced). + # Don't bother redoing so on insertion. + effect_free_computed::Bool + NewInstruction(@nospecialize(stmt), @nospecialize(type), @nospecialize(info), + line::Union{Int32, Nothing}, flag::UInt8, effect_free_computed::Bool) = + new(stmt, type, info, line, flag, effect_free_computed) +end +NewInstruction(@nospecialize(stmt), @nospecialize(type)) = + NewInstruction(stmt, type, nothing) +NewInstruction(@nospecialize(stmt), @nospecialize(type), line::Union{Nothing, Int32}) = + NewInstruction(stmt, type, nothing, line, IR_FLAG_NULL, false) + +effect_free(inst::NewInstruction) = + NewInstruction(inst.stmt, inst.type, inst.info, inst.line, inst.flag | IR_FLAG_EFFECT_FREE, true) +non_effect_free(inst::NewInstruction) = + NewInstruction(inst.stmt, inst.type, inst.info, inst.line, inst.flag & ~IR_FLAG_EFFECT_FREE, true) + + struct InstructionStream inst::Vector{Any} type::Vector{Any} @@ -170,7 +209,7 @@ function InstructionStream(len::Int) info = Array{Any}(undef, len) fill!(info, nothing) lines = fill(Int32(0), len) - flags = fill(0x00, len) + flags = fill(IR_FLAG_NULL, len) return InstructionStream(insts, types, info, lines, flags) end InstructionStream() = InstructionStream(0) @@ -198,7 +237,7 @@ function resize!(stmts::InstructionStream, len) resize!(stmts.flag, len) for i in (old_length + 1):len stmts.line[i] = 0 - stmts.flag[i] = 0x00 + stmts.flag[i] = IR_FLAG_NULL stmts.info[i] = nothing end return stmts @@ -228,6 +267,10 @@ function setindex!(is::InstructionStream, newval::Instruction, idx::Int) is.flag[idx] = newval[:flag] return is end +function setindex!(is::InstructionStream, newval::AnySSAValue, idx::Int) + is.inst[idx] = newval + return is +end function setindex!(node::Instruction, newval::Instruction) node.data[node.idx] = newval return node @@ -259,9 +302,9 @@ struct IRCode linetable::Vector{LineInfoNode} cfg::CFG new_nodes::NewNodeStream - meta::Vector{Any} + meta::Vector{Expr} - function IRCode(stmts::InstructionStream, cfg::CFG, linetable::Vector{LineInfoNode}, argtypes::Vector{Any}, meta::Vector{Any}, sptypes::Vector{Any}) + function IRCode(stmts::InstructionStream, cfg::CFG, linetable::Vector{LineInfoNode}, argtypes::Vector{Any}, meta::Vector{Expr}, sptypes::Vector{Any}) return new(stmts, argtypes, sptypes, linetable, cfg, NewNodeStream(), meta) end function IRCode(ir::IRCode, stmts::InstructionStream, cfg::CFG, new_nodes::NewNodeStream) @@ -272,152 +315,143 @@ struct IRCode copy(ir.linetable), copy(ir.cfg), copy(ir.new_nodes), copy(ir.meta)) end +function block_for_inst(ir::IRCode, inst::Int) + if inst > length(ir.stmts) + inst = ir.new_nodes.info[inst - length(ir.stmts)].pos + end + block_for_inst(ir.cfg, inst) +end + function getindex(x::IRCode, s::SSAValue) if s.id <= length(x.stmts) - return x.stmts[s.id][:inst] + return x.stmts[s.id] else - return x.new_nodes.stmts[s.id - length(x.stmts)][:inst] + return x.new_nodes.stmts[s.id - length(x.stmts)] end end -function setindex!(x::IRCode, @nospecialize(repl), s::SSAValue) +function setindex!(x::IRCode, repl::Union{Instruction, AnySSAValue}, s::SSAValue) if s.id <= length(x.stmts) - x.stmts[s.id][:inst] = repl + x.stmts[s.id] = repl else - x.new_nodes.stmts[s.id - length(x.stmts)][:inst] = repl + x.new_nodes.stmts[s.id - length(x.stmts)] = repl end return x end -# SSA values that need renaming -struct OldSSAValue - id::Int -end - -# SSA values that are in `new_new_nodes` of an `IncrementalCompact` and are to -# be actually inserted next time (they become `new_nodes` next time) -struct NewSSAValue - id::Int -end - -const AnySSAValue = Union{SSAValue, OldSSAValue, NewSSAValue} - -mutable struct UseRef +mutable struct UseRefIterator stmt::Any - op::Int - UseRef(@nospecialize(a)) = new(a, 0) -end -struct UseRefIterator - use::Tuple{UseRef, Nothing} relevant::Bool - UseRefIterator(@nospecialize(a), relevant::Bool) = new((UseRef(a), nothing), relevant) + UseRefIterator(@nospecialize(a), relevant::Bool) = new(a, relevant) end -getindex(it::UseRefIterator) = it.use[1].stmt +getindex(it::UseRefIterator) = it.stmt -# TODO: stack-allocation -#struct UseRef -# urs::UseRefIterator -# use::Int -#end - -struct OOBToken -end - -struct UndefToken +struct UseRef + urs::UseRefIterator + op::Int + UseRef(urs::UseRefIterator) = new(urs, 0) + UseRef(urs::UseRefIterator, op::Int) = new(urs, op) end -const undef_token = UndefToken() +struct OOBToken end; const OOB_TOKEN = OOBToken() +struct UndefToken end; const UNDEF_TOKEN = UndefToken() -function getindex(x::UseRef) - stmt = x.stmt +@noinline function _useref_getindex(@nospecialize(stmt), op::Int) if isa(stmt, Expr) && stmt.head === :(=) rhs = stmt.args[2] if isa(rhs, Expr) if is_relevant_expr(rhs) - x.op > length(rhs.args) && return OOBToken() - return rhs.args[x.op] + op > length(rhs.args) && return OOB_TOKEN + return rhs.args[op] end end - x.op == 1 || return OOBToken() + op == 1 || return OOB_TOKEN return rhs elseif isa(stmt, Expr) # @assert is_relevant_expr(stmt) - x.op > length(stmt.args) && return OOBToken() - return stmt.args[x.op] + op > length(stmt.args) && return OOB_TOKEN + return stmt.args[op] elseif isa(stmt, GotoIfNot) - x.op == 1 || return OOBToken() + op == 1 || return OOB_TOKEN return stmt.cond elseif isa(stmt, ReturnNode) - isdefined(stmt, :val) || return OOBToken() - x.op == 1 || return OOBToken() + isdefined(stmt, :val) || return OOB_TOKEN + op == 1 || return OOB_TOKEN return stmt.val elseif isa(stmt, PiNode) - isdefined(stmt, :val) || return OOBToken() - x.op == 1 || return OOBToken() + isdefined(stmt, :val) || return OOB_TOKEN + op == 1 || return OOB_TOKEN return stmt.val elseif isa(stmt, UpsilonNode) - isdefined(stmt, :val) || return OOBToken() - x.op == 1 || return OOBToken() + isdefined(stmt, :val) || return OOB_TOKEN + op == 1 || return OOB_TOKEN return stmt.val elseif isa(stmt, PhiNode) - x.op > length(stmt.values) && return OOBToken() - isassigned(stmt.values, x.op) || return UndefToken() - return stmt.values[x.op] + op > length(stmt.values) && return OOB_TOKEN + isassigned(stmt.values, op) || return UNDEF_TOKEN + return stmt.values[op] elseif isa(stmt, PhiCNode) - x.op > length(stmt.values) && return OOBToken() - isassigned(stmt.values, x.op) || return UndefToken() - return stmt.values[x.op] + op > length(stmt.values) && return OOB_TOKEN + isassigned(stmt.values, op) || return UNDEF_TOKEN + return stmt.values[op] else - return OOBToken() + return OOB_TOKEN end end +@inline getindex(x::UseRef) = _useref_getindex(x.urs.stmt, x.op) function is_relevant_expr(e::Expr) - return e.head in (:call, :invoke, :new, :splatnew, :(=), :(&), + return e.head in (:call, :invoke, :invoke_modify, + :new, :splatnew, :(=), :(&), :gc_preserve_begin, :gc_preserve_end, :foreigncall, :isdefined, :copyast, :undefcheck, :throw_undef_if_not, - :cfunction, :method, :pop_exception) + :cfunction, :method, :pop_exception, + :new_opaque_closure) end -function setindex!(x::UseRef, @nospecialize(v)) - stmt = x.stmt +@noinline function _useref_setindex!(@nospecialize(stmt), op::Int, @nospecialize(v)) if isa(stmt, Expr) && stmt.head === :(=) rhs = stmt.args[2] if isa(rhs, Expr) if is_relevant_expr(rhs) - x.op > length(rhs.args) && throw(BoundsError()) - rhs.args[x.op] = v - return v + op > length(rhs.args) && throw(BoundsError()) + rhs.args[op] = v + return stmt end end - x.op == 1 || throw(BoundsError()) + op == 1 || throw(BoundsError()) stmt.args[2] = v elseif isa(stmt, Expr) # @assert is_relevant_expr(stmt) - x.op > length(stmt.args) && throw(BoundsError()) - stmt.args[x.op] = v + op > length(stmt.args) && throw(BoundsError()) + stmt.args[op] = v elseif isa(stmt, GotoIfNot) - x.op == 1 || throw(BoundsError()) - x.stmt = GotoIfNot(v, stmt.dest) + op == 1 || throw(BoundsError()) + stmt = GotoIfNot(v, stmt.dest) elseif isa(stmt, ReturnNode) - x.op == 1 || throw(BoundsError()) - x.stmt = typeof(stmt)(v) + op == 1 || throw(BoundsError()) + stmt = typeof(stmt)(v) elseif isa(stmt, UpsilonNode) - x.op == 1 || throw(BoundsError()) - x.stmt = typeof(stmt)(v) + op == 1 || throw(BoundsError()) + stmt = typeof(stmt)(v) elseif isa(stmt, PiNode) - x.op == 1 || throw(BoundsError()) - x.stmt = typeof(stmt)(v, stmt.typ) + op == 1 || throw(BoundsError()) + stmt = typeof(stmt)(v, stmt.typ) elseif isa(stmt, PhiNode) - x.op > length(stmt.values) && throw(BoundsError()) - isassigned(stmt.values, x.op) || throw(BoundsError()) - stmt.values[x.op] = v + op > length(stmt.values) && throw(BoundsError()) + isassigned(stmt.values, op) || throw(BoundsError()) + stmt.values[op] = v elseif isa(stmt, PhiCNode) - x.op > length(stmt.values) && throw(BoundsError()) - isassigned(stmt.values, x.op) || throw(BoundsError()) - stmt.values[x.op] = v + op > length(stmt.values) && throw(BoundsError()) + isassigned(stmt.values, op) || throw(BoundsError()) + stmt.values[op] = v else throw(BoundsError()) end + return stmt +end + +@inline function setindex!(x::UseRef, @nospecialize(v)) + x.urs.stmt = _useref_setindex!(x.urs.stmt, x.op, v) return x end @@ -428,18 +462,22 @@ function userefs(@nospecialize(x)) return UseRefIterator(x, relevant) end -iterate(it::UseRefIterator) = (it.use[1].op = 0; iterate(it, nothing)) -@noinline function iterate(it::UseRefIterator, ::Nothing) - it.relevant || return nothing - use = it.use[1] +@noinline function _advance(@nospecialize(stmt), op) while true - use.op += 1 - y = use[] - y === OOBToken() && return nothing - y === UndefToken() || return it.use + op += 1 + y = _useref_getindex(stmt, op) + y === OOB_TOKEN && return nothing + y === UNDEF_TOKEN || return op end end +@inline function iterate(it::UseRefIterator, op::Int=0) + it.relevant || return nothing + op = _advance(it.stmt, op) + op === nothing && return nothing + return (UseRef(it, op), op) +end + # This function is used from the show code, which may have a different # `push!`/`used` type since it's in Base. function scan_ssa_use!(push!, used, @nospecialize(stmt)) @@ -487,10 +525,16 @@ function foreachssa(f, @nospecialize(stmt)) end end -function insert_node!(ir::IRCode, pos::Int, @nospecialize(typ), @nospecialize(val), attach_after::Bool=false) - line = ir.stmts[pos][:line] +function insert_node!(ir::IRCode, pos::Int, inst::NewInstruction, attach_after::Bool=false) node = add!(ir.new_nodes, pos, attach_after) - node[:inst], node[:type], node[:line], node[:flag] = val, typ, line, 0x00 + node[:line] = something(inst.line, ir.stmts[pos][:line]) + flag = inst.flag + if !inst.effect_free_computed + if stmt_effect_free(inst.stmt, inst.type, ir) + flag |= IR_FLAG_EFFECT_FREE + end + end + node[:inst], node[:type], node[:flag] = inst.stmt, inst.type, flag return SSAValue(length(ir.stmts) + node.idx) end @@ -519,6 +563,7 @@ mutable struct IncrementalCompact new_nodes_idx::Int # This supports insertion while compacting new_new_nodes::NewNodeStream # New nodes that were before the compaction point at insertion time + new_new_used_ssas::Vector{Int} # TODO: Switch these two to a min-heap of some sort pending_nodes::NewNodeStream # New nodes that were after the compaction point at insertion time pending_perm::Vector{Int} @@ -539,12 +584,14 @@ mutable struct IncrementalCompact new_len = length(code.stmts) + length(code.new_nodes) result = InstructionStream(new_len) used_ssas = fill(0, new_len) + new_new_used_ssas = Vector{Int}() blocks = code.cfg.blocks if allow_cfg_transforms bb_rename = Vector{Int}(undef, length(blocks)) cur_bb = 1 + domtree = construct_domtree(blocks) for i = 1:length(bb_rename) - if i != 1 && length(blocks[i].preds) == 0 + if bb_unreachable(domtree, i) bb_rename[i] = -1 else bb_rename[i] = cur_bb @@ -580,7 +627,7 @@ mutable struct IncrementalCompact pending_nodes = NewNodeStream() pending_perm = Int[] return new(code, result, result_bbs, ssa_rename, bb_rename, bb_rename, used_ssas, late_fixup, perm, 1, - new_new_nodes, pending_nodes, pending_perm, + new_new_nodes, new_new_used_ssas, pending_nodes, pending_perm, 1, 1, 1, false, allow_cfg_transforms, allow_cfg_transforms) end @@ -589,7 +636,7 @@ mutable struct IncrementalCompact perm = my_sortperm(Int[code.new_nodes.info[i].pos for i in 1:length(code.new_nodes)]) new_len = length(code.stmts) + length(code.new_nodes) ssa_rename = Any[SSAValue(i) for i = 1:new_len] - used_ssas = fill(0, new_len) + new_new_used_ssas = Vector{Int}() late_fixup = Vector{Int}() bb_rename = Vector{Int}() new_new_nodes = NewNodeStream() @@ -598,7 +645,7 @@ mutable struct IncrementalCompact return new(code, parent.result, parent.result_bbs, ssa_rename, bb_rename, bb_rename, parent.used_ssas, late_fixup, perm, 1, - new_new_nodes, pending_nodes, pending_perm, + new_new_nodes, new_new_used_ssas, pending_nodes, pending_perm, 1, result_offset, parent.active_result_bb, false, false, false) end end @@ -608,6 +655,7 @@ struct TypesView{T} end types(ir::Union{IRCode, IncrementalCompact}) = TypesView(ir) +# TODO We can be a bit better about access here by using a pattern similar to InstructionStream function getindex(compact::IncrementalCompact, idx::Int) if idx < compact.result_idx return compact.result[idx][:inst] @@ -623,7 +671,10 @@ end function getindex(compact::IncrementalCompact, ssa::OldSSAValue) id = ssa.id - if id <= length(compact.ir.stmts) + if id < compact.idx + new_idx = compact.ssa_rename[id] + return compact.result[new_idx][:inst] + elseif id <= length(compact.ir.stmts) return compact.ir.stmts[id][:inst] end id -= length(compact.ir.stmts) @@ -638,21 +689,85 @@ function getindex(compact::IncrementalCompact, ssa::NewSSAValue) return compact.new_new_nodes.stmts[ssa.id][:inst] end +function block_for_inst(compact::IncrementalCompact, idx::SSAValue) + id = idx.id + if id < compact.result_idx # if ssa within result + return block_for_inst(compact.result_bbs, id) + else + return block_for_inst(compact.ir.cfg, id) + end +end + +function block_for_inst(compact::IncrementalCompact, idx::OldSSAValue) + id = idx.id + if id < compact.idx # if ssa within result + return block_for_inst(compact.result_bbs, compact.ssa_rename[id]) + else + return block_for_inst(compact.ir.cfg, id) + end +end + +function block_for_inst(compact::IncrementalCompact, idx::NewSSAValue) + block_for_inst(compact, SSAValue(compact.new_new_nodes.info[idx.id].pos)) +end + +function dominates_ssa(compact::IncrementalCompact, domtree::DomTree, x::AnySSAValue, y::AnySSAValue) + xb = block_for_inst(compact, x) + yb = block_for_inst(compact, y) + if xb == yb + xinfo = yinfo = nothing + if isa(x, OldSSAValue) + x′ = compact.ssa_rename[x.id]::SSAValue + elseif isa(x, NewSSAValue) + xinfo = compact.new_new_nodes.info[x.id] + x′ = SSAValue(xinfo.pos) + else + x′ = x + end + if isa(y, OldSSAValue) + y′ = compact.ssa_rename[y.id]::SSAValue + elseif isa(y, NewSSAValue) + yinfo = compact.new_new_nodes.info[y.id] + y′ = SSAValue(yinfo.pos) + else + y′ = y + end + if x′.id == y′.id && (xinfo !== nothing || yinfo !== nothing) + if xinfo !== nothing && yinfo !== nothing + if xinfo.attach_after == yinfo.attach_after + return x.id < y.id + end + return yinfo.attach_after + elseif xinfo !== nothing + return !xinfo.attach_after + else + return yinfo.attach_after + end + end + return x′.id < y′.id + end + return dominates(domtree, xb, yb) +end + function count_added_node!(compact::IncrementalCompact, @nospecialize(v)) - needs_late_fixup = isa(v, NewSSAValue) + needs_late_fixup = false if isa(v, SSAValue) compact.used_ssas[v.id] += 1 + elseif isa(v, NewSSAValue) + compact.new_new_used_ssas[v.id] += 1 + needs_late_fixup = true else for ops in userefs(v) val = ops[] if isa(val, SSAValue) compact.used_ssas[val.id] += 1 elseif isa(val, NewSSAValue) + compact.new_new_used_ssas[val.id] += 1 needs_late_fixup = true end end end - needs_late_fixup + return needs_late_fixup end function add_pending!(compact::IncrementalCompact, pos::Int, attach_after::Bool) @@ -663,18 +778,20 @@ function add_pending!(compact::IncrementalCompact, pos::Int, attach_after::Bool) return node end -function insert_node!(compact::IncrementalCompact, before, @nospecialize(typ), @nospecialize(val), attach_after::Bool=false) +function insert_node!(compact::IncrementalCompact, before, inst::NewInstruction, attach_after::Bool=false) + @assert inst.effect_free_computed if isa(before, SSAValue) if before.id < compact.result_idx - count_added_node!(compact, val) - line = compact.result[before.id][:line] + count_added_node!(compact, inst.stmt) + line = something(inst.line, compact.result[before.id][:line]) node = add!(compact.new_new_nodes, before.id, attach_after) - node[:inst], node[:type], node[:line] = val, typ, line + push!(compact.new_new_used_ssas, 0) + node[:inst], node[:type], node[:line], node[:flag] = inst.stmt, inst.type, line, inst.flag return NewSSAValue(node.idx) else - line = compact.ir.stmts[before.id][:line] + line = something(inst.line, compact.ir.stmts[before.id][:line]) node = add_pending!(compact, before.id, attach_after) - node[:inst], node[:type], node[:line] = val, typ, line + node[:inst], node[:type], node[:line], node[:flag] = inst.stmt, inst.type, line, inst.flag os = OldSSAValue(length(compact.ir.stmts) + length(compact.ir.new_nodes) + length(compact.pending_nodes)) push!(compact.ssa_rename, os) push!(compact.used_ssas, 0) @@ -682,33 +799,47 @@ function insert_node!(compact::IncrementalCompact, before, @nospecialize(typ), @ end elseif isa(before, OldSSAValue) pos = before.id - if pos > length(compact.ir.stmts) - #@assert attach_after - info = compact.pending_nodes.info[pos - length(compact.ir.stmts) - length(compact.ir.new_nodes)] - pos, attach_after = info.pos, info.attach_after + if pos < compact.idx + renamed = compact.ssa_rename[pos]::AnySSAValue + count_added_node!(compact, inst.stmt) + line = something(inst.line, compact.result[renamed.id][:line]) + node = add!(compact.new_new_nodes, renamed.id, attach_after) + push!(compact.new_new_used_ssas, 0) + node[:inst], node[:type], node[:line], node[:flag] = inst.stmt, inst.type, line, inst.flag + return NewSSAValue(node.idx) + else + if pos > length(compact.ir.stmts) + #@assert attach_after + info = compact.pending_nodes.info[pos - length(compact.ir.stmts) - length(compact.ir.new_nodes)] + pos, attach_after = info.pos, info.attach_after + end + line = something(inst.line, compact.ir.stmts[pos][:line]) + node = add_pending!(compact, pos, attach_after) + node[:inst], node[:type], node[:line], node[:flag] = inst.stmt, inst.type, line, inst.flag + os = OldSSAValue(length(compact.ir.stmts) + length(compact.ir.new_nodes) + length(compact.pending_nodes)) + push!(compact.ssa_rename, os) + push!(compact.used_ssas, 0) + return os end - line = compact.ir.stmts[pos][:line] - node = add_pending!(compact, pos, attach_after) - node[:inst], node[:type], node[:line] = val, typ, line - os = OldSSAValue(length(compact.ir.stmts) + length(compact.ir.new_nodes) + length(compact.pending_nodes)) - push!(compact.ssa_rename, os) - push!(compact.used_ssas, 0) - return os elseif isa(before, NewSSAValue) before_entry = compact.new_new_nodes.info[before.id] - line = compact.new_new_nodes.stmts[before.id][:line] + line = something(inst.line, compact.new_new_nodes.stmts[before.id][:line]) new_entry = add!(compact.new_new_nodes, before_entry.pos, attach_after) - new_entry[:inst], new_entry[:type], new_entry[:line] = val, typ, line + new_entry[:inst], new_entry[:type], new_entry[:line], new_entry[:flag] = inst.stmt, inst.type, line, inst.flag + push!(compact.new_new_used_ssas, 0) return NewSSAValue(new_entry.idx) else error("Unsupported") end end -function insert_node_here!(compact::IncrementalCompact, @nospecialize(val), @nospecialize(typ), ltable_idx::Int32, reverse_affinity::Bool=false) +function insert_node_here!(compact::IncrementalCompact, inst::NewInstruction, reverse_affinity::Bool=false) + @assert inst.line !== nothing refinish = false result_idx = compact.result_idx - if result_idx == first(compact.result_bbs[compact.active_result_bb].stmts) && reverse_affinity + if reverse_affinity && + ((compact.active_result_bb == length(compact.result_bbs) + 1) || + result_idx == first(compact.result_bbs[compact.active_result_bb].stmts)) compact.active_result_bb -= 1 refinish = true end @@ -716,11 +847,13 @@ function insert_node_here!(compact::IncrementalCompact, @nospecialize(val), @nos @assert result_idx == length(compact.result) + 1 resize!(compact, result_idx) end - node = compact.result[result_idx] - node[:inst], node[:type], node[:line], node[:flag] = val, typ, ltable_idx, 0x00 - if count_added_node!(compact, val) - push!(compact.late_fixup, result_idx) + flag = inst.flag + if !inst.effect_free_computed && stmt_effect_free(inst.stmt, inst.type, compact) + flag |= IR_FLAG_EFFECT_FREE end + node = compact.result[result_idx] + node[:inst], node[:type], node[:line], node[:flag] = inst.stmt, inst.type, inst.line, flag + count_added_node!(compact, inst.stmt) && push!(compact.late_fixup, result_idx) compact.result_idx = result_idx + 1 inst = SSAValue(result_idx) refinish && finish_current_bb!(compact, 0) @@ -742,22 +875,50 @@ function getindex(view::TypesView, v::OldSSAValue) return view.ir.pending_nodes.stmts[id][:type] end -function setindex!(compact::IncrementalCompact, @nospecialize(v), idx::SSAValue) - @assert idx.id < compact.result_idx - (compact.result[idx.id][:inst] === v) && return - # Kill count for current uses - for ops in userefs(compact.result[idx.id][:inst]) +function kill_current_uses(compact::IncrementalCompact, @nospecialize(stmt)) + for ops in userefs(stmt) val = ops[] if isa(val, SSAValue) @assert compact.used_ssas[val.id] >= 1 compact.used_ssas[val.id] -= 1 + elseif isa(val, NewSSAValue) + @assert compact.new_new_used_ssas[val.id] >= 1 + compact.new_new_used_ssas[val.id] -= 1 end end +end + +function setindex!(compact::IncrementalCompact, @nospecialize(v), idx::SSAValue) + @assert idx.id < compact.result_idx + (compact.result[idx.id][:inst] === v) && return + # Kill count for current uses + kill_current_uses(compact, compact.result[idx.id][:inst]) compact.result[idx.id][:inst] = v # Add count for new use - if count_added_node!(compact, v) - push!(compact.late_fixup, idx.id) + count_added_node!(compact, v) && push!(compact.late_fixup, idx.id) + return compact +end + +function setindex!(compact::IncrementalCompact, @nospecialize(v), idx::OldSSAValue) + id = idx.id + if id < compact.idx + new_idx = compact.ssa_rename[id] + (compact.result[new_idx][:inst] === v) && return + kill_current_uses(compact, compact.result[new_idx][:inst]) + compact.result[new_idx][:inst] = v + count_added_node!(compact, v) && push!(compact.late_fixup, new_idx) + return compact + elseif id <= length(compact.ir.stmts) # ir.stmts, new_nodes, and pending_nodes uses aren't counted yet, so no need to adjust + compact.ir.stmts[id][:inst] = v + return compact + end + id -= length(compact.ir.stmts) + if id <= length(compact.ir.new_nodes) + compact.ir.new_nodes.stmts[id][:inst] = v + return compact end + id -= length(compact.ir.new_nodes) + compact.pending_nodes.stmts[id][:inst] = v return compact end @@ -801,6 +962,7 @@ end function process_phinode_values(old_values::Vector{Any}, late_fixup::Vector{Int}, processed_idx::Int, result_idx::Int, ssa_rename::Vector{Any}, used_ssas::Vector{Int}, + new_new_used_ssas::Vector{Int}, do_rename_ssa::Bool) values = Vector{Any}(undef, length(old_values)) for i = 1:length(old_values) @@ -812,7 +974,7 @@ function process_phinode_values(old_values::Vector{Any}, late_fixup::Vector{Int} push!(late_fixup, result_idx) val = OldSSAValue(val.id) else - val = renumber_ssa2(val, ssa_rename, used_ssas, do_rename_ssa) + val = renumber_ssa2(val, ssa_rename, used_ssas, new_new_used_ssas, do_rename_ssa) end else used_ssas[val.id] += 1 @@ -822,17 +984,19 @@ function process_phinode_values(old_values::Vector{Any}, late_fixup::Vector{Int} push!(late_fixup, result_idx) else # Always renumber these. do_rename_ssa applies only to actual SSAValues - val = renumber_ssa2(SSAValue(val.id), ssa_rename, used_ssas, true) + val = renumber_ssa2(SSAValue(val.id), ssa_rename, used_ssas, new_new_used_ssas, true) end elseif isa(val, NewSSAValue) push!(late_fixup, result_idx) + new_new_used_ssas[val.id] += 1 end values[i] = val end return values end -function renumber_ssa2(val::SSAValue, ssanums::Vector{Any}, used_ssas::Vector{Int}, do_rename_ssa::Bool) +function renumber_ssa2(val::SSAValue, ssanums::Vector{Any}, used_ssas::Vector{Int}, + new_new_used_ssas::Vector{Int}, do_rename_ssa::Bool) id = val.id if id > length(ssanums) return val @@ -841,22 +1005,26 @@ function renumber_ssa2(val::SSAValue, ssanums::Vector{Any}, used_ssas::Vector{In val = ssanums[id] end if isa(val, SSAValue) - if used_ssas !== nothing - used_ssas[val.id] += 1 - end + used_ssas[val.id] += 1 end return val end -function renumber_ssa2!(@nospecialize(stmt), ssanums::Vector{Any}, used_ssas::Vector{Int}, late_fixup::Vector{Int}, result_idx::Int, do_rename_ssa::Bool) +function renumber_ssa2(val::NewSSAValue, ssanums::Vector{Any}, used_ssas::Vector{Int}, + new_new_used_ssas::Vector{Int}, do_rename_ssa::Bool) + new_new_used_ssas[val.id] += 1 + return val +end + +function renumber_ssa2!(@nospecialize(stmt), ssanums::Vector{Any}, used_ssas::Vector{Int}, new_new_used_ssas::Vector{Int}, late_fixup::Vector{Int}, result_idx::Int, do_rename_ssa::Bool) urs = userefs(stmt) for op in urs val = op[] if isa(val, OldSSAValue) || isa(val, NewSSAValue) push!(late_fixup, result_idx) end - if isa(val, SSAValue) - val = renumber_ssa2(val, ssanums, used_ssas, do_rename_ssa) + if isa(val, Union{SSAValue, NewSSAValue}) + val = renumber_ssa2(val, ssanums, used_ssas, new_new_used_ssas, do_rename_ssa) end if isa(val, OldSSAValue) || isa(val, NewSSAValue) push!(late_fixup, result_idx) @@ -890,7 +1058,7 @@ function kill_edge!(compact::IncrementalCompact, active_bb::Int, from::Int, to:: # Check if the block is now dead if length(preds) == 0 for succ in copy(compact.result_bbs[compact.bb_rename_succ[to]].succs) - kill_edge!(compact, active_bb, to, findfirst(x->x === succ, compact.bb_rename_pred)) + kill_edge!(compact, active_bb, to, findfirst(x->x === succ, compact.bb_rename_pred)::Int) end if to < active_bb # Kill all statements in the block @@ -936,26 +1104,33 @@ end function process_node!(compact::IncrementalCompact, result_idx::Int, inst::Instruction, idx::Int, processed_idx::Int, active_bb::Int, do_rename_ssa::Bool) stmt = inst[:inst] - result = compact.result - ssa_rename = compact.ssa_rename - late_fixup = compact.late_fixup - used_ssas = compact.used_ssas + (; result, ssa_rename, late_fixup, used_ssas, new_new_used_ssas, cfg_transforms_enabled, fold_constant_branches) = compact ssa_rename[idx] = SSAValue(result_idx) if stmt === nothing ssa_rename[idx] = stmt elseif isa(stmt, OldSSAValue) ssa_rename[idx] = ssa_rename[stmt.id] - elseif isa(stmt, GotoNode) && compact.cfg_transforms_enabled + elseif isa(stmt, GotoNode) && cfg_transforms_enabled result[result_idx][:inst] = GotoNode(compact.bb_rename_succ[stmt.label]) result_idx += 1 - elseif isa(stmt, GlobalRef) || isa(stmt, GotoNode) + elseif isa(stmt, GlobalRef) + result[result_idx][:inst] = stmt + result[result_idx][:type] = argextype(stmt, compact) + result_idx += 1 + elseif isa(stmt, GotoNode) result[result_idx][:inst] = stmt result_idx += 1 - elseif isa(stmt, GotoIfNot) && compact.cfg_transforms_enabled - stmt = renumber_ssa2!(stmt, ssa_rename, used_ssas, late_fixup, result_idx, do_rename_ssa)::GotoIfNot + elseif isa(stmt, GotoIfNot) && cfg_transforms_enabled + stmt = renumber_ssa2!(stmt, ssa_rename, used_ssas, new_new_used_ssas, late_fixup, result_idx, do_rename_ssa)::GotoIfNot result[result_idx][:inst] = stmt cond = stmt.cond - if isa(cond, Bool) && compact.fold_constant_branches + if fold_constant_branches + if !isa(cond, Bool) + condT = widenconditional(argextype(cond, compact)) + isa(condT, Const) || @goto bail + cond = condT.val + isa(cond, Bool) || @goto bail + end if cond result[result_idx][:inst] = nothing kill_edge!(compact, active_bb, active_bb, stmt.dest) @@ -966,12 +1141,13 @@ function process_node!(compact::IncrementalCompact, result_idx::Int, inst::Instr result_idx += 1 end else + @label bail result[result_idx][:inst] = GotoIfNot(cond, compact.bb_rename_succ[stmt.dest]) result_idx += 1 end elseif isa(stmt, Expr) - stmt = renumber_ssa2!(stmt, ssa_rename, used_ssas, late_fixup, result_idx, do_rename_ssa)::Expr - if compact.cfg_transforms_enabled && isexpr(stmt, :enter) + stmt = renumber_ssa2!(stmt, ssa_rename, used_ssas, new_new_used_ssas, late_fixup, result_idx, do_rename_ssa)::Expr + if cfg_transforms_enabled && isexpr(stmt, :enter) stmt.args[1] = compact.bb_rename_succ[stmt.args[1]::Int] end result[result_idx][:inst] = stmt @@ -980,10 +1156,11 @@ function process_node!(compact::IncrementalCompact, result_idx::Int, inst::Instr # As an optimization, we eliminate any trivial pinodes. For performance, we use === # type equality. We may want to consider using == in either a separate pass or if # performance turns out ok - stmt = renumber_ssa2!(stmt, ssa_rename, used_ssas, late_fixup, result_idx, do_rename_ssa)::PiNode + stmt = renumber_ssa2!(stmt, ssa_rename, used_ssas, new_new_used_ssas, late_fixup, result_idx, do_rename_ssa)::PiNode pi_val = stmt.val if isa(pi_val, SSAValue) - if stmt.typ === compact.result[pi_val.id][:type] + if stmt.typ === result[pi_val.id][:type] + used_ssas[pi_val.id] -= 1 ssa_rename[idx] = pi_val return result_idx end @@ -1002,10 +1179,10 @@ function process_node!(compact::IncrementalCompact, result_idx::Int, inst::Instr result[result_idx][:inst] = stmt result_idx += 1 elseif isa(stmt, ReturnNode) || isa(stmt, UpsilonNode) || isa(stmt, GotoIfNot) - result[result_idx][:inst] = renumber_ssa2!(stmt, ssa_rename, used_ssas, late_fixup, result_idx, do_rename_ssa) + result[result_idx][:inst] = renumber_ssa2!(stmt, ssa_rename, used_ssas, new_new_used_ssas, late_fixup, result_idx, do_rename_ssa) result_idx += 1 elseif isa(stmt, PhiNode) - if compact.cfg_transforms_enabled + if cfg_transforms_enabled # Rename phi node edges map!(i -> compact.bb_rename_pred[i], stmt.edges, stmt.edges) @@ -1039,27 +1216,31 @@ function process_node!(compact::IncrementalCompact, result_idx::Int, inst::Instr values = stmt.values end - values = process_phinode_values(values, late_fixup, processed_idx, result_idx, ssa_rename, used_ssas, do_rename_ssa) + values = process_phinode_values(values, late_fixup, processed_idx, result_idx, ssa_rename, used_ssas, new_new_used_ssas, do_rename_ssa) # Don't remove the phi node if it is before the definition of its value # because doing so can create forward references. This should only # happen with dead loops, but can cause problems when optimization # passes look at all code, dead or not. This check should be # unnecessary when DCE can remove those dead loops entirely, so this is # just to be safe. - before_def = isassigned(values, 1) && isa(values[1], OldSSAValue) && - idx < values[1].id + before_def = isassigned(values, 1) && (v = values[1]; isa(v, OldSSAValue)) && idx < v.id if length(edges) == 1 && isassigned(values, 1) && !before_def && - length(compact.cfg_transforms_enabled ? + length(cfg_transforms_enabled ? compact.result_bbs[compact.bb_rename_succ[active_bb]].preds : compact.ir.cfg.blocks[active_bb].preds) == 1 # There's only one predecessor left - just replace it - ssa_rename[idx] = values[1] + v = values[1] + @assert !isa(v, NewSSAValue) + if isa(v, SSAValue) + used_ssas[v.id] -= 1 + end + ssa_rename[idx] = v else result[result_idx][:inst] = PhiNode(edges, values) result_idx += 1 end elseif isa(stmt, PhiCNode) - result[result_idx][:inst] = PhiCNode(process_phinode_values(stmt.values, late_fixup, processed_idx, result_idx, ssa_rename, used_ssas, do_rename_ssa)) + result[result_idx][:inst] = PhiCNode(process_phinode_values(stmt.values, late_fixup, processed_idx, result_idx, ssa_rename, used_ssas, new_new_used_ssas, do_rename_ssa)) result_idx += 1 elseif isa(stmt, SSAValue) # identity assign, replace uses of this ssa value with its result @@ -1196,8 +1377,8 @@ function iterate(compact::IncrementalCompact, (idx, active_bb)::Tuple{Int, Int}= resize!(compact, old_result_idx) end bb = compact.ir.cfg.blocks[active_bb] - if compact.cfg_transforms_enabled && active_bb > 1 && active_bb <= length(compact.bb_rename_succ) && length(bb.preds) == 0 - # No predecessors, kill the entire block. + if compact.cfg_transforms_enabled && active_bb > 1 && active_bb <= length(compact.bb_rename_succ) && compact.bb_rename_succ[active_bb] == -1 + # Dead block, so kill the entire block. compact.idx = last(bb.stmts) # Pop any remaining insertion nodes while compact.new_nodes_idx <= length(compact.perm) @@ -1260,30 +1441,35 @@ function iterate(compact::IncrementalCompact, (idx, active_bb)::Tuple{Int, Int}= compact.result[old_result_idx][:inst]), (compact.idx, active_bb) end -function maybe_erase_unused!(extra_worklist, compact, idx, callback = x->nothing) - stmt = compact.result[idx][:inst] +function maybe_erase_unused!( + extra_worklist::Vector{Int}, compact::IncrementalCompact, idx::Int, in_worklist::Bool, + callback = null_dce_callback) + + inst = idx <= length(compact.result) ? compact.result[idx] : + compact.new_new_nodes.stmts[idx - length(compact.result)] + stmt = inst[:inst] stmt === nothing && return false - if compact_exprtype(compact, SSAValue(idx)) === Bottom + if inst[:type] === Bottom effect_free = false else - effect_free = stmt_effect_free(stmt, compact.result[idx][:type], compact, compact.ir.sptypes) + effect_free = inst[:flag] & IR_FLAG_EFFECT_FREE != 0 end - if effect_free - for ops in userefs(stmt) - val = ops[] - # If the pass we ran inserted new nodes, it's possible for those - # to be outside our used_ssas count. - if isa(val, SSAValue) && val.id <= length(compact.used_ssas) - if compact.used_ssas[val.id] == 1 - if val.id < idx - push!(extra_worklist, val.id) - end - end - compact.used_ssas[val.id] -= 1 - callback(val) + function kill_ssa_value(val::SSAValue) + if compact.used_ssas[val.id] == 1 + if val.id < idx || in_worklist + push!(extra_worklist, val.id) end end - compact.result[idx][:inst] = nothing + compact.used_ssas[val.id] -= 1 + callback(val) + end + if effect_free + if isa(stmt, SSAValue) + kill_ssa_value(stmt) + else + foreachssa(kill_ssa_value, stmt) + end + inst[:inst] = nothing return true end return false @@ -1294,13 +1480,8 @@ function fixup_phinode_values!(compact::IncrementalCompact, old_values::Vector{A for i = 1:length(old_values) isassigned(old_values, i) || continue val = old_values[i] - if isa(val, OldSSAValue) - val = compact.ssa_rename[val.id] - if isa(val, SSAValue) - compact.used_ssas[val.id] += 1 - end - elseif isa(val, NewSSAValue) - val = SSAValue(length(compact.result) + val.id) + if isa(val, Union{OldSSAValue, NewSSAValue}) + val = fixup_node(compact, val) end values[i] = val end @@ -1315,29 +1496,30 @@ function fixup_node(compact::IncrementalCompact, @nospecialize(stmt)) elseif isa(stmt, NewSSAValue) return SSAValue(length(compact.result) + stmt.id) elseif isa(stmt, OldSSAValue) - return compact.ssa_rename[stmt.id] + val = compact.ssa_rename[stmt.id] + if isa(val, SSAValue) + # If `val.id` is greater than the length of `compact.result` or + # `compact.used_ssas`, this SSA value is in `new_new_nodes`, so + # don't count the use + compact.used_ssas[val.id] += 1 + end + return val else urs = userefs(stmt) for ur in urs val = ur[] - if isa(val, NewSSAValue) - val = SSAValue(length(compact.result) + val.id) - elseif isa(val, OldSSAValue) - val = compact.ssa_rename[val.id] - end - if isa(val, SSAValue) && val.id <= length(compact.used_ssas) - # If `val.id` is greater than the length of `compact.result` or - # `compact.used_ssas`, this SSA value is in `new_new_nodes`, so - # don't count the use - compact.used_ssas[val.id] += 1 + if isa(val, Union{NewSSAValue, OldSSAValue}) + ur[] = fixup_node(compact, val) end - ur[] = val end return urs[] end end function just_fixup!(compact::IncrementalCompact) + resize!(compact.used_ssas, length(compact.result)) + append!(compact.used_ssas, compact.new_new_used_ssas) + empty!(compact.new_new_used_ssas) for idx in compact.late_fixup stmt = compact.result[idx][:inst] new_stmt = fixup_node(compact, stmt) @@ -1353,19 +1535,21 @@ function just_fixup!(compact::IncrementalCompact) end end -function simple_dce!(compact::IncrementalCompact) +function simple_dce!(compact::IncrementalCompact, callback = null_dce_callback) # Perform simple DCE for unused values + @assert isempty(compact.new_new_used_ssas) # just_fixup! wasn't run? extra_worklist = Int[] for (idx, nused) in Iterators.enumerate(compact.used_ssas) - idx >= compact.result_idx && break nused == 0 || continue - maybe_erase_unused!(extra_worklist, compact, idx) + maybe_erase_unused!(extra_worklist, compact, idx, false, callback) end while !isempty(extra_worklist) - maybe_erase_unused!(extra_worklist, compact, pop!(extra_worklist)) + maybe_erase_unused!(extra_worklist, compact, pop!(extra_worklist), true, callback) end end +null_dce_callback(x::SSAValue) = return + function non_dce_finish!(compact::IncrementalCompact) result_idx = compact.result_idx resize!(compact.result, result_idx - 1) diff --git a/base/compiler/ssair/legacy.jl b/base/compiler/ssair/legacy.jl index 1fa847734359b2..ffafa77d8fc58e 100644 --- a/base/compiler/ssair/legacy.jl +++ b/base/compiler/ssair/legacy.jl @@ -23,32 +23,31 @@ function inflate_ir(ci::CodeInfo, sptypes::Vector{Any}, argtypes::Vector{Any}) elseif isa(stmt, PhiNode) code[i] = PhiNode(Int32[block_for_inst(cfg, Int(edge)) for edge in stmt.edges], stmt.values) elseif isa(stmt, Expr) && stmt.head === :enter - stmt.args[1] = block_for_inst(cfg, stmt.args[1]) - code[i] = stmt - else + stmt.args[1] = block_for_inst(cfg, stmt.args[1]::Int) code[i] = stmt end end - ssavaluetypes = ci.ssavaluetypes nstmts = length(code) - ssavaluetypes = ci.ssavaluetypes isa Vector{Any} ? copy(ci.ssavaluetypes) : Any[ Any for i = 1:(ci.ssavaluetypes::Int) ] + ssavaluetypes = let ssavaluetypes = ci.ssavaluetypes + ssavaluetypes isa Vector{Any} ? copy(ssavaluetypes) : Any[ Any for i = 1:(ssavaluetypes::Int) ] + end stmts = InstructionStream(code, ssavaluetypes, Any[nothing for i = 1:nstmts], copy(ci.codelocs), copy(ci.ssaflags)) - ir = IRCode(stmts, cfg, collect(LineInfoNode, ci.linetable), argtypes, Any[], sptypes) + ir = IRCode(stmts, cfg, collect(LineInfoNode, ci.linetable), argtypes, Expr[], sptypes) return ir end function replace_code_newstyle!(ci::CodeInfo, ir::IRCode, nargs::Int) @assert isempty(ir.new_nodes) # All but the first `nargs` slots will now be unused - resize!(ci.slotflags, nargs + 1) + resize!(ci.slotflags, nargs) stmts = ir.stmts ci.code, ci.ssavaluetypes, ci.codelocs, ci.ssaflags, ci.linetable = stmts.inst, stmts.type, stmts.line, stmts.flag, ir.linetable for metanode in ir.meta push!(ci.code, metanode) push!(ci.codelocs, 1) - push!(ci.ssavaluetypes, Any) - push!(ci.ssaflags, 0x00) + push!(ci.ssavaluetypes::Vector{Any}, Any) + push!(ci.ssaflags, IR_FLAG_NULL) end # Translate BB Edges to statement edges # (and undo normalization for now) diff --git a/base/compiler/ssair/passes.jl b/base/compiler/ssair/passes.jl index 4af13d81b76d01..c2597363df2824 100644 --- a/base/compiler/ssair/passes.jl +++ b/base/compiler/ssair/passes.jl @@ -1,71 +1,93 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license +function is_known_call(@nospecialize(x), @nospecialize(func), ir::Union{IRCode,IncrementalCompact}) + isexpr(x, :call) || return false + ft = argextype(x.args[1], ir) + return singleton_type(ft) === func +end + +struct SSAUse + kind::Symbol + idx::Int +end +GetfieldUse(idx::Int) = SSAUse(:getfield, idx) +PreserveUse(idx::Int) = SSAUse(:preserve, idx) +NoPreserve() = SSAUse(:nopreserve, 0) +IsdefinedUse(idx::Int) = SSAUse(:isdefined, idx) + """ - This struct keeps track of all uses of some mutable struct allocated - in the current function. `uses` are all instances of `getfield` on the - struct. `defs` are all instances of `setfield!` on the struct. The terminology - refers to the uses/defs of the ``slot bundle'' that the mutable struct represents. - - In addition we keep track of all instances of a foreigncall preserve of this mutable - struct. Somewhat counterintuitively, we don't actually need to make sure that the - struct itself is live (or even allocated) at a ccall site. If there are no other places - where the struct escapes (and thus e.g. where its address is taken), it need not be - allocated. We do however, need to make sure to preserve any elements of this struct. + du::SSADefUse + +This struct keeps track of all uses of some mutable struct allocated in the current function: +- `du.uses::Vector{SSAUse}` are some "usages" (like `getfield`) of the struct +- `du.defs::Vector{Int}` are all instances of `setfield!` on the struct +The terminology refers to the uses/defs of the "slot bundle" that the mutable struct represents. + +`du.uses` tracks all instances of `getfield` and `isdefined` calls on the struct. +Additionally it also tracks all instances of a `:foreigncall` that preserves of this mutable +struct. Somewhat counterintuitively, we don't actually need to make sure that the struct +itself is live (or even allocated) at a `ccall` site. If there are no other places where +the struct escapes (and thus e.g. where its address is taken), it need not be allocated. +We do however, need to make sure to preserve any elements of this struct. """ struct SSADefUse - uses::Vector{Int} + uses::Vector{SSAUse} defs::Vector{Int} - ccall_preserve_uses::Vector{Int} end -SSADefUse() = SSADefUse(Int[], Int[], Int[]) +SSADefUse() = SSADefUse(SSAUse[], Int[]) -function try_compute_fieldidx_expr(@nospecialize(typ), @nospecialize(use_expr)) - field = use_expr.args[3] - isa(field, QuoteNode) && (field = field.value) - isa(field, Union{Int, Symbol}) || return nothing - return try_compute_fieldidx(typ, field) +function compute_live_ins(cfg::CFG, du::SSADefUse) + uses = Int[] + for use in du.uses + use.kind === :isdefined && continue # filter out `isdefined` usages + push!(uses, use.idx) + end + compute_live_ins(cfg, du.defs, uses) end -function lift_defuse(cfg::CFG, ssa::SSADefUse) - # We remove from `uses` any block where all uses are dominated - # by a def. This prevents insertion of dead phi nodes at the top - # of such a block if that block happens to be in a loop - ordered = Tuple{Int, Int, Bool}[(x, block_for_inst(cfg, x), true) for x in ssa.uses] - for x in ssa.defs - push!(ordered, (x, block_for_inst(cfg, x), false)) - end - ordered = sort(ordered, by=x->x[1]) - bb_defs = Int[] - bb_uses = Int[] - last_bb = last_def_bb = 0 - for (_, bb, is_use) in ordered - if bb != last_bb && is_use - push!(bb_uses, bb) - end - last_bb = bb - if last_def_bb != bb && !is_use - push!(bb_defs, bb) - last_def_bb = bb +# assume `stmt == getfield(obj, field, ...)` or `stmt == setfield!(obj, field, val, ...)` +try_compute_field_stmt(ir::Union{IncrementalCompact,IRCode}, stmt::Expr) = + try_compute_field(ir, stmt.args[3]) + +function try_compute_field(ir::Union{IncrementalCompact,IRCode}, @nospecialize(field)) + # fields are usually literals, handle them manually + if isa(field, QuoteNode) + field = field.value + elseif isa(field, Int) || isa(field, Symbol) + else + # try to resolve other constants, e.g. global reference + field = argextype(field, ir) + if isa(field, Const) + field = field.val + else + return nothing end end - SSADefUse(bb_uses, bb_defs, Int[]) + return isa(field, Union{Int, Symbol}) ? field : nothing +end + +function try_compute_fieldidx_stmt(ir::Union{IncrementalCompact,IRCode}, stmt::Expr, typ::DataType) + field = try_compute_field_stmt(ir, stmt) + return try_compute_fieldidx(typ, field) end function find_curblock(domtree::DomTree, allblocks::Vector{Int}, curblock::Int) # TODO: This can be much faster by looking at current level and only # searching for those blocks in a sorted order while !(curblock in allblocks) - curblock = domtree.idoms[curblock] + curblock = domtree.idoms_bb[curblock] end return curblock end function val_for_def_expr(ir::IRCode, def::Int, fidx::Int) - if isexpr(ir[SSAValue(def)], :new) - return ir[SSAValue(def)].args[1+fidx] + ex = ir[SSAValue(def)][:inst] + if isexpr(ex, :new) + return ex.args[1+fidx] else + @assert isa(ex, Expr) # The use is whatever the setfield was - return ir[SSAValue(def)].args[4] + return ex.args[4] end end @@ -80,34 +102,14 @@ function compute_value_for_block(ir::IRCode, domtree::DomTree, allblocks::Vector def == 0 ? phinodes[curblock] : val_for_def_expr(ir, def, fidx) end -function compute_value_for_use(ir::IRCode, domtree::DomTree, allblocks::Vector{Int}, du::SSADefUse, phinodes::IdDict{Int, SSAValue}, fidx::Int, use_idx::Int) - # Find the first dominating def - curblock = stmtblock = block_for_inst(ir.cfg, use_idx) - curblock = find_curblock(domtree, allblocks, curblock) - defblockdefs = let curblock = curblock - Int[stmt for stmt in du.defs if block_for_inst(ir.cfg, stmt) == curblock] - end - def = 0 - if !isempty(defblockdefs) - if curblock != stmtblock - # Find the last def in this block - def = 0 - for x in defblockdefs - def = max(def, x) - end - else - # Find the last def before our use - def = 0 - for x in defblockdefs - def = max(def, x >= use_idx ? 0 : x) - end - end - end +function compute_value_for_use(ir::IRCode, domtree::DomTree, allblocks::Vector{Int}, + du::SSADefUse, phinodes::IdDict{Int, SSAValue}, fidx::Int, use::Int) + def, useblock, curblock = find_def_for_use(ir, domtree, allblocks, du, use) if def == 0 if !haskey(phinodes, curblock) # If this happens, we need to search the predecessors for defs. Which # one doesn't matter - if it did, we'd have had a phinode - return compute_value_for_block(ir, domtree, allblocks, du, phinodes, fidx, first(ir.cfg.blocks[stmtblock].preds)) + return compute_value_for_block(ir, domtree, allblocks, du, phinodes, fidx, first(ir.cfg.blocks[useblock].preds)) end # The use is the phinode return phinodes[curblock] @@ -116,7 +118,75 @@ function compute_value_for_use(ir::IRCode, domtree::DomTree, allblocks::Vector{I end end -function simple_walk(compact::IncrementalCompact, @nospecialize(defssa#=::AnySSAValue=#), pi_callback=(pi, idx)->false) +# even when the allocation contains an uninitialized field, we try an extra effort to check +# if this load at `idx` have any "safe" `setfield!` calls that define the field +function has_safe_def( + ir::IRCode, domtree::DomTree, allblocks::Vector{Int}, du::SSADefUse, + newidx::Int, idx::Int) + def, _, _ = find_def_for_use(ir, domtree, allblocks, du, idx) + # will throw since we already checked this `:new` site doesn't define this field + def == newidx && return false + # found a "safe" definition + def ≠ 0 && return true + # we may still be able to replace this load with `PhiNode` + # examine if all predecessors of `block` have any "safe" definition + block = block_for_inst(ir, idx) + seen = BitSet(block) + worklist = BitSet(ir.cfg.blocks[block].preds) + isempty(worklist) && return false + while !isempty(worklist) + pred = pop!(worklist) + # if this block has already been examined, bail out to avoid infinite cycles + pred in seen && return false + idx = last(ir.cfg.blocks[pred].stmts) + # NOTE `idx` isn't a load, thus we can use inclusive coondition within the `find_def_for_use` + def, _, _ = find_def_for_use(ir, domtree, allblocks, du, idx, true) + # will throw since we already checked this `:new` site doesn't define this field + def == newidx && return false + push!(seen, pred) + # found a "safe" definition for this predecessor + def ≠ 0 && continue + # check for the predecessors of this predecessor + for newpred in ir.cfg.blocks[pred].preds + push!(worklist, newpred) + end + end + return true +end + +# find the first dominating def for the given use +function find_def_for_use( + ir::IRCode, domtree::DomTree, allblocks::Vector{Int}, du::SSADefUse, use::Int, inclusive::Bool=false) + useblock = block_for_inst(ir.cfg, use) + curblock = find_curblock(domtree, allblocks, useblock) + local def = 0 + for idx in du.defs + if block_for_inst(ir.cfg, idx) == curblock + if curblock != useblock + # Find the last def in this block + def = max(def, idx) + else + # Find the last def before our use + if inclusive + def = max(def, idx ≤ use ? idx : 0) + else + def = max(def, idx < use ? idx : 0) + end + end + end + end + return def, useblock, curblock +end + +function collect_leaves(compact::IncrementalCompact, @nospecialize(val), @nospecialize(typeconstraint)) + if isa(val, Union{OldSSAValue, SSAValue}) + val, typeconstraint = simple_walk_constraint(compact, val, typeconstraint) + end + return walk_to_defs(compact, val, typeconstraint) +end + +function simple_walk(compact::IncrementalCompact, @nospecialize(defssa#=::AnySSAValue=#), + callback = (@nospecialize(pi), @nospecialize(idx)) -> false) while true if isa(defssa, OldSSAValue) if already_inserted(compact, defssa) @@ -130,7 +200,7 @@ function simple_walk(compact::IncrementalCompact, @nospecialize(defssa#=::AnySSA end def = compact[defssa] if isa(def, PiNode) - if pi_callback(def, defssa) + if callback(def, defssa) return defssa end def = def.val @@ -141,7 +211,7 @@ function simple_walk(compact::IncrementalCompact, @nospecialize(defssa#=::AnySSA end defssa = def elseif isa(def, AnySSAValue) - pi_callback(def, defssa) + callback(def, defssa) if isa(def, SSAValue) is_old(compact, defssa) && (def = OldSSAValue(def.id)) end @@ -154,33 +224,31 @@ function simple_walk(compact::IncrementalCompact, @nospecialize(defssa#=::AnySSA end end -function simple_walk_constraint(compact::IncrementalCompact, @nospecialize(defidx), @nospecialize(typeconstraint) = types(compact)[defidx]) +function simple_walk_constraint(compact::IncrementalCompact, @nospecialize(defssa#=::AnySSAValue=#), + @nospecialize(typeconstraint)) callback = function (@nospecialize(pi), @nospecialize(idx)) - isa(pi, PiNode) && (typeconstraint = typeintersect(typeconstraint, widenconst(pi.typ))) + if isa(pi, PiNode) + typeconstraint = typeintersect(typeconstraint, widenconst(pi.typ)) + end return false end - def = simple_walk(compact, defidx, callback) + def = simple_walk(compact, defssa, callback) return Pair{Any, Any}(def, typeconstraint) end """ - walk_to_defs(compact, val, intermediaries) + walk_to_defs(compact, val, typeconstraint) -Starting at `val` walk use-def chains to get all the leaves feeding into -this val (pruning those leaves rules out by path conditions). +Starting at `val` walk use-def chains to get all the leaves feeding into this `val` +(pruning those leaves rules out by path conditions). """ -function walk_to_defs(compact::IncrementalCompact, @nospecialize(defssa), @nospecialize(typeconstraint), visited_phinodes::Vector{Any}=Any[]) - if !isa(defssa, AnySSAValue) || !isa(compact[defssa], PhiNode) - return Any[defssa] - end - # Step 2: Figure out what the struct is defined as +function walk_to_defs(compact::IncrementalCompact, @nospecialize(defssa), @nospecialize(typeconstraint)) + visited_phinodes = AnySSAValue[] + isa(defssa, AnySSAValue) || return Any[defssa], visited_phinodes def = compact[defssa] - ## Track definitions through PiNode/PhiNode - found_def = false - ## Track which PhiNodes, SSAValue intermediaries - ## we forwarded through. - visited = IdDict{Any, Any}() - worklist_defs = Any[] + isa(def, PhiNode) || return Any[defssa], visited_phinodes + visited_constraints = IdDict{AnySSAValue, Any}() + worklist_defs = AnySSAValue[] worklist_constraints = Any[] leaves = Any[] push!(worklist_defs, defssa) @@ -188,7 +256,7 @@ function walk_to_defs(compact::IncrementalCompact, @nospecialize(defssa), @nospe while !isempty(worklist_defs) defssa = pop!(worklist_defs) typeconstraint = pop!(worklist_constraints) - visited[defssa] = typeconstraint + visited_constraints[defssa] = typeconstraint def = compact[defssa] if isa(def, PhiNode) push!(visited_phinodes, defssa) @@ -199,8 +267,8 @@ function walk_to_defs(compact::IncrementalCompact, @nospecialize(defssa), @nospe if is_old(compact, defssa) && isa(val, SSAValue) val = OldSSAValue(val.id) end - edge_typ = widenconst(compact_exprtype(compact, val)) - typeintersect(edge_typ, typeconstraint) === Union{} && continue + edge_typ = widenconst(argextype(val, compact)) + hasintersect(edge_typ, typeconstraint) || continue push!(possible_predecessors, n) end for n in possible_predecessors @@ -212,15 +280,15 @@ function walk_to_defs(compact::IncrementalCompact, @nospecialize(defssa), @nospe if isa(val, AnySSAValue) new_def, new_constraint = simple_walk_constraint(compact, val, typeconstraint) if isa(new_def, AnySSAValue) - if !haskey(visited, new_def) + if !haskey(visited_constraints, new_def) push!(worklist_defs, new_def) push!(worklist_constraints, new_constraint) - elseif !(new_constraint <: visited[new_def]) + elseif !(new_constraint <: visited_constraints[new_def]) # We have reached the same definition via a different # path, with a different type constraint. We may have # to redo some work here with the wider typeconstraint push!(worklist_defs, new_def) - push!(worklist_constraints, tmerge(new_constraint, visited[new_def])) + push!(worklist_constraints, tmerge(new_constraint, visited_constraints[new_def])) end continue end @@ -240,12 +308,14 @@ function walk_to_defs(compact::IncrementalCompact, @nospecialize(defssa), @nospe push!(leaves, defssa) end end - leaves + return leaves, visited_phinodes end -function process_immutable_preserve(new_preserves::Vector{Any}, compact::IncrementalCompact, def::Expr) - for arg in (isexpr(def, :new) ? def.args : def.args[2:end]) - if !isbitstype(widenconst(compact_exprtype(compact, arg))) +function record_immutable_preserve!(new_preserves::Vector{Any}, def::Expr, compact::IncrementalCompact) + args = isexpr(def, :new) ? def.args : def.args[2:end] + for i = 1:length(args) + arg = args[i] + if !isbitstype(widenconst(argextype(arg, compact))) push!(new_preserves, arg) end end @@ -258,7 +328,7 @@ function already_inserted(compact::IncrementalCompact, old::OldSSAValue) end id -= length(compact.ir.stmts) if id < length(compact.ir.new_nodes) - error() + error("") end id -= length(compact.ir.new_nodes) @assert id <= length(compact.pending_nodes) @@ -269,55 +339,52 @@ function is_pending(compact::IncrementalCompact, old::OldSSAValue) return old.id > length(compact.ir.stmts) + length(compact.ir.new_nodes) end -function lift_leaves(compact::IncrementalCompact, @nospecialize(stmt), - @nospecialize(result_t), field::Int, leaves::Vector{Any}) +function is_getfield_captures(@nospecialize(def), compact::IncrementalCompact) + isa(def, Expr) || return false + length(def.args) >= 3 || return false + is_known_call(def, getfield, compact) || return false + which = argextype(def.args[3], compact) + isa(which, Const) || return false + which.val === :captures || return false + oc = argextype(def.args[2], compact) + return oc ⊑ Core.OpaqueClosure +end + +struct LiftedValue + x + LiftedValue(@nospecialize x) = new(x) +end +const LiftedLeaves = IdDict{Any, Union{Nothing,LiftedValue}} + +# try to compute lifted values that can replace `getfield(x, field)` call +# where `x` is an immutable struct that are defined at any of `leaves` +function lift_leaves(compact::IncrementalCompact, + @nospecialize(result_t), field::Int, leaves::Vector{Any}) # For every leaf, the lifted value - lifted_leaves = IdDict{Any, Any}() + lifted_leaves = LiftedLeaves() maybe_undef = false - for leaf in leaves - leaf_key = leaf + for i = 1:length(leaves) + leaf = leaves[i] + cache_key = leaf if isa(leaf, AnySSAValue) - if isa(leaf, OldSSAValue) && already_inserted(compact, leaf) - leaf = compact.ssa_rename[leaf.id] - if isa(leaf, AnySSAValue) - leaf = simple_walk(compact, leaf) - end - if isa(leaf, AnySSAValue) - def = compact[leaf] - else - def = leaf - end - else - def = compact[leaf] - end - if is_tuple_call(compact, def) && isa(field, Int) && 1 <= field < length(def.args) - lifted = def.args[1+field] - if is_old(compact, leaf) && isa(lifted, SSAValue) - lifted = OldSSAValue(lifted.id) - end - if isa(lifted, GlobalRef) || isa(lifted, Expr) - lifted = insert_node!(compact, leaf, compact_exprtype(compact, lifted), lifted) - def.args[1+field] = lifted - (isa(leaf, SSAValue) && (leaf.id < compact.result_idx)) && push!(compact.late_fixup, leaf.id) - end - lifted_leaves[leaf_key] = RefValue{Any}(lifted) + (def, leaf) = walk_to_def(compact, leaf) + if is_known_call(def, tuple, compact) && 1 ≤ field < length(def.args) + lift_arg!(compact, leaf, cache_key, def, 1+field, lifted_leaves) continue elseif isexpr(def, :new) - typ = widenconst(types(compact)[leaf]) - if isa(typ, UnionAll) - typ = unwrap_unionall(typ) - end - (isa(typ, DataType) && (!typ.abstract)) || return nothing - @assert !typ.mutable - field = try_compute_fieldidx_expr(typ, stmt) - field === nothing && return nothing - if length(def.args) < 1 + field + typ = unwrap_unionall(widenconst(types(compact)[leaf])) + (isa(typ, DataType) && !isabstracttype(typ)) || return nothing + @assert !ismutabletype(typ) + if length(def.args) < 1+field + if field > fieldcount(typ) + return nothing + end ftyp = fieldtype(typ, field) if !isbitstype(ftyp) # On this branch, this will be a guaranteed UndefRefError. # We use the regular undef mechanic to lift this to a boolean slot maybe_undef = true - lifted_leaves[leaf_key] = nothing + lifted_leaves[cache_key] = nothing continue end return nothing @@ -325,45 +392,36 @@ function lift_leaves(compact::IncrementalCompact, @nospecialize(stmt), compact[leaf] = nothing for i = (length(def.args) + 1):(1+field) ftyp = fieldtype(typ, i - 1) - isbits(ftyp) || return nothing - push!(def.args, insert_node!(compact, leaf, result_t, Expr(:new, ftyp))) + isbitstype(ftyp) || return nothing + ninst = effect_free(NewInstruction(Expr(:new, ftyp), result_t)) + push!(def.args, insert_node!(compact, leaf, ninst)) end compact[leaf] = def end - lifted = def.args[1+field] - if is_old(compact, leaf) && isa(lifted, SSAValue) - lifted = OldSSAValue(lifted.id) + lift_arg!(compact, leaf, cache_key, def, 1+field, lifted_leaves) + continue + elseif is_getfield_captures(def, compact) + # Walk to new_opaque_closure + ocleaf = def.args[2] + if isa(ocleaf, AnySSAValue) + ocleaf = simple_walk(compact, ocleaf) end - if isa(lifted, GlobalRef) || isa(lifted, Expr) - lifted = insert_node!(compact, leaf, compact_exprtype(compact, lifted), lifted) - def.args[1+field] = lifted - (isa(leaf, SSAValue) && (leaf.id < compact.result_idx)) && push!(compact.late_fixup, leaf.id) + ocdef, _ = walk_to_def(compact, ocleaf) + if isexpr(ocdef, :new_opaque_closure) && isa(field, Int) && 1 ≤ field ≤ length(ocdef.args)-4 + lift_arg!(compact, leaf, cache_key, ocdef, 4+field, lifted_leaves) + continue end - lifted_leaves[leaf_key] = RefValue{Any}(lifted) - continue + return nothing else - typ = compact_exprtype(compact, leaf) + typ = argextype(leaf, compact) if !isa(typ, Const) + # TODO: (disabled since #27126) # If the leaf is an old ssa value, insert a getfield here # We will revisit this getfield later when compaction gets # to the appropriate point. # N.B.: This can be a bit dangerous because it can lead to # infinite loops if we accidentally insert a node just ahead # of where we are - if is_old(compact, leaf) && (isa(field, Int) || isa(field, Symbol)) - (isa(typ, DataType) && (!typ.abstract)) || return nothing - @assert !typ.mutable - # If there's the potential for an undefref error on access, we cannot insert a getfield - if field > typ.ninitialized && !isbits(fieldtype(typ, field)) - return nothing - lifted_leaves[leaf] = RefValue{Any}(insert_node!(compact, leaf, make_MaybeUndef(result_t), Expr(:call, :unchecked_getfield, SSAValue(leaf.id), field), true)) - maybe_undef = true - else - return nothing - lifted_leaves[leaf] = RefValue{Any}(insert_node!(compact, leaf, result_t, Expr(:call, getfield, SSAValue(leaf.id), field), true)) - end - continue - end return nothing end leaf = typ.val @@ -374,72 +432,157 @@ function lift_leaves(compact::IncrementalCompact, @nospecialize(stmt), elseif isa(leaf, GlobalRef) mod, name = leaf.mod, leaf.name if isdefined(mod, name) && isconst(mod, name) - leaf = getfield(mod, name) + leaf = getglobal(mod, name) else return nothing end - elseif isa(leaf, Union{Argument, Expr}) + elseif isa(leaf, Argument) || isa(leaf, Expr) return nothing end - !ismutable(leaf) || return nothing + ismutable(leaf) && return nothing isdefined(leaf, field) || return nothing val = getfield(leaf, field) is_inlineable_constant(val) || return nothing - lifted_leaves[leaf_key] = RefValue{Any}(quoted(val)) + lifted_leaves[cache_key] = LiftedValue(quoted(val)) + end + return lifted_leaves, maybe_undef +end + +function lift_arg!( + compact::IncrementalCompact, @nospecialize(leaf), @nospecialize(cache_key), + stmt::Expr, argidx::Int, lifted_leaves::LiftedLeaves) + lifted = stmt.args[argidx] + if is_old(compact, leaf) && isa(lifted, SSAValue) + lifted = OldSSAValue(lifted.id) + if already_inserted(compact, lifted) + lifted = compact.ssa_rename[lifted.id] + end + end + if isa(lifted, GlobalRef) || isa(lifted, Expr) + lifted = insert_node!(compact, leaf, effect_free(NewInstruction(lifted, argextype(lifted, compact)))) + compact[leaf] = nothing + stmt.args[argidx] = lifted + compact[leaf] = stmt + if isa(leaf, SSAValue) && leaf.id < compact.result_idx + push!(compact.late_fixup, leaf.id) + end end - lifted_leaves, maybe_undef + lifted_leaves[cache_key] = LiftedValue(lifted) + nothing +end + +function walk_to_def(compact::IncrementalCompact, @nospecialize(leaf)) + if isa(leaf, OldSSAValue) && already_inserted(compact, leaf) + leaf = compact.ssa_rename[leaf.id] + if isa(leaf, AnySSAValue) + leaf = simple_walk(compact, leaf) + end + if isa(leaf, AnySSAValue) + def = compact[leaf] + else + def = leaf + end + elseif isa(leaf, AnySSAValue) + def = compact[leaf] + else + def = leaf + end + return Pair{Any, Any}(def, leaf) end make_MaybeUndef(@nospecialize(typ)) = isa(typ, MaybeUndef) ? typ : MaybeUndef(typ) -function lift_comparison!(compact::IncrementalCompact, idx::Int, - @nospecialize(c1), @nospecialize(c2), stmt::Expr, - lifting_cache::IdDict{Pair{AnySSAValue, Any}, AnySSAValue}) - if isa(c1, Const) - cmp = c1 - typeconstraint = widenconst(c2) - val = stmt.args[3] +""" + lift_comparison!(cmp, compact::IncrementalCompact, idx::Int, stmt::Expr) + +Replaces `cmp(φ(x, y)::Union{X,Y}, constant)` by `φ(cmp(x, constant), cmp(y, constant))`, +where `cmp(x, constant)` and `cmp(y, constant)` can be replaced with constant `Bool`eans. +It helps codegen avoid generating expensive code for `cmp` with `Union` types. +In particular, this is supposed to improve the performance of the iteration protocol: +```julia +while x !== nothing + x = iterate(...)::Union{Nothing,Tuple{Any,Any}} +end +``` +""" +function lift_comparison! end + +function lift_comparison!(::typeof(===), compact::IncrementalCompact, + idx::Int, stmt::Expr, lifting_cache::IdDict{Pair{AnySSAValue, Any}, AnySSAValue}) + args = stmt.args + length(args) == 3 || return + lhs, rhs = args[2], args[3] + vl = argextype(lhs, compact) + vr = argextype(rhs, compact) + if isa(vl, Const) + isa(vr, Const) && return + val = rhs + cmp = vl + elseif isa(vr, Const) + val = lhs + cmp = vr else - cmp = c2 - typeconstraint = widenconst(c1) - val = stmt.args[2] + return end + lift_comparison_leaves!(egal_tfunc, compact, val, cmp, lifting_cache, idx) +end - is_type_only = isdefined(typeof(cmp), :instance) +function lift_comparison!(::typeof(isa), compact::IncrementalCompact, + idx::Int, stmt::Expr, lifting_cache::IdDict{Pair{AnySSAValue, Any}, AnySSAValue}) + args = stmt.args + length(args) == 3 || return + cmp = argextype(args[3], compact) + val = args[2] + lift_comparison_leaves!(isa_tfunc, compact, val, cmp, lifting_cache, idx) +end + +function lift_comparison!(::typeof(isdefined), compact::IncrementalCompact, + idx::Int, stmt::Expr, lifting_cache::IdDict{Pair{AnySSAValue, Any}, AnySSAValue}) + args = stmt.args + length(args) == 3 || return + cmp = argextype(args[3], compact) + isa(cmp, Const) || return # `isdefined_tfunc` won't return Const + val = args[2] + lift_comparison_leaves!(isdefined_tfunc, compact, val, cmp, lifting_cache, idx) +end +function lift_comparison_leaves!(@specialize(tfunc), + compact::IncrementalCompact, @nospecialize(val), @nospecialize(cmp), + lifting_cache::IdDict{Pair{AnySSAValue, Any}, AnySSAValue}, idx::Int) + typeconstraint = widenconst(argextype(val, compact)) if isa(val, Union{OldSSAValue, SSAValue}) val, typeconstraint = simple_walk_constraint(compact, val, typeconstraint) end - - visited_phinodes = Any[] - leaves = walk_to_defs(compact, val, typeconstraint, visited_phinodes) - - # Let's check if we evaluate the comparison for each one of the leaves - lifted_leaves = IdDict{Any, Any}() - for leaf in leaves - r = egal_tfunc(compact_exprtype(compact, leaf), cmp) - if isa(r, Const) - lifted_leaves[leaf] = RefValue{Any}(r.val) + isa(typeconstraint, Union) || return # bail out if there won't be a good chance for lifting + leaves, visited_phinodes = collect_leaves(compact, val, typeconstraint) + length(leaves) ≤ 1 && return # bail out if we don't have multiple leaves + + # check if we can evaluate the comparison for each one of the leaves + lifted_leaves = nothing + for i = 1:length(leaves) + leaf = leaves[i] + result = tfunc(argextype(leaf, compact), cmp) + if isa(result, Const) + if lifted_leaves === nothing + lifted_leaves = LiftedLeaves() + end + lifted_leaves[leaf] = LiftedValue(result.val) else - # TODO: In some cases it might be profitable to hoist the === - # here. - return + return # TODO In some cases it might be profitable to hoist the comparison here end end - lifted_val = perform_lifting!(compact, visited_phinodes, cmp, lifting_cache, Bool, lifted_leaves, val) - @assert lifted_val !== nothing + # perform lifting + lifted_val = perform_lifting!(compact, + visited_phinodes, cmp, lifting_cache, Bool, + lifted_leaves::LiftedLeaves, val, nothing)::LiftedValue - #global assertion_counter - #assertion_counter::Int += 1 - #insert_node_here!(compact, Expr(:assert_egal, Symbol(string("assert_egal_", assertion_counter)), SSAValue(idx), lifted_val), nothing, 0, true) - #return compact[idx] = lifted_val.x end struct LiftedPhi ssa::AnySSAValue - node::Any + node::PhiNode need_argupdate::Bool end @@ -449,40 +592,88 @@ function is_old(compact, @nospecialize(old_node_ssa)) !already_inserted(compact, old_node_ssa) end +mutable struct LazyDomtree + ir::IRCode + domtree::DomTree + LazyDomtree(ir::IRCode) = new(ir) +end +function get(x::LazyDomtree) + isdefined(x, :domtree) && return x.domtree + return @timeit "domtree 2" x.domtree = construct_domtree(x.ir.cfg.blocks) +end + function perform_lifting!(compact::IncrementalCompact, - visited_phinodes::Vector{Any}, @nospecialize(cache_key), - lifting_cache::IdDict{Pair{AnySSAValue, Any}, AnySSAValue}, - @nospecialize(result_t), lifted_leaves::IdDict{Any, Any}, @nospecialize(stmt_val)) - reverse_mapping = IdDict{Any, Any}(ssa => id for (id, ssa) in enumerate(visited_phinodes)) + visited_phinodes::Vector{AnySSAValue}, @nospecialize(cache_key), + lifting_cache::IdDict{Pair{AnySSAValue, Any}, AnySSAValue}, + @nospecialize(result_t), lifted_leaves::LiftedLeaves, @nospecialize(stmt_val), + lazydomtree::Union{LazyDomtree,Nothing}) + reverse_mapping = IdDict{AnySSAValue, Int}(ssa => id for (id, ssa) in enumerate(visited_phinodes)) + + # Check if all the lifted leaves are the same + local the_leaf + all_same = true + for (_, val) in lifted_leaves + if !@isdefined(the_leaf) + the_leaf = val + continue + end + if val !== the_leaf + all_same = false + end + end + + the_leaf_val = isa(the_leaf, LiftedValue) ? the_leaf.x : nothing + if !isa(the_leaf_val, SSAValue) + all_same = false + end + + if all_same + dominates_all = true + if lazydomtree !== nothing + domtree = get(lazydomtree) + for item in visited_phinodes + if !dominates_ssa(compact, domtree, the_leaf_val, item) + dominates_all = false + break + end + end + if dominates_all + return the_leaf + end + end + end # Insert PhiNodes lifted_phis = LiftedPhi[] for item in visited_phinodes - if (item, cache_key) in keys(lifting_cache) - ssa = lifting_cache[Pair{AnySSAValue, Any}(item, cache_key)] - push!(lifted_phis, LiftedPhi(ssa, compact[ssa], false)) + # FIXME this cache is broken somehow + # ckey = Pair{AnySSAValue, Any}(item, cache_key) + # cached = ckey in keys(lifting_cache) + cached = false + if cached + ssa = lifting_cache[ckey] + push!(lifted_phis, LiftedPhi(ssa, compact[ssa]::PhiNode, false)) continue end n = PhiNode() - ssa = insert_node!(compact, item, result_t, n) - lifting_cache[Pair{AnySSAValue, Any}(item, cache_key)] = ssa + ssa = insert_node!(compact, item, effect_free(NewInstruction(n, result_t))) + # lifting_cache[ckey] = ssa push!(lifted_phis, LiftedPhi(ssa, n, true)) end # Fix up arguments for (old_node_ssa, lf) in zip(visited_phinodes, lifted_phis) - old_node = compact[old_node_ssa] + old_node = compact[old_node_ssa]::PhiNode new_node = lf.node lf.need_argupdate || continue for i = 1:length(old_node.edges) edge = old_node.edges[i] isassigned(old_node.values, i) || continue val = old_node.values[i] - orig_val = val if is_old(compact, old_node_ssa) && isa(val, SSAValue) val = OldSSAValue(val.id) end - if isa(val, Union{NewSSAValue, SSAValue, OldSSAValue}) + if isa(val, AnySSAValue) val = simple_walk(compact, val) end if val in keys(lifted_leaves) @@ -492,22 +683,20 @@ function perform_lifting!(compact::IncrementalCompact, resize!(new_node.values, length(new_node.values)+1) continue end - lifted_val = lifted_val.x - if isa(lifted_val, Union{NewSSAValue, SSAValue, OldSSAValue}) - lifted_val = simple_walk(compact, lifted_val, (pi, idx)->true) + val = lifted_val.x + if isa(val, AnySSAValue) + callback = (@nospecialize(pi), @nospecialize(idx)) -> true + val = simple_walk(compact, val, callback) end - push!(new_node.values, lifted_val) - elseif isa(val, Union{NewSSAValue, SSAValue, OldSSAValue}) && val in keys(reverse_mapping) + push!(new_node.values, val) + elseif isa(val, AnySSAValue) && val in keys(reverse_mapping) push!(new_node.edges, edge) push!(new_node.values, lifted_phis[reverse_mapping[val]].ssa) else # Probably ignored by path condition, skip this end end - end - - for lf in lifted_phis - count_added_node!(compact, lf.node) + count_added_node!(compact, new_node) end # Fixup the stmt itself @@ -516,81 +705,77 @@ function perform_lifting!(compact::IncrementalCompact, end if stmt_val in keys(lifted_leaves) - stmt_val = lifted_leaves[stmt_val] - else - isa(stmt_val, Union{SSAValue, OldSSAValue}) && stmt_val in keys(reverse_mapping) - stmt_val = RefValue{Any}(lifted_phis[reverse_mapping[stmt_val]].ssa) + return lifted_leaves[stmt_val] + elseif isa(stmt_val, AnySSAValue) && stmt_val in keys(reverse_mapping) + return LiftedValue(lifted_phis[reverse_mapping[stmt_val]].ssa) end - return stmt_val + return stmt_val # N.B. should never happen end -assertion_counter = 0 -function getfield_elim_pass!(ir::IRCode) +# NOTE we use `IdSet{Int}` instead of `BitSet` for in these passes since they work on IR after inlining, +# which can be very large sometimes, and program counters in question are often very sparse +const SPCSet = IdSet{Int} + +""" + sroa_pass!(ir::IRCode) -> newir::IRCode + +`getfield` elimination pass, a.k.a. Scalar Replacements of Aggregates optimization. + +This pass is based on a local field analysis by def-use chain walking. +It looks for struct allocation sites ("definitions"), and `getfield` calls as well as +`:foreigncall`s that preserve the structs ("usages"). If "definitions" have enough information, +then this pass will replace corresponding usages with forwarded values. +`mutable struct`s require additional cares and need to be handled separately from immutables. +For `mutable struct`s, `setfield!` calls account for "definitions" also, and the pass should +give up the lifting conservatively when there are any "intermediate usages" that may escape +the mutable struct (e.g. non-inlined generic function call that takes the mutable struct as +its argument). + +In a case when all usages are fully eliminated, `struct` allocation may also be erased as +a result of succeeding dead code elimination. +""" +function sroa_pass!(ir::IRCode) compact = IncrementalCompact(ir) - insertions = Vector{Any}() - defuses = IdDict{Int, Tuple{IdSet{Int}, SSADefUse}}() + defuses = nothing # will be initialized once we encounter mutability in order to reduce dynamic allocations lifting_cache = IdDict{Pair{AnySSAValue, Any}, AnySSAValue}() - revisit_worklist = Int[] - #ndone, nmax = 0, 200 + # initialization of domtree is delayed to avoid the expensive computation in many cases + lazydomtree = LazyDomtree(ir) for ((_, idx), stmt) in compact + # check whether this statement is `getfield` / `setfield!` (or other "interesting" statement) isa(stmt, Expr) || continue - #ndone >= nmax && continue - #ndone += 1 - result_t = compact_exprtype(compact, SSAValue(idx)) - is_getfield = is_setfield = false - is_ccall = false - is_unchecked = false - # Step 1: Check whether the statement we're looking at is a getfield/setfield! + is_setfield = is_isdefined = false + field_ordering = :unspecified if is_known_call(stmt, setfield!, compact) - is_setfield = true 4 <= length(stmt.args) <= 5 || continue + is_setfield = true + if length(stmt.args) == 5 + field_ordering = argextype(stmt.args[5], compact) + end elseif is_known_call(stmt, getfield, compact) - is_getfield = true + 3 <= length(stmt.args) <= 5 || continue + if length(stmt.args) == 5 + field_ordering = argextype(stmt.args[5], compact) + elseif length(stmt.args) == 4 + field_ordering = argextype(stmt.args[4], compact) + widenconst(field_ordering) === Bool && (field_ordering = :unspecified) + end + elseif is_known_call(stmt, isdefined, compact) 3 <= length(stmt.args) <= 4 || continue - elseif is_known_call(stmt, isa, compact) - # TODO - continue - elseif is_known_call(stmt, typeassert, compact) - # Canonicalize - # X = typeassert(Y, T)::S - # into - # typeassert(Y, T) - # X = PiNode(Y, S) - # N.B.: Inference may have a more precise type for `S`, than - # just T, but from here on out, there's no problem with - # using just using that. - # so subsequent analysis only has to deal with the latter - # form. TODO: This isn't the best place to put this. - # Also, we should probably have a version of typeassert - # that's defined not to return its value to make life easier - # for the backend. - pi = insert_node_here!(compact, - PiNode(stmt.args[2], compact.result[idx][:type]), - compact.result[idx][:type], - compact.result[idx][:line], true) - compact.ssa_rename[compact.idx-1] = pi - continue - elseif is_known_call(stmt, (===), compact) - c1 = compact_exprtype(compact, stmt.args[2]) - c2 = compact_exprtype(compact, stmt.args[3]) - if !(isa(c1, Const) || isa(c2, Const)) - continue + is_isdefined = true + if length(stmt.args) == 4 + field_ordering = argextype(stmt.args[4], compact) + widenconst(field_ordering) === Bool && (field_ordering = :unspecified) end - (isa(c1, Const) && isa(c2, Const)) && continue - lift_comparison!(compact, idx, c1, c2, stmt, lifting_cache) - continue - elseif isexpr(stmt, :call) && stmt.args[1] === :unchecked_getfield - is_getfield = true - is_unchecked = true elseif isexpr(stmt, :foreigncall) nccallargs = length(stmt.args[3]::SimpleVector) + preserved = Int[] new_preserves = Any[] - old_preserves = stmt.args[(6+nccallargs):end] - for (pidx, preserved_arg) in enumerate(old_preserves) + for pidx in (6+nccallargs):length(stmt.args) + preserved_arg = stmt.args[pidx] isa(preserved_arg, SSAValue) || continue - let intermediaries = IdSet() - callback = function(@nospecialize(pi), ssa::AnySSAValue) + let intermediaries = SPCSet() + callback = function (@nospecialize(pi), @nospecialize(ssa)) push!(intermediaries, ssa.id) return false end @@ -598,146 +783,160 @@ function getfield_elim_pass!(ir::IRCode) isa(def, SSAValue) || continue defidx = def.id def = compact[defidx] - if is_tuple_call(compact, def) - process_immutable_preserve(new_preserves, compact, def) - old_preserves[pidx] = nothing + if is_known_call(def, tuple, compact) + record_immutable_preserve!(new_preserves, def, compact) + push!(preserved, preserved_arg.id) continue elseif isexpr(def, :new) - typ = widenconst(compact_exprtype(compact, SSAValue(defidx))) - if isa(typ, UnionAll) - typ = unwrap_unionall(typ) - end - if typ isa DataType && !typ.mutable - process_immutable_preserve(new_preserves, compact, def) - old_preserves[pidx] = nothing + typ = unwrap_unionall(widenconst(argextype(SSAValue(defidx), compact))) + if typ isa DataType && !ismutabletype(typ) + record_immutable_preserve!(new_preserves, def, compact) + push!(preserved, preserved_arg.id) continue end else continue end - mid, defuse = get!(defuses, defidx, (IdSet{Int}(), SSADefUse())) - push!(defuse.ccall_preserve_uses, idx) + if defuses === nothing + defuses = IdDict{Int, Tuple{SPCSet, SSADefUse}}() + end + mid, defuse = get!(()->(SPCSet(),SSADefUse()), defuses, defidx) + push!(defuse.uses, PreserveUse(idx)) union!(mid, intermediaries) end continue end if !isempty(new_preserves) - old_preserves = filter(ssa->ssa !== nothing, old_preserves) - new_expr = Expr(:foreigncall, stmt.args[1:(6+nccallargs-1)]..., - old_preserves..., new_preserves...) - compact[idx] = new_expr + compact[idx] = nothing + compact[idx] = form_new_preserves(stmt, preserved, new_preserves) end continue - else + else # TODO: This isn't the best place to put these + if is_known_call(stmt, typeassert, compact) + canonicalize_typeassert!(compact, idx, stmt) + elseif is_known_call(stmt, (===), compact) + lift_comparison!(===, compact, idx, stmt, lifting_cache) + elseif is_known_call(stmt, isa, compact) + lift_comparison!(isa, compact, idx, stmt, lifting_cache) + end continue end - ## Normalize the field argument to getfield/setfield - field = stmt.args[3] - isa(field, QuoteNode) && (field = field.value) - isa(field, Union{Int, Symbol}) || continue - struct_typ = unwrap_unionall(widenconst(compact_exprtype(compact, stmt.args[2]))) + # analyze this `getfield` / `isdefined` / `setfield!` call + + field = try_compute_field_stmt(compact, stmt) + field === nothing && continue + + val = stmt.args[2] + + struct_typ = unwrap_unionall(widenconst(argextype(val, compact))) + if isa(struct_typ, Union) && struct_typ <: Tuple + struct_typ = unswitchtupleunion(struct_typ) + end + if isa(struct_typ, Union) && is_isdefined + lift_comparison!(isdefined, compact, idx, stmt, lifting_cache) + continue + end isa(struct_typ, DataType) || continue - def, typeconstraint = stmt.args[2], struct_typ + struct_typ.name.atomicfields == C_NULL || continue # TODO: handle more + if !((field_ordering === :unspecified) || + (field_ordering isa Const && field_ordering.val === :not_atomic)) + continue + end - if struct_typ.mutable - isa(def, SSAValue) || continue - let intermediaries = IdSet() - callback = function(@nospecialize(pi), ssa::AnySSAValue) + # analyze this mutable struct here for the later pass + if ismutabletype(struct_typ) + isa(val, SSAValue) || continue + let intermediaries = SPCSet() + callback = function (@nospecialize(pi), @nospecialize(ssa)) push!(intermediaries, ssa.id) return false end - def = simple_walk(compact, def, callback) + def = simple_walk(compact, val, callback) # Mutable stuff here isa(def, SSAValue) || continue - mid, defuse = get!(defuses, def.id, (IdSet{Int}(), SSADefUse())) + if defuses === nothing + defuses = IdDict{Int, Tuple{SPCSet, SSADefUse}}() + end + mid, defuse = get!(()->(SPCSet(),SSADefUse()), defuses, def.id) if is_setfield push!(defuse.defs, idx) + elseif is_isdefined + push!(defuse.uses, IsdefinedUse(idx)) else - push!(defuse.uses, idx) + push!(defuse.uses, GetfieldUse(idx)) end union!(mid, intermediaries) end continue elseif is_setfield - continue + continue # invalid `setfield!` call, but just ignore here + elseif is_isdefined + continue # TODO? end - if isa(def, Union{OldSSAValue, SSAValue}) - def, typeconstraint = simple_walk_constraint(compact, def, typeconstraint) - end + # perform SROA on immutable structs here on - visited_phinodes = Any[] - leaves = walk_to_defs(compact, def, typeconstraint, visited_phinodes) + field = try_compute_fieldidx(struct_typ, field) + field === nothing && continue + leaves, visited_phinodes = collect_leaves(compact, val, struct_typ) isempty(leaves) && continue - field = try_compute_fieldidx_expr(struct_typ, stmt) - field === nothing && continue - - r = lift_leaves(compact, stmt, result_t, field, leaves) - r === nothing && continue - lifted_leaves, any_undef = r + result_t = argextype(SSAValue(idx), compact) + lifted_result = lift_leaves(compact, result_t, field, leaves) + lifted_result === nothing && continue + lifted_leaves, any_undef = lifted_result if any_undef result_t = make_MaybeUndef(result_t) end -# @Base.show result_t -# @Base.show stmt -# for (k,v) in lifted_leaves -# @Base.show (k, v) -# if isa(k, AnySSAValue) -# @Base.show compact[k] -# end -# if isa(v, RefValue) && isa(v.x, AnySSAValue) -# @Base.show compact[v.x] -# end -# end - val = perform_lifting!(compact, visited_phinodes, field, lifting_cache, result_t, lifted_leaves, stmt.args[2]) + val = perform_lifting!(compact, + visited_phinodes, field, lifting_cache, result_t, lifted_leaves, val, lazydomtree) # Insert the undef check if necessary - if any_undef && !is_unchecked + if any_undef if val === nothing - insert_node!(compact, SSAValue(idx), Nothing, Expr(:throw_undef_if_not, Symbol("##getfield##"), false)) + insert_node!(compact, SSAValue(idx), + non_effect_free(NewInstruction(Expr(:throw_undef_if_not, Symbol("##getfield##"), false), Nothing))) else - insert_node!(compact, SSAValue(idx), Nothing, Expr(:undefcheck, Symbol("##getfield##"), val.x)) + # val must be defined end else @assert val !== nothing end - global assertion_counter - assertion_counter::Int += 1 - #insert_node_here!(compact, Expr(:assert_egal, Symbol(string("assert_egal_", assertion_counter)), SSAValue(idx), val), nothing, 0, true) - #continue compact[idx] = val === nothing ? nothing : val.x end - non_dce_finish!(compact) - # Copy the use count, `simple_dce!` may modify it and for our predicate - # below we need it consistent with the state of the IR here (after tracking - # phi node arguments, but before dce). - used_ssas = copy(compact.used_ssas) - simple_dce!(compact) - ir = complete(compact) - - # Compute domtree, needed below, now that we have finished compacting the - # IR. This needs to be after we iterate through the IR with - # `IncrementalCompact` because removing dead blocks can invalidate the - # domtree. - @timeit "domtree 2" domtree = construct_domtree(ir.cfg) - - # Now go through any mutable structs and see which ones we can eliminate + if defuses !== nothing + # now go through analyzed mutable structs and see which ones we can eliminate + # NOTE copy the use count here, because `simple_dce!` may modify it and we need it + # consistent with the state of the IR here (after tracking `PhiNode` arguments, + # but before the DCE) for our predicate within `sroa_mutables!`, but we also + # try an extra effort using a callback so that reference counts are updated + used_ssas = copy(compact.used_ssas) + simple_dce!(compact, (x::SSAValue) -> used_ssas[x.id] -= 1) + ir = complete(compact) + sroa_mutables!(ir, defuses, used_ssas, lazydomtree) + return ir + else + simple_dce!(compact) + return complete(compact) + end +end + +function sroa_mutables!(ir::IRCode, defuses::IdDict{Int, Tuple{SPCSet, SSADefUse}}, used_ssas::Vector{Int}, lazydomtree::LazyDomtree) for (idx, (intermediaries, defuse)) in defuses intermediaries = collect(intermediaries) # Check if there are any uses we did not account for. If so, the variable # escapes and we cannot eliminate the allocation. This works, because we're guaranteed # not to include any intermediaries that have dead uses. As a result, missing uses will only ever # show up in the nuses_total count. - nleaves = length(defuse.uses) + length(defuse.defs) + length(defuse.ccall_preserve_uses) + nleaves = length(defuse.uses) + length(defuse.defs) nuses = 0 for idx in intermediaries nuses += used_ssas[idx] @@ -745,112 +944,210 @@ function getfield_elim_pass!(ir::IRCode) nuses_total = used_ssas[idx] + nuses - length(intermediaries) nleaves == nuses_total || continue # Find the type for this allocation - defexpr = ir[SSAValue(idx)] + defexpr = ir[SSAValue(idx)][:inst] isexpr(defexpr, :new) || continue - typ = ir.stmts[idx][:type] - if isa(typ, UnionAll) - typ = unwrap_unionall(typ) - end - # Could still end up here if we tried to setfield! and immutable, which would + newidx = idx + typ = unwrap_unionall(ir.stmts[newidx][:type]) + # Could still end up here if we tried to setfield! on an immutable, which would # error at runtime, but is not illegal to have in the IR. - typ.mutable || continue + ismutabletype(typ) || continue + typ = typ::DataType # Partition defuses by field fielddefuse = SSADefUse[SSADefUse() for _ = 1:fieldcount(typ)] - ok = true + all_eliminated = all_forwarded = true for use in defuse.uses - stmt = ir[SSAValue(use)] + if use.kind === :preserve + for du in fielddefuse + push!(du.uses, use) + end + continue + end + stmt = ir[SSAValue(use.idx)][:inst] # == `getfield`/`isdefined` call # We may have discovered above that this use is dead # after the getfield elim of immutables. In that case, # it would have been deleted. That's fine, just ignore # the use in that case. - stmt === nothing && continue - field = try_compute_fieldidx_expr(typ, stmt) - field === nothing && (ok = false; break) + if stmt === nothing + all_forwarded = false + continue + end + field = try_compute_fieldidx_stmt(ir, stmt::Expr, typ) + field === nothing && @goto skip push!(fielddefuse[field].uses, use) end - ok || continue - for use in defuse.defs - field = try_compute_fieldidx_expr(typ, ir[SSAValue(use)]) - field === nothing && (ok = false; break) - push!(fielddefuse[field].defs, use) + for def in defuse.defs + stmt = ir[SSAValue(def)][:inst]::Expr # == `setfield!` call + field = try_compute_fieldidx_stmt(ir, stmt, typ) + field === nothing && @goto skip + isconst(typ, field) && @goto skip # we discovered an attempt to mutate a const field, which must error + push!(fielddefuse[field].defs, def) end - ok || continue # Check that the defexpr has defined values for all the fields # we're accessing. In the future, we may want to relax this, # but we should come up with semantics for well defined semantics # for uninitialized fields first. - for (fidx, du) in pairs(fielddefuse) + ndefuse = length(fielddefuse) + blocks = Vector{Tuple{#=phiblocks=# Vector{Int}, #=allblocks=# Vector{Int}}}(undef, ndefuse) + for fidx in 1:ndefuse + du = fielddefuse[fidx] isempty(du.uses) && continue + push!(du.defs, newidx) + ldu = compute_live_ins(ir.cfg, du) + if isempty(ldu.live_in_bbs) + phiblocks = Int[] + else + phiblocks = iterated_dominance_frontier(ir.cfg, ldu, get(lazydomtree)) + end + allblocks = sort(vcat(phiblocks, ldu.def_bbs)) + blocks[fidx] = phiblocks, allblocks if fidx + 1 > length(defexpr.args) - ok = false - break + for i = 1:length(du.uses) + use = du.uses[i] + if use.kind === :isdefined + if has_safe_def(ir, get(lazydomtree), allblocks, du, newidx, use.idx) + ir[SSAValue(use.idx)][:inst] = true + else + all_eliminated = false + end + continue + elseif use.kind === :preserve + if length(du.defs) == 1 # allocation with this field unintialized + # there is nothing to preserve, just ignore this use + du.uses[i] = NoPreserve() + continue + end + end + has_safe_def(ir, get(lazydomtree), allblocks, du, newidx, use.idx) || @goto skip + end + else # always have some definition at the allocation site + for i = 1:length(du.uses) + use = du.uses[i] + if use.kind === :isdefined + ir[SSAValue(use.idx)][:inst] = true + end + end end end - ok || continue - preserve_uses = IdDict{Int, Vector{Any}}((idx=>Any[] for idx in IdSet{Int}(defuse.ccall_preserve_uses))) - # Everything accounted for. Go field by field and perform idf - for (fidx, du) in pairs(fielddefuse) + # Everything accounted for. Go field by field and perform idf: + # Compute domtree now, needed below, now that we have finished compacting the IR. + # This needs to be after we iterate through the IR with `IncrementalCompact` + # because removing dead blocks can invalidate the domtree. + domtree = get(lazydomtree) + local preserve_uses = nothing + for fidx in 1:ndefuse + du = fielddefuse[fidx] ftyp = fieldtype(typ, fidx) if !isempty(du.uses) - push!(du.defs, idx) - ldu = compute_live_ins(ir.cfg, du) - phiblocks = Int[] - if !isempty(ldu.live_in_bbs) - phiblocks = idf(ir.cfg, ldu, domtree) - end + phiblocks, allblocks = blocks[fidx] phinodes = IdDict{Int, SSAValue}() for b in phiblocks - n = PhiNode() - phinodes[b] = insert_node!(ir, first(ir.cfg.blocks[b].stmts), ftyp, n) + phinodes[b] = insert_node!(ir, first(ir.cfg.blocks[b].stmts), + NewInstruction(PhiNode(), ftyp)) end # Now go through all uses and rewrite them - allblocks = sort(vcat(phiblocks, ldu.def_bbs)) - for stmt in du.uses - ir[SSAValue(stmt)] = compute_value_for_use(ir, domtree, allblocks, du, phinodes, fidx, stmt) - end - if !isbitstype(fieldtype(typ, fidx)) - for (use, list) in preserve_uses - push!(list, compute_value_for_use(ir, domtree, allblocks, du, phinodes, fidx, use)) + for use in du.uses + if use.kind === :getfield + ir[SSAValue(use.idx)][:inst] = compute_value_for_use(ir, domtree, allblocks, + du, phinodes, fidx, use.idx) + elseif use.kind === :isdefined + continue # already rewritten if possible + elseif use.kind === :nopreserve + continue # nothing to preserve (may happen when there are unintialized fields) + elseif use.kind === :preserve + newval = compute_value_for_use(ir, domtree, allblocks, + du, phinodes, fidx, use.idx) + if !isbitstype(widenconst(argextype(newval, ir))) + if preserve_uses === nothing + preserve_uses = IdDict{Int, Vector{Any}}() + end + push!(get!(()->Any[], preserve_uses, use.idx), newval) + end + else + @assert false "sroa_mutables!: unexpected use" end end for b in phiblocks + n = ir[phinodes[b]][:inst]::PhiNode for p in ir.cfg.blocks[b].preds - n = ir[phinodes[b]] push!(n.edges, p) push!(n.values, compute_value_for_block(ir, domtree, allblocks, du, phinodes, fidx, p)) end end end + all_eliminated || continue + # all "usages" (i.e. `getfield` and `isdefined` calls) are eliminated, + # now eliminate "definitions" (`setfield!`) calls + # (NOTE the allocation itself will be eliminated by DCE pass later) for stmt in du.defs - stmt == idx && continue - ir[SSAValue(stmt)] = nothing + stmt == newidx && continue + ir[SSAValue(stmt)][:inst] = nothing end - continue end - isempty(defuse.ccall_preserve_uses) && continue - push!(intermediaries, idx) + preserve_uses === nothing && continue + if all_forwarded + # this means all ccall preserves have been replaced with forwarded loads + # so we can potentially eliminate the allocation, otherwise we must preserve + # the whole allocation. + push!(intermediaries, newidx) + end # Insert the new preserves - for (use, new_preserves) in preserve_uses - useexpr = ir[SSAValue(use)] - nccallargs = length(useexpr.args[3]::SimpleVector) - old_preserves = let intermediaries = intermediaries - filter(ssa->!isa(ssa, SSAValue) || !(ssa.id in intermediaries), useexpr.args[(6+nccallargs):end]) - end - new_expr = Expr(:foreigncall, useexpr.args[1:(6+nccallargs-1)]..., - old_preserves..., new_preserves...) - ir[SSAValue(use)] = new_expr + for (useidx, new_preserves) in preserve_uses + ir[SSAValue(useidx)][:inst] = form_new_preserves(ir[SSAValue(useidx)][:inst]::Expr, + intermediaries, new_preserves) end + + @label skip end - ir end -function adce_erase!(phi_uses::Vector{Int}, extra_worklist::Vector{Int}, compact::IncrementalCompact, idx::Int) +function form_new_preserves(origex::Expr, intermediates::Vector{Int}, new_preserves::Vector{Any}) + newex = Expr(:foreigncall) + nccallargs = length(origex.args[3]::SimpleVector) + for i in 1:(6+nccallargs-1) + push!(newex.args, origex.args[i]) + end + for i in (6+nccallargs):length(origex.args) + x = origex.args[i] + # don't need to preserve intermediaries + if isa(x, SSAValue) && x.id in intermediates + continue + end + push!(newex.args, x) + end + for i in 1:length(new_preserves) + push!(newex.args, new_preserves[i]) + end + return newex +end + +""" + canonicalize_typeassert!(compact::IncrementalCompact, idx::Int, stmt::Expr) + +Canonicalizes `X = typeassert(Y, T)::S` into `typeassert(Y, T); X = π(Y, S)` +so that subsequent analysis only has to deal with the latter form. + +N.B. Inference may have a more precise type for `S`, than just `T`, but from here on out, +there's no problem with just using that. +We should probably have a version of `typeassert` that's defined not to return its value to +make life easier for the backend. +""" +function canonicalize_typeassert!(compact::IncrementalCompact, idx::Int, stmt::Expr) + length(stmt.args) == 3 || return + pi = insert_node_here!(compact, + NewInstruction( + PiNode(stmt.args[2], compact.result[idx][:type]), + compact.result[idx][:type], + compact.result[idx][:line]), true) + compact.ssa_rename[compact.idx-1] = pi +end + +function adce_erase!(phi_uses::Vector{Int}, extra_worklist::Vector{Int}, compact::IncrementalCompact, idx::Int, in_worklist::Bool) # return whether this made a change if isa(compact.result[idx][:inst], PhiNode) - return maybe_erase_unused!(extra_worklist, compact, idx, val -> phi_uses[val.id] -= 1) + return maybe_erase_unused!(extra_worklist, compact, idx, in_worklist, val::SSAValue -> phi_uses[val.id] -= 1) else - return maybe_erase_unused!(extra_worklist, compact, idx) + return maybe_erase_unused!(extra_worklist, compact, idx, in_worklist) end end @@ -863,7 +1160,7 @@ function count_uses(@nospecialize(stmt), uses::Vector{Int}) end end -function mark_phi_cycles(compact::IncrementalCompact, safe_phis::BitSet, phi::Int) +function mark_phi_cycles!(compact::IncrementalCompact, safe_phis::SPCSet, phi::Int) worklist = Int[] push!(worklist, phi) while !isempty(worklist) @@ -879,38 +1176,145 @@ function mark_phi_cycles(compact::IncrementalCompact, safe_phis::BitSet, phi::In end end +function is_some_union(@nospecialize(t)) + isa(t, MaybeUndef) && (t = t.typ) + return isa(t, Union) +end + +function is_union_phi(compact::IncrementalCompact, idx::Int) + inst = compact.result[idx] + return isa(inst[:inst], PhiNode) && is_some_union(inst[:type]) +end + +""" + adce_pass!(ir::IRCode) -> newir::IRCode + +Aggressive Dead Code Elimination pass. + +In addition to a simple DCE for unused values and allocations, +this pass also nullifies `typeassert` calls that can be proved to be no-op, +in order to allow LLVM to emit simpler code down the road. + +Note that this pass is more effective after SROA optimization (i.e. `sroa_pass!`), +since SROA often allows this pass to: +- eliminate allocation of object whose field references are all replaced with scalar values, and +- nullify `typeassert` call whose first operand has been replaced with a scalar value + (, which may have introduced new type information that inference did not understand) + +Also note that currently this pass _needs_ to run after `sroa_pass!`, because +the `typeassert` elimination depends on the transformation by `canonicalize_typeassert!` done +within `sroa_pass!` which redirects references of `typeassert`ed value to the corresponding `PiNode`. +""" function adce_pass!(ir::IRCode) phi_uses = fill(0, length(ir.stmts) + length(ir.new_nodes)) all_phis = Int[] + unionphis = Pair{Int,Any}[] # sorted compact = IncrementalCompact(ir) for ((_, idx), stmt) in compact if isa(stmt, PhiNode) push!(all_phis, idx) + if is_some_union(compact.result[idx][:type]) + push!(unionphis, Pair{Int,Any}(idx, Union{})) + end + elseif isa(stmt, PiNode) + val = stmt.val + if isa(val, SSAValue) && is_union_phi(compact, val.id) + r = searchsorted(unionphis, val.id; by = first) + if !isempty(r) + unionphi = unionphis[first(r)] + t = tmerge(unionphi[2], stmt.typ) + unionphis[first(r)] = Pair{Int,Any}(unionphi[1], t) + end + end + else + if is_known_call(stmt, typeassert, compact) && length(stmt.args) == 3 + # nullify safe `typeassert` calls + ty, isexact = instanceof_tfunc(argextype(stmt.args[3], compact)) + if isexact && argextype(stmt.args[2], compact) ⊑ ty + compact[idx] = nothing + continue + end + end + for ur in userefs(stmt) + use = ur[] + if isa(use, SSAValue) && is_union_phi(compact, use.id) + r = searchsorted(unionphis, use.id; by = first) + if !isempty(r) + deleteat!(unionphis, first(r)) + end + end + end end end non_dce_finish!(compact) for phi in all_phis - count_uses(compact.result[phi][:inst]::PhiNode, phi_uses) + inst = compact.result[phi] + for ur in userefs(inst[:inst]::PhiNode) + use = ur[] + if isa(use, SSAValue) + phi_uses[use.id] += 1 + stmt = compact.result[use.id][:inst] + if isa(stmt, PhiNode) + r = searchsorted(unionphis, use.id; by=first) + if !isempty(r) + unionphi = unionphis[first(r)] + unionphis[first(r)] = Pair{Int,Any}(unionphi[1], + tmerge(unionphi[2], inst[:type])) + end + end + end + end + end + # Narrow any union phi nodes that have unused branches + for i = 1:length(unionphis) + unionphi = unionphis[i] + phi = unionphi[1] + t = unionphi[2] + if t === Union{} + compact.result[phi][:inst] = nothing + continue + elseif t === Any + continue + elseif compact.result[phi][:type] ⊑ t + continue + end + to_drop = Int[] + stmt = compact[phi] + stmt === nothing && continue + stmt = stmt::PhiNode + for i = 1:length(stmt.values) + if !isassigned(stmt.values, i) + # Should be impossible to have something used only by PiNodes that's undef + push!(to_drop, i) + elseif !hasintersect(widenconst(argextype(stmt.values[i], compact)), + widenconst(t)) + push!(to_drop, i) + end + end + compact.result[phi][:type] = t + isempty(to_drop) && continue + deleteat!(stmt.values, to_drop) + deleteat!(stmt.edges, to_drop) end # Perform simple DCE for unused values extra_worklist = Int[] for (idx, nused) in Iterators.enumerate(compact.used_ssas) idx >= compact.result_idx && break nused == 0 || continue - adce_erase!(phi_uses, extra_worklist, compact, idx) + adce_erase!(phi_uses, extra_worklist, compact, idx, false) end while !isempty(extra_worklist) - adce_erase!(phi_uses, extra_worklist, compact, pop!(extra_worklist)) + adce_erase!(phi_uses, extra_worklist, compact, pop!(extra_worklist), true) end # Go back and erase any phi cycles changed = true while changed changed = false - safe_phis = BitSet() + safe_phis = SPCSet() for phi in all_phis # Save any phi cycles that have non-phi uses if compact.used_ssas[phi] - phi_uses[phi] != 0 - mark_phi_cycles(compact, safe_phis, phi) + mark_phi_cycles!(compact, safe_phis, phi) end end for phi in all_phis @@ -919,7 +1323,7 @@ function adce_pass!(ir::IRCode) end end while !isempty(extra_worklist) - if adce_erase!(phi_uses, extra_worklist, compact, pop!(extra_worklist)) + if adce_erase!(phi_uses, extra_worklist, compact, pop!(extra_worklist), true) changed = true end end @@ -928,24 +1332,36 @@ function adce_pass!(ir::IRCode) end function type_lift_pass!(ir::IRCode) - type_ctx_uses = Vector{Vector{Int}}[] - has_non_type_ctx_uses = IdSet{Int}() lifted_undef = IdDict{Int, Any}() insts = ir.stmts for idx in 1:length(insts) stmt = insts[idx][:inst] stmt isa Expr || continue if (stmt.head === :isdefined || stmt.head === :undefcheck) - val = (stmt.head === :isdefined) ? stmt.args[1] : stmt.args[2] - # undef can only show up by being introduced in a phi - # node (or an UpsilonNode() argument to a PhiC node), - # so lift all these nodes that have maybe undef values + # after optimization, undef can only show up by being introduced in + # a phi node (or an UpsilonNode() argument to a PhiC node), so lift + # all these nodes that have maybe undef values + val = stmt.args[(stmt.head === :isdefined) ? 1 : 2] + if stmt.head === :isdefined && (val isa Slot || val isa GlobalRef || + isexpr(val, :static_parameter) || val isa Argument || val isa Symbol) + # this is a legal node, so assume it was not introduced by + # slot2ssa (at worst, we might leave in a runtime check that + # shouldn't have been there) + continue + end + # otherwise, we definitely have a corrupt node from slot2ssa, and + # must fix or delete that now processed = IdDict{Int, Union{SSAValue, Bool}}() - while isa(val, SSAValue) && isa(insts[val.id][:inst], PiNode) - val = (insts[val.id][:inst]::PiNode).val + def = val + while true + # peek through PiNodes + isa(val, SSAValue) || break + def = insts[val.id][:inst] + isa(def, PiNode) || break + val = def.val end - if !isa(val, SSAValue) || (!isa(insts[val.id][:inst], PhiNode) && !isa(insts[val.id][:inst], PhiCNode)) - (isa(val, GlobalRef) || isexpr(val, :static_parameter)) && continue + if !isa(val, SSAValue) || (!isa(def, PhiNode) && !isa(def, PhiCNode)) + # in most cases, reaching this statement implies we had a value if stmt.head === :undefcheck insts[idx][:inst] = nothing else @@ -955,7 +1371,6 @@ function type_lift_pass!(ir::IRCode) end stmt_id = val.id worklist = Tuple{Int, Int, SSAValue, Int}[(stmt_id, 0, SSAValue(0), 0)] - def = insts[stmt_id][:inst] if !haskey(lifted_undef, stmt_id) first = true while !isempty(worklist) @@ -964,10 +1379,19 @@ function type_lift_pass!(ir::IRCode) if isa(def, PhiNode) edges = copy(def.edges) values = Vector{Any}(undef, length(edges)) - new_phi = length(values) == 0 ? false : insert_node!(ir, item, Bool, PhiNode(edges, values)) + new_phi = if length(values) == 0 + false + else + insert_node!(ir, item, NewInstruction(PhiNode(edges, values), Bool)) + end else + def = def::PhiCNode values = Vector{Any}(undef, length(def.values)) - new_phi = length(values) == 0 ? false : insert_node!(ir, item, Bool, PhiCNode(values)) + new_phi = if length(values) == 0 + false + else + insert_node!(ir, item, NewInstruction(PhiCNode(values), Bool)) + end end processed[item] = new_phi if first @@ -981,7 +1405,7 @@ function type_lift_pass!(ir::IRCode) elseif !isa(def.values[i], SSAValue) val = true else - up_id = id = def.values[i].id + up_id = id = (def.values[i]::SSAValue).id @label restart if !isa(ir.stmts[id][:type], MaybeUndef) val = true @@ -993,7 +1417,7 @@ function type_lift_pass!(ir::IRCode) elseif !isa(node.val, SSAValue) val = true else - id = node.val.id + id = (node.val::SSAValue).id @goto restart end else @@ -1005,7 +1429,7 @@ function type_lift_pass!(ir::IRCode) if haskey(processed, id) val = processed[id] else - push!(worklist, (id, up_id, new_phi, i)) + push!(worklist, (id, up_id, new_phi::SSAValue, i)) continue end else @@ -1017,34 +1441,69 @@ function type_lift_pass!(ir::IRCode) if isa(def, PhiNode) values[i] = val else - values[i] = insert_node!(ir, up_id, Bool, UpsilonNode(val)) + values[i] = insert_node!(ir, up_id, NewInstruction(UpsilonNode(val), Bool)) end end if which !== SSAValue(0) - phi = ir[which] + phi = ir[which][:inst] if isa(phi, PhiNode) phi.values[use] = new_phi else phi = phi::PhiCNode - phi.values[use] = insert_node!(ir, w_up_id, Bool, UpsilonNode(new_phi)) + phi.values[use] = insert_node!(ir, w_up_id, NewInstruction(UpsilonNode(new_phi), Bool)) end end end end - if stmt.head === :isdefined - insts[idx][:inst] = lifted_undef[stmt_id] - else - insts[idx][:inst] = Expr(:throw_undef_if_not, stmt.args[1], lifted_undef[stmt_id]) + inst = lifted_undef[stmt_id] + if stmt.head === :undefcheck + inst = Expr(:throw_undef_if_not, stmt.args[1], inst) end + insts[idx][:inst] = inst end end ir end +function is_bb_empty(ir::IRCode, bb::BasicBlock) + isempty(bb.stmts) && return true + if length(bb.stmts) == 1 + stmt = ir[SSAValue(first(bb.stmts))][:inst] + return stmt === nothing || isa(stmt, GotoNode) + end + return false +end + +# TODO: This is terrible, we should change the IR for GotoIfNot to gain an else case +function is_legal_bb_drop(ir::IRCode, bbidx::Int, bb::BasicBlock) + # If the block we're going to is the same as the fallthrow, it's always legal to drop + # the block. + length(bb.stmts) == 0 && return true + if length(bb.stmts) == 1 + stmt = ir[SSAValue(first(bb.stmts))][:inst] + stmt === nothing && return true + ((stmt::GotoNode).label == bbidx + 1) && return true + end + # Otherwise make sure we're not the fallthrough case of any predecessor + for pred in bb.preds + if pred == bbidx - 1 + terminator = ir[SSAValue(first(bb.stmts)-1)][:inst] + if isa(terminator, GotoIfNot) + if terminator.dest != bbidx + return false + end + end + break + end + end + return true +end + function cfg_simplify!(ir::IRCode) bbs = ir.cfg.blocks merge_into = zeros(Int, length(bbs)) merged_succ = zeros(Int, length(bbs)) + dropped_bbs = Vector{Int}() # sorted function follow_merge_into(idx::Int) while merge_into[idx] != 0 idx = merge_into[idx] @@ -1069,6 +1528,27 @@ function cfg_simplify!(ir::IRCode) merge_into[succ] = idx merged_succ[idx] = succ end + elseif is_bb_empty(ir, bb) && is_legal_bb_drop(ir, idx, bb) + # If this BB is empty, we can still merge it as long as none of our successor's phi nodes + # reference our predecessors. + found_interference = false + for idx in bbs[succ].stmts + stmt = ir[SSAValue(idx)][:inst] + stmt === nothing && continue + isa(stmt, PhiNode) || break + for edge in stmt.edges + for pred in bb.preds + if pred == edge + found_interference = true + @goto done + end + end + end + end + @label done + if !found_interference + push!(dropped_bbs, idx) + end end end end @@ -1085,6 +1565,10 @@ function cfg_simplify!(ir::IRCode) if i != 1 && length(ir.cfg.blocks[i].preds) == 0 bb_rename_succ[i] = -1 end + # Mark dropped blocks for fixup + if !isempty(searchsorted(dropped_bbs, i)) + bb_rename_succ[i] = -bbs[i].succs[1] + end bb_rename_succ[i] != 0 && continue @@ -1101,6 +1585,30 @@ function cfg_simplify!(ir::IRCode) break end curr += 1 + if !isempty(searchsorted(dropped_bbs, curr)) + break + end + end + end + + # Compute map from new to old blocks + result_bbs = Int[findfirst(j->i==j, bb_rename_succ) for i = 1:max_bb_num-1] + + # Fixup dropped BBs + resolved_all = false + while !resolved_all + # TODO: There are faster ways to do this + resolved_all = true + for bb in dropped_bbs + obb = bb_rename_succ[bb] + if obb < -1 + nsucc = bb_rename_succ[-obb] + if nsucc == -1 + nsucc = -merge_into[-obb] + end + bb_rename_succ[bb] = nsucc + resolved_all = false + end end end @@ -1113,13 +1621,14 @@ function cfg_simplify!(ir::IRCode) bb_rename_pred[i] = -1 continue end - bbnum = follow_merge_into(i) + pred = i + while pred !== 1 && !isempty(searchsorted(dropped_bbs, pred)) + pred = bbs[pred].preds[1] + end + bbnum = follow_merge_into(pred) bb_rename_pred[i] = bb_rename_succ[bbnum] end - # Compute map from new to old blocks - result_bbs = Int[findfirst(j->i==j, bb_rename_succ) for i = 1:max_bb_num-1] - # Compute new block lengths result_bbs_lengths = zeros(Int, max_bb_num-1) for (idx, orig_bb) in enumerate(result_bbs) @@ -1146,12 +1655,12 @@ function cfg_simplify!(ir::IRCode) # Compute (renamed) successors and predecessors given (renamed) block function compute_succs(i) orig_bb = follow_merged_succ(result_bbs[i]) - return map(i -> bb_rename_succ[i], bbs[orig_bb].succs) + return Int[bb_rename_succ[i] for i in bbs[orig_bb].succs] end function compute_preds(i) orig_bb = result_bbs[i] preds = bbs[orig_bb].preds - return map(pred -> bb_rename_pred[pred], preds) + return Int[bb_rename_pred[pred] for pred in preds] end BasicBlock[ @@ -1163,6 +1672,25 @@ function cfg_simplify!(ir::IRCode) for i = 1:length(result_bbs)] end + # Fixup terminators for any blocks that would have caused double edges + for (bbidx, (new_bb, old_bb)) in enumerate(zip(cresult_bbs, result_bbs)) + @assert length(new_bb.succs) <= 2 + length(new_bb.succs) <= 1 && continue + if new_bb.succs[1] == new_bb.succs[2] + terminator = ir[SSAValue(last(bbs[old_bb].stmts))] + @assert isa(terminator[:inst], GotoIfNot) + terminator[:inst] = GotoNode(terminator[:inst].dest) + pop!(new_bb.succs) + new_succ = cresult_bbs[new_bb.succs[1]] + for (i, nsp) in enumerate(new_succ.preds) + if nsp == bbidx + deleteat!(new_succ.preds, i) + break + end + end + end + end + compact = IncrementalCompact(ir, true) # Run instruction compaction to produce the result, # but we're messing with the CFG diff --git a/base/compiler/ssair/queries.jl b/base/compiler/ssair/queries.jl deleted file mode 100644 index 6a6ac89c91e7c6..00000000000000 --- a/base/compiler/ssair/queries.jl +++ /dev/null @@ -1,87 +0,0 @@ -# This file is a part of Julia. License is MIT: https://julialang.org/license - -""" -Determine whether a statement is side-effect-free, i.e. may be removed if it has no uses. -""" -function stmt_effect_free(@nospecialize(stmt), @nospecialize(rt), src, sptypes::Vector{Any}) - isa(stmt, PiNode) && return true - isa(stmt, PhiNode) && return true - isa(stmt, ReturnNode) && return false - isa(stmt, GotoNode) && return false - isa(stmt, GotoIfNot) && return false - isa(stmt, Slot) && return false # Slots shouldn't occur in the IR at this point, but let's be defensive here - isa(stmt, GlobalRef) && return isdefined(stmt.mod, stmt.name) - if isa(stmt, Expr) - e = stmt::Expr - head = e.head - if head === :static_parameter - etyp = sptypes[e.args[1]] - # if we aren't certain enough about the type, it might be an UndefVarError at runtime - return isa(etyp, Const) - end - ea = e.args - if head === :call - f = argextype(ea[1], src, sptypes) - f = singleton_type(f) - f === nothing && return false - is_return_type(f) && return true - if isa(f, IntrinsicFunction) - intrinsic_effect_free_if_nothrow(f) || return false - return intrinsic_nothrow(f, - Any[argextype(ea[i], src, sptypes) for i = 2:length(ea)]) - end - contains_is(_PURE_BUILTINS, f) && return true - contains_is(_PURE_OR_ERROR_BUILTINS, f) || return false - rt === Bottom && return false - return _builtin_nothrow(f, Any[argextype(ea[i], src, sptypes) for i = 2:length(ea)], rt) - elseif head === :new - a = ea[1] - typ = argextype(a, src, sptypes) - # `Expr(:new)` of unknown type could raise arbitrary TypeError. - typ, isexact = instanceof_tfunc(typ) - isexact || return false - isconcretedispatch(typ) || return false - typ = typ::DataType - fieldcount(typ) >= length(ea) - 1 || return false - for fld_idx in 1:(length(ea) - 1) - eT = argextype(ea[fld_idx + 1], src, sptypes) - fT = fieldtype(typ, fld_idx) - eT ⊑ fT || return false - end - return true - elseif head === :isdefined || head === :the_exception || head === :copyast || head === :inbounds || head === :boundscheck - return true - else - # e.g. :loopinfo - return false - end - end - return true -end - -function abstract_eval_ssavalue(s::SSAValue, src::IRCode) - return types(src)[s] -end - -function abstract_eval_ssavalue(s::SSAValue, src::IncrementalCompact) - return types(src)[s] -end - -function compact_exprtype(compact::IncrementalCompact, @nospecialize(value)) - if isa(value, AnySSAValue) - return types(compact)[value] - elseif isa(value, Argument) - return compact.ir.argtypes[value.n] - end - return argextype(value, compact.ir, compact.ir.sptypes) -end - -is_tuple_call(ir::IRCode, @nospecialize(def)) = isa(def, Expr) && is_known_call(def, tuple, ir, ir.sptypes) -is_tuple_call(compact::IncrementalCompact, @nospecialize(def)) = isa(def, Expr) && is_known_call(def, tuple, compact) -function is_known_call(e::Expr, @nospecialize(func), src::IncrementalCompact) - if e.head !== :call - return false - end - f = compact_exprtype(src, e.args[1]) - return singleton_type(f) === func -end diff --git a/base/compiler/ssair/show.jl b/base/compiler/ssair/show.jl index 861c75c7d888b6..f4c826a45156fd 100644 --- a/base/compiler/ssair/show.jl +++ b/base/compiler/ssair/show.jl @@ -47,7 +47,7 @@ function print_stmt(io::IO, idx::Int, @nospecialize(stmt), used::BitSet, maxleng # XXX: this is wrong if `sig` is not a concretetype method # more correct would be to use `fieldtype(sig, i)`, but that would obscure / discard Varargs information in show sig = linfo.specTypes == Tuple ? Core.svec() : Base.unwrap_unionall(linfo.specTypes).parameters::Core.SimpleVector - print_arg(i) = sprint() do io + print_arg(i) = sprint(; context=io) do io show_unquoted(io, stmt.args[i], indent) if (i - 1) <= length(sig) print(io, "::", sig[i - 1]) @@ -64,6 +64,10 @@ function print_stmt(io::IO, idx::Int, @nospecialize(stmt), used::BitSet, maxleng show_unquoted_phinode(io, stmt, indent, "#") elseif stmt isa GotoIfNot show_unquoted_gotoifnot(io, stmt, indent, "#") + elseif stmt isa TypedSlot + # call `show` with the type set to Any so it will not be shown, since + # we will show the type ourselves. + show_unquoted(io, SlotNumber(stmt.id), indent, show_type ? prec_decl : 0) # everything else in the IR, defer to the generic AST printer else show_unquoted(io, stmt, indent, show_type ? prec_decl : 0) @@ -75,14 +79,15 @@ show_unquoted(io::IO, val::Argument, indent::Int, prec::Int) = show_unquoted(io, show_unquoted(io::IO, stmt::PhiNode, indent::Int, ::Int) = show_unquoted_phinode(io, stmt, indent, "%") function show_unquoted_phinode(io::IO, stmt::PhiNode, indent::Int, prefix::String) - args = map(1:length(stmt.edges)) do i + args = String[let e = stmt.edges[i] v = !isassigned(stmt.values, i) ? "#undef" : - sprint() do io′ + sprint(; context=io) do io′ show_unquoted(io′, stmt.values[i], indent) end - return "$prefix$e => $v" - end + "$prefix$e => $v" + end for i in 1:length(stmt.edges) + ] print(io, "φ ", '(') join(io, args, ", ") print(io, ')') @@ -188,7 +193,7 @@ example (taken from `@code_typed sin(1.0)`): ``` The three annotations are indicated with `*`. The first one is the line number of the -active function (printed once whenver the outer most line number changes). The second +active function (printed once whenever the outer most line number changes). The second is the inlining indicator. The number of lines indicate the level of nesting, with a half-size line (╷) indicating the start of a scope and a full size line (│) indicating a continuing scope. The last annotation is the most complicated one. It is a heuristic @@ -197,7 +202,7 @@ scope that hasn't been printed before. Let's work a number of examples to see th and tradeoffs involved. ``` -f() = leaf_function() # Delibarately not defined to end up in the IR verbatim +f() = leaf_function() # Deliberately not defined to end up in the IR verbatim g() = f() h() = g() top_function() = h() @@ -371,20 +376,40 @@ function DILineInfoPrinter(linetable::Vector, showtypes::Bool=false) nctx = i end update_line_only::Bool = false - if collapse && 0 < nctx - # check if we're adding more frames with the same method name, - # if so, drop all existing calls to it from the top of the context - # AND check if instead the context was previously printed that way - # but now has removed the recursive frames - let method = method_name(context[nctx]) - if (nctx < nframes && method_name(DI[nframes - nctx]) === method) || - (nctx < length(context) && method_name(context[nctx + 1]) === method) - update_line_only = true - while nctx > 0 && method_name(context[nctx]) === method - nctx -= 1 + if collapse + if nctx > 0 + # check if we're adding more frames with the same method name, + # if so, drop all existing calls to it from the top of the context + # AND check if instead the context was previously printed that way + # but now has removed the recursive frames + let method = method_name(context[nctx]) # last matching frame + if (nctx < nframes && method_name(DI[nframes - nctx]) === method) || + (nctx < length(context) && method_name(context[nctx + 1]) === method) + update_line_only = true + while nctx > 0 && method_name(context[nctx]) === method + nctx -= 1 + end + end + end + end + # look at the first non-matching element to see if we are only changing the line number + if !update_line_only && nctx < length(context) && nctx < nframes + let CtxLine = context[nctx + 1], + FrameLine = DI[nframes - nctx] + if method_name(CtxLine) === method_name(FrameLine) + update_line_only = true end end end + elseif nctx < length(context) && nctx < nframes + # look at the first non-matching element to see if we are only changing the line number + let CtxLine = context[nctx + 1], + FrameLine = DI[nframes - nctx] + if CtxLine.file === FrameLine.file && + method_name(CtxLine) === method_name(FrameLine) + update_line_only = true + end + end end # examine what frames we're returning from if nctx < length(context) @@ -400,16 +425,6 @@ function DILineInfoPrinter(linetable::Vector, showtypes::Bool=false) end else npops = length(context) - nctx - # look at the first non-matching element to see if we are only changing the line number - if !update_line_only && nctx < nframes - let CtxLine = context[nctx + 1], - FrameLine = DI[nframes - nctx] - if CtxLine.file === FrameLine.file && - method_name(CtxLine) === method_name(FrameLine) - update_line_only = true - end - end - end end resize!(context, nctx) update_line_only && (npops -= 1) @@ -448,7 +463,7 @@ function DILineInfoPrinter(linetable::Vector, showtypes::Bool=false) if frame.line != typemax(frame.line) && frame.line != 0 print(io, ":", frame.line) end - print(io, " within `", method_name(frame), "'") + print(io, " within `", method_name(frame), "`") if collapse method = method_name(frame) while nctx < nframes @@ -480,31 +495,81 @@ function DILineInfoPrinter(linetable::Vector, showtypes::Bool=false) return emit_lineinfo_update end +# line_info_preprinter(io::IO, indent::String, idx::Int) may print relevant info +# at the beginning of the line, and should at least print `indent`. It returns a +# string that will be printed after the final basic-block annotation. +# line_info_postprinter(io::IO, typ, used::Bool) prints the type-annotation at the end +# of the statement +# should_print_stmt(idx::Int) -> Bool: whether the statement at index `idx` should be +# printed as part of the IR or not +# bb_color: color used for printing the basic block brackets on the left +struct IRShowConfig + line_info_preprinter + line_info_postprinter + should_print_stmt + bb_color::Symbol + function IRShowConfig(line_info_preprinter, line_info_postprinter=default_expr_type_printer; + should_print_stmt=Returns(true), bb_color::Symbol=:light_black) + return new(line_info_preprinter, line_info_postprinter, should_print_stmt, bb_color) + end +end -function show_ir(io::IO, code::IRCode, expr_type_printer=default_expr_type_printer; verbose_linetable=false) - cols = (displaysize(io)::Tuple{Int,Int})[2] - used = BitSet() +struct _UNDEF + global const UNDEF = _UNDEF.instance +end + +function _stmt(code::IRCode, idx::Int) stmts = code.stmts - isempty(stmts) && return # unlikely, but avoid errors from reducing over empty sets - cfg = code.cfg - max_bb_idx_size = length(string(length(cfg.blocks))) - new_nodes = code.new_nodes.stmts - new_nodes_info = code.new_nodes.info - bb_idx = 1 - for stmt in stmts - scan_ssa_use!(push!, used, stmt[:inst]) - end - if any(i -> !isassigned(new_nodes.inst, i), 1:length(new_nodes)) - printstyled(io, "ERROR: New node array has unset entry\n", color=:red) - new_nodes_perm = filter(i -> isassigned(new_nodes.inst, i), 1:length(new_nodes)) - else - new_nodes_perm = collect(1:length(new_nodes)) - end - for nn in new_nodes_perm - scan_ssa_use!(push!, used, new_nodes[nn][:inst]) + return isassigned(stmts.inst, idx) ? stmts[idx][:inst] : UNDEF +end +function _stmt(code::CodeInfo, idx::Int) + code = code.code + return isassigned(code, idx) ? code[idx] : UNDEF +end + +function _type(code::IRCode, idx::Int) + stmts = code.stmts + return isassigned(stmts.type, idx) ? stmts[idx][:type] : UNDEF +end +function _type(code::CodeInfo, idx::Int) + types = code.ssavaluetypes + types isa Vector{Any} || return nothing + return isassigned(types, idx) ? types[idx] : UNDEF +end + +function statement_indices_to_labels(stmt, cfg::CFG) + # convert statement index to labels, as expected by print_stmt + if stmt isa Expr + if stmt.head === :enter && length(stmt.args) == 1 && stmt.args[1] isa Int + stmt = Expr(:enter, block_for_inst(cfg, stmt.args[1]::Int)) + end + elseif isa(stmt, GotoIfNot) + stmt = GotoIfNot(stmt.cond, block_for_inst(cfg, stmt.dest)) + elseif stmt isa GotoNode + stmt = GotoNode(block_for_inst(cfg, stmt.label)) + elseif stmt isa PhiNode + e = stmt.edges + stmt = PhiNode(Int32[block_for_inst(cfg, Int(e[i])) for i in 1:length(e)], stmt.values) end - sort!(new_nodes_perm, by = x -> (x = new_nodes_info[x]; (x.pos, x.attach_after))) - perm_idx = 1 + return stmt +end + +# Show a single statement, code.stmts[idx]/code.code[idx], in the context of the whole IRCode/CodeInfo. +# Returns the updated value of bb_idx. +# pop_new_node!(idx::Int) -> (node_idx, new_node_inst, new_node_type) may return a new +# node at the current index `idx`, which is printed before the statement at index +# `idx`. This function is repeatedly called until it returns `nothing` +function show_ir_stmt(io::IO, code::Union{IRCode, CodeInfo}, idx::Int, config::IRShowConfig, + used::BitSet, cfg::CFG, bb_idx::Int; pop_new_node! = Returns(nothing)) + return show_ir_stmt(io, code, idx, config.line_info_preprinter, config.line_info_postprinter, + used, cfg, bb_idx; pop_new_node!, config.bb_color) +end + +function show_ir_stmt(io::IO, code::Union{IRCode, CodeInfo}, idx::Int, line_info_preprinter, line_info_postprinter, + used::BitSet, cfg::CFG, bb_idx::Int; pop_new_node! = Returns(nothing), bb_color=:light_black) + stmt = _stmt(code, idx) + type = _type(code, idx) + max_bb_idx_size = length(string(length(cfg.blocks))) if isempty(used) maxlength_idx = 0 @@ -512,255 +577,233 @@ function show_ir(io::IO, code::IRCode, expr_type_printer=default_expr_type_print maxused = maximum(used) maxlength_idx = length(string(maxused)) end - if !verbose_linetable - (loc_annotations, loc_methods, loc_lineno) = compute_ir_line_annotations(code) - max_loc_width = maximum(length(str) for str in loc_annotations) - max_lineno_width = maximum(length(str) for str in loc_lineno) - max_method_width = maximum(length(str) for str in loc_methods) + + if stmt === UNDEF + # This is invalid, but do something useful rather + # than erroring, to make debugging easier + printstyled(io, "#UNDEF\n", color=:red) + return bb_idx end - max_depth = maximum(compute_inlining_depth(code.linetable, stmts[i][:line]) for i in 1:length(stmts.line)) - last_stack = [] - for idx in 1:length(stmts) - if !isassigned(stmts.inst, idx) - # This is invalid, but do something useful rather - # than erroring, to make debugging easier - printstyled(io, "#UNDEF\n", color=:red) - continue - end - stmt = stmts[idx] + + i = 1 + while true + next = pop_new_node!(idx) # Compute BB guard rail if bb_idx > length(cfg.blocks) - # Even if invariants are violated, try our best to still print - bbrange = (length(cfg.blocks) == 0 ? 1 : last(cfg.blocks[end].stmts) + 1):typemax(Int) - bb_idx_str = "!" - bb_type = "─" + # If invariants are violated, print a special leader + linestart = " "^(max_bb_idx_size + 2) # not inside a basic block bracket + inlining_indent = line_info_preprinter(io, linestart, i == 1 ? idx : 0) + printstyled(io, "!!! ", "─"^max_bb_idx_size, color=bb_color) else bbrange = cfg.blocks[bb_idx].stmts bbrange = bbrange.start:bbrange.stop - bb_idx_str = string(bb_idx) - bb_type = length(cfg.blocks[bb_idx].preds) <= 1 ? "─" : "┄" - end - bb_pad = max_bb_idx_size - length(bb_idx_str) - bb_start_str = string(bb_idx_str, " ", bb_type, "─"^bb_pad, " ") - bb_guard_rail_cont = string("│ ", " "^max_bb_idx_size) - if idx == first(bbrange) - bb_guard_rail = bb_start_str - else - bb_guard_rail = bb_guard_rail_cont - end - # Print linetable information - if verbose_linetable - stack = compute_loc_stack(code.linetable, stmt[:line]) - # We need to print any stack frames that did not exist in the last stack - ndepth = max(1, length(stack)) - rail = string(" "^(max_depth+1-ndepth), "│"^ndepth) - start_column = cols - max_depth - 10 - for (i, x) in enumerate(stack) - if i > length(last_stack) || last_stack[i] != x - entry = code.linetable[x] - printstyled(io, "\e[$(start_column)G$(rail)\e[1G", color = :light_black) - print(io, bb_guard_rail) - ssa_guard = " "^(maxlength_idx + 4 + (i - 1)) - entry_label = "$(ssa_guard)$(method_name(entry)) at $(entry.file):$(entry[:line]) " - hline = string("─"^(start_column-length(entry_label)-length(bb_guard_rail)+max_depth-i), "┐") - printstyled(io, string(entry_label, hline), "\n"; color=:light_black) - bb_guard_rail = bb_guard_rail_cont - end - end - printstyled(io, "\e[$(start_column)G$(rail)\e[1G", color = :light_black) - last_stack = stack - else - if idx <= length(loc_annotations) - # N.B.: The line array length not matching is invalid, - # but let's be robust here - annotation = loc_annotations[idx] - loc_method = loc_methods[idx] - lineno = loc_lineno[idx] + # Print line info update + linestart = idx == first(bbrange) ? " " : sprint(io -> printstyled(io, "│ ", color=bb_color), context=io) + linestart *= " "^max_bb_idx_size + # idx == 0 means only indentation is printed, so we don't print linfos + # multiple times if the are new nodes + inlining_indent = line_info_preprinter(io, linestart, i == 1 ? idx : 0) + + if i == 1 && idx == first(bbrange) + bb_idx_str = string(bb_idx) + bb_pad = max_bb_idx_size - length(bb_idx_str) + bb_type = length(cfg.blocks[bb_idx].preds) <= 1 ? "─" : "┄" + printstyled(io, bb_idx_str, " ", bb_type, "─"^bb_pad, color=bb_color) + elseif next === nothing && idx == last(bbrange) # print separator + printstyled(io, "└", "─"^(1 + max_bb_idx_size), color=bb_color) else - annotation = "!" - loc_method = "" - lineno = "" - end - # Print location information right aligned. If the line below is too long, it'll overwrite this, - # but that's what we want. - if get(io, :color, false) - method_start_column = cols - max_method_width - max_loc_width - 2 - filler = " "^(max_loc_width-length(annotation)) - printstyled(io, "\e[$(method_start_column)G$(annotation)$(filler)$(loc_method)\e[1G", color = :light_black) + printstyled(io, "│ ", " "^max_bb_idx_size, color=bb_color) end - printstyled(io, lineno, " "^(max_lineno_width - length(lineno) + 1); color = :light_black) end - idx != last(bbrange) && print(io, bb_guard_rail) - print_sep = false - if idx == last(bbrange) - print_sep = true + print(io, inlining_indent, " ") + + if next === nothing + if bb_idx <= length(cfg.blocks) && idx == last(bbrange) + bb_idx += 1 + end + break end - floop = true + # print new nodes first in the right position - while perm_idx <= length(new_nodes_perm) - node_idx = new_nodes_perm[perm_idx] - if new_nodes_info[node_idx].pos != idx - break - end - perm_idx += 1 - if !floop && !verbose_linetable - print(io, " "^(max_lineno_width + 1)) - end - if print_sep - if idx == first(bbrange) && floop - print(io, bb_start_str) - else - print(io, "│ ", " "^max_bb_idx_size) - end - end - print_sep = true - floop = false - new_node = new_nodes[node_idx] - node_idx += length(stmts) - show_type = should_print_ssa_type(new_node[:inst]) + node_idx, new_node_inst, new_node_type = next + + @assert new_node_inst !== UNDEF # we filtered these out earlier + show_type = should_print_ssa_type(new_node_inst) + let maxlength_idx=maxlength_idx, show_type=show_type with_output_color(:green, io) do io′ - print_stmt(io′, node_idx, new_node[:inst], used, maxlength_idx, false, show_type) - end - if !isassigned(stmts.type, idx) # try to be robust against errors - printstyled(io, "::#UNDEF", color=:red) - elseif show_type - expr_type_printer(io, new_node[:type], node_idx in used) - end - println(io) - end - if !floop && !verbose_linetable - print(io, " "^(max_lineno_width + 1)) - end - if print_sep - if idx == first(bbrange) && floop - print(io, bb_start_str) - elseif idx == last(bbrange) - print(io, "└", "─"^(1 + max_bb_idx_size), " ") - else - print(io, "│ ", " "^max_bb_idx_size) + print_stmt(io′, node_idx, new_node_inst, used, maxlength_idx, false, show_type) end end - if idx == last(bbrange) - bb_idx += 1 - end - show_type = should_print_ssa_type(stmt[:inst]) - print_stmt(io, idx, stmt[:inst], used, maxlength_idx, true, show_type) - if !isassigned(stmts.type, idx) # try to be robust against errors + + if new_node_type === UNDEF # try to be robust against errors printstyled(io, "::#UNDEF", color=:red) elseif show_type - expr_type_printer(io, stmt[:type], idx in used) + line_info_postprinter(IOContext(io, :idx => node_idx), new_node_type, node_idx in used) end println(io) + i += 1 end -end - -# Show a single statement, code.code[idx], in the context of the whole CodeInfo. -# Returns the updated value of bb_idx. -# line_info_preprinter(io::IO, indent::String, idx::Int) may print relevant info -# at the beginning of the line, and should at least print `indent`. It returns a -# string that will be printed after the final basic-block annotation. -# line_info_postprinter(io::IO, typ, used::Bool) prints the type-annotation at the end -# of the statement -function show_ir_stmt(io::IO, code::CodeInfo, idx::Int, line_info_preprinter, line_info_postprinter, used::BitSet, cfg::CFG, bb_idx::Int) - ds = get(io, :displaysize, (24, 80))::Tuple{Int,Int} - cols = ds[2] - stmts = code.code - types = code.ssavaluetypes - max_bb_idx_size = length(string(length(cfg.blocks))) - - if isempty(used) - maxlength_idx = 0 - else - maxused = maximum(used) - maxlength_idx = length(string(maxused)) - end - - if !isassigned(stmts, idx) - # This is invalid, but do something useful rather - # than erroring, to make debugging easier - printstyled(io, "#UNDEF\n", color=:red) - return bb_idx - end - stmt = stmts[idx] - # Compute BB guard rail - if bb_idx > length(cfg.blocks) - # If invariants are violated, print a special leader - linestart = " "^(max_bb_idx_size + 2) # not inside a basic block bracket - inlining_indent = line_info_preprinter(io, linestart, idx) - printstyled(io, "!!! ", "─"^max_bb_idx_size, color=:light_black) - else - bbrange = cfg.blocks[bb_idx].stmts - bbrange = bbrange.start:bbrange.stop - # Print line info update - linestart = idx == first(bbrange) ? " " : sprint(io -> printstyled(io, "│ ", color=:light_black), context=io) - linestart *= " "^max_bb_idx_size - inlining_indent = line_info_preprinter(io, linestart, idx) - if idx == first(bbrange) - bb_idx_str = string(bb_idx) - bb_pad = max_bb_idx_size - length(bb_idx_str) - bb_type = length(cfg.blocks[bb_idx].preds) <= 1 ? "─" : "┄" - printstyled(io, bb_idx_str, " ", bb_type, "─"^bb_pad, color=:light_black) - elseif idx == last(bbrange) # print separator - printstyled(io, "└", "─"^(1 + max_bb_idx_size), color=:light_black) - else - printstyled(io, "│ ", " "^max_bb_idx_size, color=:light_black) - end - if idx == last(bbrange) - bb_idx += 1 - end - end - print(io, inlining_indent, " ") - # convert statement index to labels, as expected by print_stmt - if stmt isa Expr - if stmt.head === :enter && length(stmt.args) == 1 && stmt.args[1] isa Int - stmt = Expr(:enter, block_for_inst(cfg, stmt.args[1]::Int)) - end - elseif isa(stmt, GotoIfNot) - stmt = GotoIfNot(stmt.cond, block_for_inst(cfg, stmt.dest)) - elseif stmt isa GotoNode - stmt = GotoNode(block_for_inst(cfg, stmt.label)) - elseif stmt isa PhiNode - e = stmt.edges - stmt = PhiNode(Int32[block_for_inst(cfg, Int(e[i])) for i in 1:length(e)], stmt.values) + if code isa CodeInfo + stmt = statement_indices_to_labels(stmt, cfg) end - show_type = types isa Vector{Any} && should_print_ssa_type(stmt) + show_type = type !== nothing && should_print_ssa_type(stmt) print_stmt(io, idx, stmt, used, maxlength_idx, true, show_type) - if types isa Vector{Any} # ignore types for pre-inference code - if !isassigned(types, idx) + if type !== nothing # ignore types for pre-inference code + if type === UNDEF # This is an error, but can happen if passes don't update their type information printstyled(io, "::#UNDEF", color=:red) elseif show_type - typ = types[idx] - line_info_postprinter(io, typ, idx in used) + line_info_postprinter(IOContext(io, :idx => idx), type, idx in used) end end println(io) return bb_idx end +function ircode_new_nodes_iter(code::IRCode) + stmts = code.stmts + new_nodes = code.new_nodes.stmts + new_nodes_info = code.new_nodes.info + new_nodes_perm = filter(i -> isassigned(new_nodes.inst, i), 1:length(new_nodes)) + sort!(new_nodes_perm, by = x -> (x = new_nodes_info[x]; (x.pos, x.attach_after))) + perm_idx = Ref(1) + + function (idx::Int) + perm_idx[] <= length(new_nodes_perm) || return nothing + node_idx = new_nodes_perm[perm_idx[]] + if new_nodes_info[node_idx].pos != idx + return nothing + end + perm_idx[] += 1 + new_node = new_nodes[node_idx] + new_node_inst = isassigned(new_nodes.inst, node_idx) ? new_node[:inst] : UNDEF + new_node_type = isassigned(new_nodes.type, node_idx) ? new_node[:type] : UNDEF + node_idx += length(stmts) + return node_idx, new_node_inst, new_node_type + end +end + +# print only line numbers on the left, some of the method names and nesting depth on the right +function inline_linfo_printer(code::IRCode) + loc_annotations, loc_methods, loc_lineno = compute_ir_line_annotations(code) + max_loc_width = maximum(length, loc_annotations) + max_lineno_width = maximum(length, loc_lineno) + max_method_width = maximum(length, loc_methods) + + function (io::IO, indent::String, idx::Int) + cols = (displaysize(io)::Tuple{Int,Int})[2] + + if idx == 0 + annotation = "" + loc_method = "" + lineno = "" + elseif idx <= length(loc_annotations) + # N.B.: The line array length not matching is invalid, + # but let's be robust here + annotation = loc_annotations[idx] + loc_method = loc_methods[idx] + lineno = loc_lineno[idx] + else + annotation = "!" + loc_method = "" + lineno = "" + end + # Print location information right aligned. If the line below is too long, it'll overwrite this, + # but that's what we want. + if get(io, :color, false) + method_start_column = cols - max_method_width - max_loc_width - 2 + filler = " "^(max_loc_width-length(annotation)) + printstyled(io, "\e[$(method_start_column)G$(annotation)$(filler)$(loc_method)\e[1G", color = :light_black) + end + printstyled(io, lineno, " "^(max_lineno_width - length(lineno) + 1); color = :light_black) + return "" + end +end + +_strip_color(s::String) = replace(s, r"\e\[\d+m" => "") + +function statementidx_lineinfo_printer(f, code::IRCode) + printer = f(code.linetable) + function (io::IO, indent::String, idx::Int) + printer(io, indent, idx > 0 ? code.stmts[idx][:line] : typemin(Int32)) + end +end function statementidx_lineinfo_printer(f, code::CodeInfo) printer = f(code.linetable) - return (io::IO, indent::String, idx::Int) -> printer(io, indent, idx > 0 ? code.codelocs[idx] : typemin(Int32)) + function (io::IO, indent::String, idx::Int) + printer(io, indent, idx > 0 ? code.codelocs[idx] : typemin(Int32)) + end end -statementidx_lineinfo_printer(code::CodeInfo) = statementidx_lineinfo_printer(DILineInfoPrinter, code) +statementidx_lineinfo_printer(code) = statementidx_lineinfo_printer(DILineInfoPrinter, code) -function show_ir(io::IO, code::CodeInfo, line_info_preprinter=statementidx_lineinfo_printer(code), line_info_postprinter=default_expr_type_printer) - ioctx = IOContext(io, :displaysize => displaysize(io)::Tuple{Int,Int}) +function stmts_used(io::IO, code::IRCode, warn_unset_entry=true) + stmts = code.stmts + used = BitSet() + for stmt in stmts + scan_ssa_use!(push!, used, stmt[:inst]) + end + new_nodes = code.new_nodes.stmts + for nn in 1:length(new_nodes) + if isassigned(new_nodes.inst, nn) + scan_ssa_use!(push!, used, new_nodes[nn][:inst]) + elseif warn_unset_entry + printstyled(io, "ERROR: New node array has unset entry\n", color=:red) + warn_unset_entry = false + end + end + return used +end + +function stmts_used(::IO, code::CodeInfo) stmts = code.code used = BitSet() - cfg = compute_basic_blocks(stmts) for stmt in stmts scan_ssa_use!(push!, used, stmt) end + return used +end + +function default_config(code::IRCode; verbose_linetable=false) + return IRShowConfig(verbose_linetable ? statementidx_lineinfo_printer(code) + : inline_linfo_printer(code); + bb_color=:normal) +end +default_config(code::CodeInfo) = IRShowConfig(statementidx_lineinfo_printer(code)) + +function show_ir(io::IO, code::Union{IRCode, CodeInfo}, config::IRShowConfig=default_config(code); + pop_new_node! = code isa IRCode ? ircode_new_nodes_iter(code) : Returns(nothing)) + stmts = code isa IRCode ? code.stmts : code.code + used = stmts_used(io, code) + cfg = code isa IRCode ? code.cfg : compute_basic_blocks(stmts) bb_idx = 1 for idx in 1:length(stmts) - bb_idx = show_ir_stmt(ioctx, code, idx, line_info_preprinter, line_info_postprinter, used, cfg, bb_idx) + if config.should_print_stmt(code, idx, used) + bb_idx = show_ir_stmt(io, code, idx, config, used, cfg, bb_idx; pop_new_node!) + elseif bb_idx <= length(cfg.blocks) && idx == cfg.blocks[bb_idx].stmts.stop + bb_idx += 1 + end end max_bb_idx_size = length(string(length(cfg.blocks))) - line_info_preprinter(io, " "^(max_bb_idx_size + 2), 0) + config.line_info_preprinter(io, " "^(max_bb_idx_size + 2), 0) nothing end +tristate_letter(t::TriState) = t === ALWAYS_TRUE ? '+' : t === ALWAYS_FALSE ? '!' : '?' +tristate_color(t::TriState) = t === ALWAYS_TRUE ? :green : t === ALWAYS_FALSE ? :red : :orange + +function Base.show(io::IO, e::Core.Compiler.Effects) + print(io, "(") + printstyled(io, string(tristate_letter(e.consistent), 'c'); color=tristate_color(e.consistent)) + print(io, ',') + printstyled(io, string(tristate_letter(e.effect_free), 'e'); color=tristate_color(e.effect_free)) + print(io, ',') + printstyled(io, string(tristate_letter(e.nothrow), 'n'); color=tristate_color(e.nothrow)) + print(io, ',') + printstyled(io, string(tristate_letter(e.terminates), 't'); color=tristate_color(e.terminates)) + print(io, ')') + e.nonoverlayed || printstyled(io, '′'; color=:red) +end + @specialize diff --git a/base/compiler/ssair/slot2ssa.jl b/base/compiler/ssair/slot2ssa.jl index 057bb72ff11521..a5dd6a0fd8f299 100644 --- a/base/compiler/ssair/slot2ssa.jl +++ b/base/compiler/ssair/slot2ssa.jl @@ -33,21 +33,11 @@ function scan_entry!(result::Vector{SlotInfo}, idx::Int, @nospecialize(stmt)) end -function lift_defuse(cfg::CFG, defuse) - map(defuse) do slot - SlotInfo( - Int[block_for_inst(cfg, x) for x in slot.defs], - Int[block_for_inst(cfg, x) for x in slot.uses], - slot.any_newvar - ) - end -end - function scan_slot_def_use(nargs::Int, ci::CodeInfo, code::Vector{Any}) nslots = length(ci.slotflags) result = SlotInfo[SlotInfo() for i = 1:nslots] # Set defs for arguments - for var in result[1:(1+nargs)] + for var in result[1:nargs] push!(var.defs, 0) end for idx in 1:length(code) @@ -77,7 +67,7 @@ function make_ssa!(ci::CodeInfo, code::Vector{Any}, idx, slot, @nospecialize(typ stmt = code[idx] @assert isexpr(stmt, :(=)) code[idx] = stmt.args[2] - ci.ssavaluetypes[idx] = typ + (ci.ssavaluetypes::Vector{Any})[idx] = typ idx end @@ -98,17 +88,20 @@ end function fixup_slot!(ir::IRCode, ci::CodeInfo, idx::Int, slot::Int, @nospecialize(stmt::Union{SlotNumber, TypedSlot}), @nospecialize(ssa)) # We don't really have the information here to get rid of these. # We'll do so later - if ssa === undef_token - insert_node!(ir, idx, Any, Expr(:throw_undef_if_not, ci.slotnames[slot], false)) - return undef_token + if ssa === UNDEF_TOKEN + insert_node!(ir, idx, NewInstruction( + Expr(:throw_undef_if_not, ci.slotnames[slot], false), Any)) + return UNDEF_TOKEN end if !isa(ssa, Argument) && !(ssa === nothing) && ((ci.slotflags[slot] & SLOT_USEDUNDEF) != 0) - insert_node!(ir, idx, Any, Expr(:undefcheck, ci.slotnames[slot], ssa)) + # insert a temporary node. type_lift_pass! will remove it + insert_node!(ir, idx, NewInstruction( + Expr(:undefcheck, ci.slotnames[slot], ssa), Any)) end if isa(stmt, SlotNumber) return ssa elseif isa(stmt, TypedSlot) - return NewSSAValue(insert_node!(ir, idx, stmt.typ, PiNode(ssa, stmt.typ)).id - length(ir.stmts)) + return NewSSAValue(insert_node!(ir, idx, NewInstruction(PiNode(ssa, stmt.typ), stmt.typ)).id - length(ir.stmts)) end @assert false # unreachable end @@ -141,12 +134,13 @@ function fixemup!(cond, rename, ir::IRCode, ci::CodeInfo, idx::Int, @nospecializ return true else ssa = rename(val) - if ssa === undef_token + if ssa === UNDEF_TOKEN return false elseif !isa(ssa, SSAValue) && !isa(ssa, NewSSAValue) return true end end + # temporarily corrupt the isdefined node. type_lift_pass! will fix it stmt.args[1] = ssa end return stmt @@ -158,12 +152,12 @@ function fixemup!(cond, rename, ir::IRCode, ci::CodeInfo, idx::Int, @nospecializ x = fixup_slot!(ir, ci, idx, slot_id(val), val, rename(val)) # We inserted an undef error node. Delete subsequent statement # to avoid confusing the optimizer - if x === undef_token + if x === UNDEF_TOKEN return nothing end op[] = x - elseif isa(val, GlobalRef) && !isdefined(val.mod, val.name) - op[] = NewSSAValue(insert_node!(ir, idx, Any, val).id - length(ir.stmts)) + elseif isa(val, GlobalRef) && !(isdefined(val.mod, val.name) && isconst(val.mod, val.name)) + op[] = NewSSAValue(insert_node!(ir, idx, NewInstruction(val, Any)).id - length(ir.stmts)) end end return urs[] @@ -179,16 +173,18 @@ function rename_uses!(ir::IRCode, ci::CodeInfo, idx::Int, @nospecialize(stmt), r return fixemup!(stmt->true, stmt->renames[slot_id(stmt)], ir, ci, idx, stmt) end -function strip_trailing_junk!(ci::CodeInfo, code::Vector{Any}, info::Vector{Any}, flags::Vector{UInt8}) +function strip_trailing_junk!(ci::CodeInfo, code::Vector{Any}, info::Vector{Any}) # Remove `nothing`s at the end, we don't handle them well # (we expect the last instruction to be a terminator) + ssavaluetypes = ci.ssavaluetypes::Vector{Any} + (; codelocs, ssaflags) = ci for i = length(code):-1:1 if code[i] !== nothing resize!(code, i) - resize!(ci.ssavaluetypes, i) - resize!(ci.codelocs, i) + resize!(ssavaluetypes, i) + resize!(codelocs, i) resize!(info, i) - resize!(flags, i) + resize!(ssaflags, i) break end end @@ -197,10 +193,10 @@ function strip_trailing_junk!(ci::CodeInfo, code::Vector{Any}, info::Vector{Any} term = code[end] if !isa(term, GotoIfNot) && !isa(term, GotoNode) && !isa(term, ReturnNode) push!(code, ReturnNode()) - push!(ci.ssavaluetypes, Union{}) - push!(ci.codelocs, 0) + push!(ssavaluetypes, Union{}) + push!(codelocs, 0) push!(info, nothing) - push!(flags, 0x00) + push!(ssaflags, IR_FLAG_NULL) end nothing end @@ -213,16 +209,16 @@ end function typ_for_val(@nospecialize(x), ci::CodeInfo, sptypes::Vector{Any}, idx::Int, slottypes::Vector{Any}) if isa(x, Expr) if x.head === :static_parameter - return sptypes[x.args[1]] + return sptypes[x.args[1]::Int] elseif x.head === :boundscheck return Bool elseif x.head === :copyast return typ_for_val(x.args[1], ci, sptypes, idx, slottypes) end - return ci.ssavaluetypes[idx] + return (ci.ssavaluetypes::Vector{Any})[idx] end isa(x, GlobalRef) && return abstract_eval_global(x.mod, x.name) - isa(x, SSAValue) && return ci.ssavaluetypes[x.id] + isa(x, SSAValue) && return (ci.ssavaluetypes::Vector{Any})[x.id] isa(x, Argument) && return slottypes[x.n] isa(x, NewSSAValue) && return DelayedTyp(x) isa(x, QuoteNode) && return Const(x.value) @@ -235,41 +231,49 @@ struct BlockLiveness live_in_bbs::Vector{Int} end -# Run iterated dominance frontier -# -# The algorithm we have here essentially follows LLVM, which itself is a -# a cleaned up version of the linear-time algorithm described in -# -# A Linear Time Algorithm for Placing phi-Nodes (by Sreedhar and Gao) -# -# The algorithm here, is quite straightforward. Suppose we have a CFG: -# -# A -> B -> D -> F -# \-> C -------/ -# -# and a corresponding dominator tree: -# -# A -# |- B - D -# |- C -# |- F -# -# Now, for every definition of our slot, we simply walk down the dominator -# tree and look for any edges that leave the sub-domtree rooted by our definition. -# -# E.g. in our example above, if we have a definition in `B`, we look at its successors, -# which is only `D`, which is dominated by `B` and hence doesn't need a phi node. -# Then we descend down the subtree rooted at `B` and end up in `D`. `D` has a successor -# `F`, which is not part of the current subtree, (i.e. not dominated by `B`), so it -# needs a phi node. -# -# Now, the key insight of that algorithm is that we have two defs, in blocks `A` and `B`, -# and `A` dominates `B`, then we do not need to recurse into `B`, because the set of -# potential backedges from a subtree rooted at `B` (to outside the subtree) is a strict -# subset of those backedges from a subtree rooted at `A` (out outside the subtree rooted -# at `A`). Note however that this does not work the other way. Thus, the algorithm -# needs to make sure that we always visit `B` before `A`. -function idf(cfg::CFG, liveness::BlockLiveness, domtree::DomTree) +""" + iterated_dominance_frontier(cfg::CFG, liveness::BlockLiveness, domtree::DomTree) + -> phinodes::Vector{Int} + +Run iterated dominance frontier. +The algorithm we have here essentially follows LLVM, which itself is a +a cleaned up version of the linear-time algorithm described in [^SG95]. + +The algorithm here, is quite straightforward. Suppose we have a CFG: + + A -> B -> D -> F + \\-> C ------>/ + +and a corresponding dominator tree: + + A + |- B - D + |- C + |- F + +Now, for every definition of our slot, we simply walk down the dominator +tree and look for any edges that leave the sub-domtree rooted by our definition. + +In our example above, if we have a definition in `B`, we look at its successors, +which is only `D`, which is dominated by `B` and hence doesn't need a ϕ-node. +Then we descend down the subtree rooted at `B` and end up in `D`. `D` has a successor +`F`, which is not part of the current subtree, (i.e. not dominated by `B`), +so it needs a ϕ-node. + +Now, the key insight of that algorithm is that we have two defs, in blocks `A` and `B`, +and `A` dominates `B`, then we do not need to recurse into `B`, because the set of +potential backedges from a subtree rooted at `B` (to outside the subtree) is a strict +subset of those backedges from a subtree rooted at `A` (out outside the subtree rooted +at `A`). Note however that this does not work the other way. Thus, the algorithm +needs to make sure that we always visit `B` before `A`. + +[^SG95]: Vugranam C. Sreedhar and Guang R. Gao. 1995. + A linear time algorithm for placing φ-nodes. + In Proceedings of the 22nd ACM SIGPLAN-SIGACT symposium on Principles of programming languages (POPL '95). + Association for Computing Machinery, New York, NY, USA, 62–73. + DOI: . +""" +function iterated_dominance_frontier(cfg::CFG, liveness::BlockLiveness, domtree::DomTree) # This should be a priority queue, but TODO - sorted array for now defs = liveness.def_bbs pq = Tuple{Int, Int}[(defs[i], domtree.nodes[defs[i]].level) for i in 1:length(defs)] @@ -367,11 +371,11 @@ function rename_phinode_edges(node, bb, result_order, bb_rename) end """ - Sort the basic blocks in `ir` into domtree order (i.e. if bb`` is higher in - the domtree than bb2, it will come first in the linear order). The resulting - ir has the property that a linear traversal of basic blocks will also be a - RPO traversal and in particular, any use of an SSA value must come after (by linear - order) its definition. +Sort the basic blocks in `ir` into domtree order (i.e. if `bb1` is higher in +the domtree than `bb2`, it will come first in the linear order). The resulting +`ir` has the property that a linear traversal of basic blocks will also be a +RPO traversal and in particular, any use of an SSA value must come after +(by linear order) its definition. """ function domsort_ssa!(ir::IRCode, domtree::DomTree) # First compute the new order of basic blocks @@ -519,12 +523,14 @@ function domsort_ssa!(ir::IRCode, domtree::DomTree) return new_ir end -function compute_live_ins(cfg::CFG, defuse) +compute_live_ins(cfg::CFG, slot::SlotInfo) = compute_live_ins(cfg, slot.defs, slot.uses) + +function compute_live_ins(cfg::CFG, defs::Vector{Int}, uses::Vector{Int}) # We remove from `uses` any block where all uses are dominated # by a def. This prevents insertion of dead phi nodes at the top # of such a block if that block happens to be in a loop - ordered = Tuple{Int, Int, Bool}[(x, block_for_inst(cfg, x), true) for x in defuse.uses] - for x in defuse.defs + ordered = Tuple{Int, Int, Bool}[(x, block_for_inst(cfg, x), true) for x in uses] + for x in defs push!(ordered, (x, block_for_inst(cfg, x), false)) end ordered = sort(ordered, by=x->x[1]) @@ -545,13 +551,13 @@ function compute_live_ins(cfg::CFG, defuse) extra_liveins = BitSet() worklist = Int[] for bb in bb_uses - append!(worklist, filter(p->p != 0 && !(p in bb_defs), cfg.blocks[bb].preds)) + append!(worklist, Iterators.filter(p->p != 0 && !(p in bb_defs), cfg.blocks[bb].preds)) end while !isempty(worklist) elem = pop!(worklist) (elem in bb_uses || elem in extra_liveins) && continue push!(extra_liveins, elem) - append!(worklist, filter(p->p != 0 && !(p in bb_defs), cfg.blocks[elem].preds)) + append!(worklist, Iterators.filter(p->p != 0 && !(p in bb_defs), cfg.blocks[elem].preds)) end append!(bb_uses, extra_liveins) BlockLiveness(bb_defs, bb_uses) @@ -581,16 +587,15 @@ function recompute_type(node::Union{PhiNode, PhiCNode}, ci::CodeInfo, ir::IRCode return new_typ end -function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, nargs::Int, sptypes::Vector{Any}, - slottypes::Vector{Any}) +function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, + defuses::Vector{SlotInfo}, slottypes::Vector{Any}) code = ir.stmts.inst cfg = ir.cfg - left = Int[] catch_entry_blocks = Tuple{Int, Int}[] for idx in 1:length(code) stmt = code[idx] if isexpr(stmt, :enter) - push!(catch_entry_blocks, (block_for_inst(cfg, idx), block_for_inst(cfg, stmt.args[1]))) + push!(catch_entry_blocks, (block_for_inst(cfg, idx), block_for_inst(cfg, stmt.args[1]::Int))) end end @@ -611,7 +616,7 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg for (_, exc) in catch_entry_blocks phicnodes[exc] = Vector{Tuple{SlotNumber, NewSSAValue, PhiCNode}}() end - @timeit "idf" for (idx, slot) in Iterators.enumerate(defuse) + @timeit "idf" for (idx, slot) in Iterators.enumerate(defuses) # No uses => no need for phi nodes isempty(slot.uses) && continue # TODO: Restore this optimization @@ -624,12 +629,13 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg typ = MaybeUndef(Union{}) ssaval = nothing for use in slot.uses[] - insert_node!(ir, use, Union{}, Expr(:throw_undef_if_not, ci.slotnames[idx], false)) + insert_node!(ir, use, + NewInstruction(Expr(:throw_undef_if_not, ci.slotnames[idx], false), Union{})) end fixup_uses!(ir, ci, code, slot.uses, idx, nothing) else val = code[slot.defs[]].args[2] - typ = typ_for_val(val, ci, sptypes, slot.defs[], slottypes) + typ = typ_for_val(val, ci, ir.sptypes, slot.defs[], slottypes) ssaval = SSAValue(make_ssa!(ci, code, slot.defs[], idx, typ)) fixup_uses!(ir, ci, code, slot.uses, idx, ssaval) end @@ -643,7 +649,9 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg # Create a PhiC node in the catch entry block and # an upsilon node in the corresponding enter block node = PhiCNode(Any[]) - phic_ssa = NewSSAValue(insert_node!(ir, first_insert_for_bb(code, cfg, li), Union{}, node).id - length(ir.stmts)) + phic_ssa = NewSSAValue( + insert_node!(ir, first_insert_for_bb(code, cfg, li), + NewInstruction(node, Union{})).id - length(ir.stmts)) push!(phicnodes[li], (SlotNumber(idx), phic_ssa, node)) # Inform IDF that we now have a def in the catch block if !(li in live.def_bbs) @@ -651,21 +659,21 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg end end end - phiblocks = idf(cfg, live, domtree) + phiblocks = iterated_dominance_frontier(cfg, live, domtree) for block in phiblocks push!(phi_slots[block], idx) node = PhiNode() - ssa = NewSSAValue(insert_node!(ir, first_insert_for_bb(code, cfg, block), Union{}, node).id - length(ir.stmts)) + ssa = NewSSAValue(insert_node!(ir, + first_insert_for_bb(code, cfg, block), NewInstruction(node, Union{})).id - length(ir.stmts)) push!(phi_nodes[block], ssa=>node) end - push!(left, idx) end # Perform SSA renaming initial_incoming_vals = Any[ - if 0 in defuse[x].defs + if 0 in defuses[x].defs Argument(x) - elseif !defuse[x].any_newvar - undef_token + elseif !defuses[x].any_newvar + UNDEF_TOKEN else SSAValue(-2) end for x in 1:length(ci.slotflags) @@ -702,7 +710,7 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg continue end push!(node.edges, pred) - if incoming_val === undef_token + if incoming_val === UNDEF_TOKEN resize!(node.values, length(node.values)+1) else push!(node.values, incoming_val) @@ -712,7 +720,7 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg if isa(incoming_val, NewSSAValue) push!(type_refine_phi, ssaval.id) end - typ = incoming_val === undef_token ? MaybeUndef(Union{}) : typ_for_val(incoming_val, ci, sptypes, -1, slottypes) + typ = incoming_val === UNDEF_TOKEN ? MaybeUndef(Union{}) : typ_for_val(incoming_val, ci, ir.sptypes, -1, slottypes) old_entry = new_nodes.stmts[ssaval.id] if isa(typ, DelayedTyp) push!(type_refine_phi, ssaval.id) @@ -734,12 +742,12 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg if eidx !== nothing for (slot, _, node) in phicnodes[catch_entry_blocks[eidx][2]] ival = incoming_vals[slot_id(slot)] - ivalundef = ival === undef_token + ivalundef = ival === UNDEF_TOKEN unode = ivalundef ? UpsilonNode() : UpsilonNode(ival) - typ = ivalundef ? MaybeUndef(Union{}) : typ_for_val(ival, ci, sptypes, -1, slottypes) + typ = ivalundef ? MaybeUndef(Union{}) : typ_for_val(ival, ci, ir.sptypes, -1, slottypes) push!(node.values, NewSSAValue(insert_node!(ir, first_insert_for_bb(code, cfg, item), - typ, unode, true).id - length(ir.stmts))) + NewInstruction(unode, typ), true).id - length(ir.stmts))) end end push!(visited, item) @@ -747,7 +755,7 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg stmt = code[idx] (isa(stmt, PhiNode) || (isexpr(stmt, :(=)) && isa(stmt.args[2], PhiNode))) && continue if isa(stmt, NewvarNode) - incoming_vals[slot_id(stmt.slot)] = undef_token + incoming_vals[slot_id(stmt.slot)] = UNDEF_TOKEN code[idx] = nothing else stmt = rename_uses!(ir, ci, idx, stmt, incoming_vals) @@ -760,14 +768,14 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg if isexpr(stmt, :(=)) && isa(stmt.args[1], SlotNumber) id = slot_id(stmt.args[1]) val = stmt.args[2] - typ = typ_for_val(val, ci, sptypes, idx, slottypes) - # Having undef_token appear on the RHS is possible if we're on a dead branch. + typ = typ_for_val(val, ci, ir.sptypes, idx, slottypes) + # Having UNDEF_TOKEN appear on the RHS is possible if we're on a dead branch. # Do something reasonable here, by marking the LHS as undef as well. - if val !== undef_token - incoming_vals[id] = SSAValue(make_ssa!(ci, code, idx, id, typ)) + if val !== UNDEF_TOKEN + incoming_vals[id] = SSAValue(make_ssa!(ci, code, idx, id, typ)::Int) else code[idx] = nothing - incoming_vals[id] = undef_token + incoming_vals[id] = UNDEF_TOKEN end eidx = item while haskey(exc_handlers, eidx) @@ -775,12 +783,12 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg cidx = findfirst(x->slot_id(x[1]) == id, phicnodes[exc]) if cidx !== nothing node = UpsilonNode(incoming_vals[id]) - if incoming_vals[id] === undef_token + if incoming_vals[id] === UNDEF_TOKEN node = UpsilonNode() typ = MaybeUndef(Union{}) end push!(phicnodes[exc][cidx][3].values, - NewSSAValue(insert_node!(ir, idx, typ, node, true).id - length(ir.stmts))) + NewSSAValue(insert_node!(ir, idx, NewInstruction(node, typ), true).id - length(ir.stmts))) end end end @@ -801,9 +809,10 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg end end # Convert into IRCode form + ssavaluetypes = ci.ssavaluetypes::Vector{Any} nstmts = length(ir.stmts) new_code = Vector{Any}(undef, nstmts) - ssavalmap = fill(SSAValue(-1), length(ci.ssavaluetypes) + 1) + ssavalmap = fill(SSAValue(-1), length(ssavaluetypes) + 1) result_types = Any[Any for _ in 1:nstmts] # Detect statement positions for assignments and construct array for (bb, idx) in bbidxiter(ir) @@ -815,19 +824,19 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg new_dest = block_for_inst(cfg, stmt.dest) if new_dest == bb+1 # Drop this node - it's a noop - new_code[idx] = stmt.cond + new_code[idx] = Expr(:call, GlobalRef(Core, :typeassert), stmt.cond, GlobalRef(Core, :Bool)) else new_code[idx] = GotoIfNot(stmt.cond, new_dest) end elseif isexpr(stmt, :enter) - new_code[idx] = Expr(:enter, block_for_inst(cfg, stmt.args[1])) + new_code[idx] = Expr(:enter, block_for_inst(cfg, stmt.args[1]::Int)) ssavalmap[idx] = SSAValue(idx) # Slot to store token for pop_exception elseif isexpr(stmt, :leave) || isexpr(stmt, :(=)) || isa(stmt, ReturnNode) || isexpr(stmt, :meta) || isa(stmt, NewvarNode) new_code[idx] = stmt else ssavalmap[idx] = SSAValue(idx) - result_types[idx] = ci.ssavaluetypes[idx] + result_types[idx] = ssavaluetypes[idx] if isa(stmt, PhiNode) edges = Int32[edge == 0 ? 0 : block_for_inst(cfg, Int(edge)) for edge in stmt.edges] new_code[idx] = PhiNode(edges, stmt.values) @@ -845,7 +854,7 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg node = new_nodes.stmts[new_idx] phic_values = (node[:inst]::PhiCNode).values for i = 1:length(phic_values) - orig_typ = typ = typ_for_val(phic_values[i], ci, sptypes, -1, slottypes) + orig_typ = typ = typ_for_val(phic_values[i], ci, ir.sptypes, -1, slottypes) @assert !isa(typ, MaybeUndef) while isa(typ, DelayedTyp) typ = types(ir)[typ.phi::NewSSAValue] @@ -863,7 +872,7 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, domtree::DomTree, defuse, narg changed = false for new_idx in type_refine_phi node = new_nodes.stmts[new_idx] - new_typ = recompute_type(node[:inst], ci, ir, sptypes, slottypes) + new_typ = recompute_type(node[:inst]::Union{PhiNode,PhiCNode}, ci, ir, ir.sptypes, slottypes) if !(node[:type] ⊑ new_typ) || !(new_typ ⊑ node[:type]) node[:type] = new_typ changed = true diff --git a/base/compiler/ssair/verify.jl b/base/compiler/ssair/verify.jl index 40cc8731ce4771..1578bdb9c348ac 100644 --- a/base/compiler/ssair/verify.jl +++ b/base/compiler/ssair/verify.jl @@ -14,17 +14,17 @@ end function check_op(ir::IRCode, domtree::DomTree, @nospecialize(op), use_bb::Int, use_idx::Int, print::Bool) if isa(op, SSAValue) if op.id > length(ir.stmts) - def_bb = block_for_inst(ir.cfg, ir.new_nodes[op.id - length(ir.stmts)].pos) + def_bb = block_for_inst(ir.cfg, ir.new_nodes.info[op.id - length(ir.stmts)].pos) else def_bb = block_for_inst(ir.cfg, op.id) end if (def_bb == use_bb) if op.id > length(ir.stmts) - @assert ir.new_nodes[op.id - length(ir.stmts)].pos <= use_idx + @assert ir.new_nodes.info[op.id - length(ir.stmts)].pos <= use_idx else if op.id >= use_idx @verify_error "Def ($(op.id)) does not dominate use ($(use_idx)) in same BB" - error() + error("") end end else @@ -32,21 +32,21 @@ function check_op(ir::IRCode, domtree::DomTree, @nospecialize(op), use_bb::Int, # At the moment, we allow GC preserve tokens outside the standard domination notion #@Base.show ir @verify_error "Basic Block $def_bb does not dominate block $use_bb (tried to use value $(op.id))" - error() + error("") end end elseif isa(op, GlobalRef) - if !isdefined(op.mod, op.name) + if !isdefined(op.mod, op.name) || !isconst(op.mod, op.name) @verify_error "Unbound GlobalRef not allowed in value position" - error() + error("") end elseif isa(op, Union{OldSSAValue, NewSSAValue}) #@Base.show ir @verify_error "Left over SSA marker" - error() + error("") elseif isa(op, Union{SlotNumber, TypedSlot}) @verify_error "Left over slot detected in converted IR" - error() + error("") end end @@ -66,12 +66,12 @@ function verify_ir(ir::IRCode, print::Bool=true) # Verify CFG last_end = 0 # Verify statements - domtree = construct_domtree(ir.cfg) + domtree = construct_domtree(ir.cfg.blocks) for (idx, block) in pairs(ir.cfg.blocks) if first(block.stmts) != last_end + 1 #ranges = [(idx,first(bb.stmts),last(bb.stmts)) for (idx, bb) in pairs(ir.cfg.blocks)] @verify_error "First statement of BB $idx ($(first(block.stmts))) does not match end of previous ($last_end)" - error() + error("") end last_end = last(block.stmts) terminator = ir.stmts[last_end][:inst] @@ -82,38 +82,38 @@ function verify_ir(ir::IRCode, print::Bool=true) c = count_int(idx, ir.cfg.blocks[p].succs) if c == 0 @verify_error "Predecessor $p of block $idx not in successor list" - error() + error("") elseif c == 2 if count_int(p, block.preds) != 2 @verify_error "Double edge from $p to $idx not correctly accounted" - error() + error("") end end end if isa(terminator, ReturnNode) if !isempty(block.succs) @verify_error "Block $idx ends in return or unreachable, but has successors" - error() + error("") end elseif isa(terminator, GotoNode) if length(block.succs) != 1 || block.succs[1] != terminator.label @verify_error "Block $idx successors ($(block.succs)), does not match GotoNode terminator" - error() + error("") end elseif isa(terminator, GotoIfNot) if terminator.dest == idx + 1 @verify_error "Block $idx terminator forms a double edge to block $(idx+1)" - error() + error("") end if length(block.succs) != 2 || (block.succs != [terminator.dest, idx+1] && block.succs != [idx+1, terminator.dest]) @verify_error "Block $idx successors ($(block.succs)), does not match GotoIfNot terminator" - error() + error("") end elseif isexpr(terminator, :enter) @label enter_check - if length(block.succs) != 2 || (block.succs != [terminator.args[1], idx+1] && block.succs != [idx+1, terminator.args[1]]) + if length(block.succs) != 2 || (block.succs != Int[terminator.args[1], idx+1] && block.succs != Int[idx+1, terminator.args[1]]) @verify_error "Block $idx successors ($(block.succs)), does not match :enter terminator" - error() + error("") end else if length(block.succs) != 1 || block.succs[1] != idx + 1 @@ -128,7 +128,7 @@ function verify_ir(ir::IRCode, print::Bool=true) isa(stmt, PhiNode) || break end @verify_error "Block $idx successors ($(block.succs)), does not match fall-through terminator ($terminator)" - error() + error("") end end for s in block.succs @@ -137,7 +137,7 @@ function verify_ir(ir::IRCode, print::Bool=true) #@Base.show ir #@Base.show ir.argtypes @verify_error "Successor $s of block $idx not in predecessor list" - error() + error("") end end end @@ -151,11 +151,19 @@ function verify_ir(ir::IRCode, print::Bool=true) @assert length(stmt.edges) == length(stmt.values) for i = 1:length(stmt.edges) edge = stmt.edges[i] + for j = (i+1):length(stmt.edges) + edge′ = stmt.edges[j] + if edge == edge′ + # TODO: Move `unique` to Core.Compiler. For now we assume the predecessor list is + @verify_error "Edge list φ node $idx in bb $bb not unique (double edge?)" + error("") + end + end if !(edge == 0 && bb == 1) && !(edge in ir.cfg.blocks[bb].preds) #@Base.show ir.argtypes #@Base.show ir @verify_error "Edge $edge of φ node $idx not in predecessor list" - error() + error("") end edge == 0 && continue isassigned(stmt.values, i) || continue @@ -168,11 +176,11 @@ function verify_ir(ir::IRCode, print::Bool=true) # PhiNode type was $phiT # Value type was $(ir.stmts[val.id][:type]) #""" - #error() + #error("") end elseif isa(val, GlobalRef) || isa(val, Expr) @verify_error "GlobalRefs and Exprs are not allowed as PhiNode values" - error() + error("") end check_op(ir, domtree, val, Int(edge), last(ir.cfg.blocks[stmt.edges[i]].stmts)+1, print) end @@ -181,11 +189,11 @@ function verify_ir(ir::IRCode, print::Bool=true) val = stmt.values[i] if !isa(val, SSAValue) @verify_error "Operand $i of PhiC node $idx must be an SSA Value." - error() + error("") end - if !isa(ir[val], UpsilonNode) + if !isa(ir[val][:inst], UpsilonNode) @verify_error "Operand $i of PhiC node $idx must reference an Upsilon node." - error() + error("") end end else @@ -200,13 +208,20 @@ function verify_ir(ir::IRCode, print::Bool=true) if stmt.head === :(=) if stmt.args[1] isa SSAValue @verify_error "SSAValue as assignment LHS" - error() + error("") + end + if stmt.args[2] isa GlobalRef + # undefined GlobalRef as assignment RHS is OK + continue end elseif stmt.head === :gc_preserve_end # We allow gc_preserve_end tokens to span across try/catch # blocks, which isn't allowed for regular SSA values, so # we skip the validation below. continue + elseif stmt.head === :isdefined && length(stmt.args) == 1 && stmt.args[1] isa GlobalRef + # a GlobalRef isdefined check does not evaluate its argument + continue end end for op in userefs(stmt) @@ -222,7 +237,7 @@ function verify_linetable(linetable::Vector{LineInfoNode}, print::Bool=true) line = linetable[i] if i <= line.inlined_at @verify_error "Misordered linetable" - error() + error("") end end end diff --git a/base/compiler/stmtinfo.jl b/base/compiler/stmtinfo.jl index 214fd89a170788..4832ce1af4a3aa 100644 --- a/base/compiler/stmtinfo.jl +++ b/base/compiler/stmtinfo.jl @@ -1,65 +1,112 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +@nospecialize + """ - struct MethodMatchInfo + call::CallMeta -Captures the result of a `method_matches` lookup for the given call. This -info may then be used by the optimizer to inline the matches, without having -to re-consult the method table. This info is illegal on any statement that is -not a call to a generic function. +A simple struct that captures both the return type (`call.rt`) +and any additional information (`call.info`) for a given generic call. """ -struct MethodMatchInfo - results::Union{Missing, MethodLookupResult} +struct CallMeta + rt::Any + info::Any end """ - struct MethodResultPure + info::MethodMatchInfo -This singleton represents a method result constant was proven to be -effect-free, including being no-throw (typically because the value was computed -by calling an `@pure` function). +Captures the result of a `:jl_matching_methods` lookup for the given call (`info.results`). +This info may then be used by the optimizer to inline the matches, without having +to re-consult the method table. This info is illegal on any statement that is +not a call to a generic function. """ -struct MethodResultPure end +struct MethodMatchInfo + results::MethodLookupResult +end """ - struct UnionSplitInfo + info::UnionSplitInfo If inference decides to partition the method search space by splitting unions, it will issue a method lookup query for each such partition. This info indicates -that such partitioning happened and wraps the corresponding MethodMatchInfo for -each partition. This info is illegal on any statement that is not a call to a -generic function. +that such partitioning happened and wraps the corresponding `MethodMatchInfo` for +each partition (`info.matches::Vector{MethodMatchInfo}`). +This info is illegal on any statement that is not a call to a generic function. """ struct UnionSplitInfo matches::Vector{MethodMatchInfo} end +nmatches(info::MethodMatchInfo) = length(info.results) +function nmatches(info::UnionSplitInfo) + n = 0 + for mminfo in info.matches + n += nmatches(mminfo) + end + return n +end + +struct ConstPropResult + result::InferenceResult +end + +struct ConcreteResult + mi::MethodInstance + effects::Effects + result + ConcreteResult(mi::MethodInstance, effects::Effects) = new(mi, effects) + ConcreteResult(mi::MethodInstance, effects::Effects, @nospecialize val) = new(mi, effects, val) +end + +const ConstResult = Union{ConstPropResult,ConcreteResult} + """ - struct CallMeta + info::ConstCallInfo -A simple struct that captures both the return type (`rt`) and any additional information -(`info`) for a given generic call. +The precision of this call was improved using constant information. +In addition to the original call information `info.call`, this info also keeps the results +of constant inference `info.results::Vector{Union{Nothing,ConstResult}}`. """ -struct CallMeta - rt::Any - info::Any +struct ConstCallInfo + call::Union{MethodMatchInfo,UnionSplitInfo} + results::Vector{Union{Nothing,ConstResult}} +end + +""" + info::MethodResultPure + +This struct represents a method result constant was proven to be +effect-free, including being no-throw (typically because the value was computed +by calling an `@pure` function). +""" +struct MethodResultPure + info::Union{MethodMatchInfo,UnionSplitInfo,Bool} +end +let instance = MethodResultPure(false) + global MethodResultPure + MethodResultPure() = instance end """ - struct AbstractIterationInfo + info::AbstractIterationInfo Captures all the information for abstract iteration analysis of a single value. -Each (abstract) call to `iterate`, corresponds to one entry in `each`. +Each (abstract) call to `iterate`, corresponds to one entry in `info.each::Vector{CallMeta}`. """ struct AbstractIterationInfo each::Vector{CallMeta} end +const MaybeAbstractIterationInfo = Union{Nothing, AbstractIterationInfo} + """ - struct ApplyCallInfo + info::ApplyCallInfo This info applies to any call of `_apply_iterate(...)` and captures both the info of the actual call being applied and the info for any implicit call to the `iterate` function. Note that it is possible for the call itself -to be yet another `_apply_iterate`, in which case the `.call` field will +to be yet another `_apply_iterate`, in which case the `info.call` field will be another `ApplyCallInfo`. This info is illegal on any statement that is not an `_apply_iterate` call. """ @@ -67,15 +114,72 @@ struct ApplyCallInfo # The info for the call itself call::Any # AbstractIterationInfo for each argument, if applicable - arginfo::Vector{Union{Nothing, AbstractIterationInfo}} + arginfo::Vector{MaybeAbstractIterationInfo} end """ - struct UnionSplitApplyCallInfo + info::UnionSplitApplyCallInfo -Like `UnionSplitInfo`, but for `ApplyCallInfo` rather than MethodMatchInfo. +Like `UnionSplitInfo`, but for `ApplyCallInfo` rather than `MethodMatchInfo`. This info is illegal on any statement that is not an `_apply_iterate` call. """ struct UnionSplitApplyCallInfo infos::Vector{ApplyCallInfo} end + +""" + info::InvokeCallInfo + +Represents a resolved call to `Core.invoke`, carrying the `info.match::MethodMatch` of +the method that has been processed. +Optionally keeps `info.result::InferenceResult` that keeps constant information. +""" +struct InvokeCallInfo + match::MethodMatch + result::Union{Nothing,ConstResult} +end + +""" + info::OpaqueClosureCallInfo + +Represents a resolved call of opaque closure, carrying the `info.match::MethodMatch` of +the method that has been processed. +Optionally keeps `info.result::InferenceResult` that keeps constant information. +""" +struct OpaqueClosureCallInfo + match::MethodMatch + result::Union{Nothing,ConstResult} +end + +""" + info::OpaqueClosureCreateInfo + +This info may be constructed upon opaque closure construction, with `info.unspec::CallMeta` +carrying out inference result of an unreal, partially specialized call (i.e. specialized on +the closure environment, but not on the argument types of the opaque closure) in order to +allow the optimizer to rewrite the return type parameter of the `OpaqueClosure` based on it. +""" +struct OpaqueClosureCreateInfo + unspec::CallMeta + function OpaqueClosureCreateInfo(unspec::CallMeta) + @assert isa(unspec.info, OpaqueClosureCallInfo) + return new(unspec) + end +end + +# Stmt infos that are used by external consumers, but not by optimization. +# These are not produced by default and must be explicitly opted into by +# the AbstractInterpreter. + +""" + info::ReturnTypeCallInfo + +Represents a resolved call of `Core.Compiler.return_type`. +`info.call` wraps the info corresponding to the call that `Core.Compiler.return_type` call +was supposed to analyze. +""" +struct ReturnTypeCallInfo + info::Any +end + +@specialize diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index cbf8e4f0f591de..e6625e2d559259 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -10,7 +10,7 @@ const _NAMEDTUPLE_NAME = NamedTuple.body.body.name const INT_INF = typemax(Int) # integer infinity -const N_IFUNC = reinterpret(Int32, arraylen) + 1 +const N_IFUNC = reinterpret(Int32, have_fma) + 1 const T_IFUNC = Vector{Tuple{Int, Int, Any}}(undef, N_IFUNC) const T_IFUNC_COST = Vector{Int}(undef, N_IFUNC) const T_FFUNC_KEY = Vector{Any}() @@ -24,16 +24,7 @@ function find_tfunc(@nospecialize f) end end -const DATATYPE_NAME_FIELDINDEX = fieldindex(DataType, :name) -const DATATYPE_PARAMETERS_FIELDINDEX = fieldindex(DataType, :parameters) const DATATYPE_TYPES_FIELDINDEX = fieldindex(DataType, :types) -const DATATYPE_SUPER_FIELDINDEX = fieldindex(DataType, :super) -const DATATYPE_MUTABLE_FIELDINDEX = fieldindex(DataType, :mutable) -const DATATYPE_INSTANCE_FIELDINDEX = fieldindex(DataType, :instance) - -const TYPENAME_NAME_FIELDINDEX = fieldindex(Core.TypeName, :name) -const TYPENAME_MODULE_FIELDINDEX = fieldindex(Core.TypeName, :module) -const TYPENAME_WRAPPER_FIELDINDEX = fieldindex(Core.TypeName, :wrapper) ########## # tfuncs # @@ -58,40 +49,55 @@ end add_tfunc(throw, 1, 1, (@nospecialize(x)) -> Bottom, 0) # the inverse of typeof_tfunc -# returns (type, isexact) +# returns (type, isexact, isconcrete, istype) # if isexact is false, the actual runtime type may (will) be a subtype of t +# if isconcrete is true, the actual runtime type is definitely concrete (unreachable if not valid as a typeof) +# if istype is true, the actual runtime value will definitely be a type (e.g. this is false for Union{Type{Int}, Int}) function instanceof_tfunc(@nospecialize(t)) if isa(t, Const) - if isa(t.val, Type) - return t.val, true + if isa(t.val, Type) && valid_as_lattice(t.val) + return t.val, true, isconcretetype(t.val), true end - return Bottom, true + return Bottom, true, false, false # runtime throws on non-Type end t = widenconst(t) - if t === Bottom || t === typeof(Bottom) || typeintersect(t, Type) === Bottom - return Bottom, true + if t === Bottom + return Bottom, true, true, false # runtime unreachable + elseif t === typeof(Bottom) || !hasintersect(t, Type) + return Bottom, true, false, false # literal Bottom or non-Type elseif isType(t) tp = t.parameters[1] - return tp, !has_free_typevars(tp) + valid_as_lattice(tp) || return Bottom, true, false, false # runtime unreachable / throws on non-Type + return tp, !has_free_typevars(tp), isconcretetype(tp), true elseif isa(t, UnionAll) t′ = unwrap_unionall(t) - t′′, isexact = instanceof_tfunc(t′) + t′′, isexact, isconcrete, istype = instanceof_tfunc(t′) tr = rewrap_unionall(t′′, t) - if t′′ isa DataType && !has_free_typevars(tr) + if t′′ isa DataType && t′′.name !== Tuple.name && !has_free_typevars(tr) # a real instance must be within the declared bounds of the type, # so we can intersect with the original wrapper. tr = typeintersect(tr, t′′.name.wrapper) + isconcrete = !isabstracttype(t′′) + if tr === Union{} + # runtime unreachable (our inference Type{T} where S is + # uninhabited with any runtime T that exists) + isexact = true + end end - return tr, isexact + return tr, isexact, isconcrete, istype elseif isa(t, Union) - ta, isexact_a = instanceof_tfunc(t.a) - tb, isexact_b = instanceof_tfunc(t.b) - ta === Union{} && return tb, isexact_b - tb === Union{} && return ta, isexact_a - ta == tb && return ta, isexact_a && isexact_b - return Union{ta, tb}, false # at runtime, will be exactly one of these - end - return Any, false + ta, isexact_a, isconcrete_a, istype_a = instanceof_tfunc(t.a) + tb, isexact_b, isconcrete_b, istype_b = instanceof_tfunc(t.b) + isconcrete = isconcrete_a && isconcrete_b + istype = istype_a && istype_b + # most users already handle the Union case, so here we assume that + # `isexact` only cares about the answers where there's actually a Type + # (and assuming other cases causing runtime errors) + ta === Union{} && return tb, isexact_b, isconcrete, istype + tb === Union{} && return ta, isexact_a, isconcrete, istype + return Union{ta, tb}, false, isconcrete, istype # at runtime, will be exactly one of these + end + return Any, false, false, false end bitcast_tfunc(@nospecialize(t), @nospecialize(x)) = instanceof_tfunc(t)[1] math_tfunc(@nospecialize(x)) = widenconst(x) @@ -176,7 +182,6 @@ add_tfunc(ne_float, 2, 2, cmp_tfunc, 2) add_tfunc(lt_float, 2, 2, cmp_tfunc, 2) add_tfunc(le_float, 2, 2, cmp_tfunc, 2) add_tfunc(fpiseq, 2, 2, cmp_tfunc, 1) -add_tfunc(fpislt, 2, 2, cmp_tfunc, 1) add_tfunc(eq_float_fast, 2, 2, cmp_tfunc, 1) add_tfunc(ne_float_fast, 2, 2, cmp_tfunc, 1) add_tfunc(lt_float_fast, 2, 2, cmp_tfunc, 1) @@ -197,6 +202,7 @@ cglobal_tfunc(@nospecialize(fptr)) = Ptr{Cvoid} cglobal_tfunc(@nospecialize(fptr), @nospecialize(t)) = (isType(t) ? Ptr{t.parameters[1]} : Ptr) cglobal_tfunc(@nospecialize(fptr), t::Const) = (isa(t.val, Type) ? Ptr{t.val} : Ptr) add_tfunc(Core.Intrinsics.cglobal, 1, 2, cglobal_tfunc, 5) +add_tfunc(Core.Intrinsics.have_fma, 1, 1, @nospecialize(x)->Bool, 1) function ifelse_tfunc(@nospecialize(cnd), @nospecialize(x), @nospecialize(y)) if isa(cnd, Const) @@ -214,7 +220,7 @@ function ifelse_tfunc(@nospecialize(cnd), @nospecialize(x), @nospecialize(y)) end return tmerge(x, y) end -add_tfunc(ifelse, 3, 3, ifelse_tfunc, 1) +add_tfunc(Core.ifelse, 3, 3, ifelse_tfunc, 1) function egal_tfunc(@nospecialize(x), @nospecialize(y)) xx = widenconditional(x) @@ -229,7 +235,7 @@ function egal_tfunc(@nospecialize(x), @nospecialize(y)) return Const(false) elseif isa(xx, Const) && isa(yy, Const) return Const(xx.val === yy.val) - elseif typeintersect(widenconst(xx), widenconst(yy)) === Bottom + elseif !hasintersect(widenconst(xx), widenconst(yy)) return Const(false) elseif (isa(xx, Const) && y === typeof(xx.val) && isdefined(y, :instance)) || (isa(yy, Const) && x === typeof(yy.val) && isdefined(x, :instance)) @@ -241,10 +247,11 @@ add_tfunc(===, 2, 2, egal_tfunc, 1) function isdefined_nothrow(argtypes::Array{Any, 1}) length(argtypes) == 2 || return false - return typeintersect(widenconst(argtypes[1]), Module) === Union{} ? - (argtypes[2] ⊑ Symbol || argtypes[2] ⊑ Int) : - argtypes[2] ⊑ Symbol + return hasintersect(widenconst(argtypes[1]), Module) ? + argtypes[2] ⊑ Symbol : + (argtypes[2] ⊑ Symbol || argtypes[2] ⊑ Int) end +isdefined_tfunc(arg1, sym, order) = (@nospecialize; isdefined_tfunc(arg1, sym)) function isdefined_tfunc(@nospecialize(arg1), @nospecialize(sym)) if isa(arg1, Const) a1 = typeof(arg1.val) @@ -255,10 +262,11 @@ function isdefined_tfunc(@nospecialize(arg1), @nospecialize(sym)) return Bool end a1 = unwrap_unionall(a1) - if isa(a1, DataType) && !a1.abstract + if isa(a1, DataType) && !isabstracttype(a1) if a1 === Module - Symbol <: widenconst(sym) || return Bottom - if isa(sym, Const) && isa(sym.val, Symbol) && isa(arg1, Const) && isdefined(arg1.val, sym.val) + hasintersect(widenconst(sym), Symbol) || return Bottom + if isa(sym, Const) && isa(sym.val, Symbol) && isa(arg1, Const) && + isdefined(arg1.val::Module, sym.val::Symbol) return Const(true) end elseif isa(sym, Const) @@ -270,62 +278,76 @@ function isdefined_tfunc(@nospecialize(arg1), @nospecialize(sym)) else return Bottom end - if 1 <= idx <= a1.ninitialized + if 1 <= idx <= datatype_min_ninitialized(a1) return Const(true) elseif a1.name === _NAMEDTUPLE_NAME if isconcretetype(a1) return Const(false) + else + ns = a1.parameters[1] + if isa(ns, Tuple) + return Const(1 <= idx <= length(ns)) + end end elseif idx <= 0 || (!isvatuple(a1) && idx > fieldcount(a1)) return Const(false) - elseif !isvatuple(a1) && isbitstype(fieldtype(a1, idx)) - return Const(true) elseif isa(arg1, Const) arg1v = (arg1::Const).val - if !ismutable(arg1v) || isdefined(arg1v, idx) || (isa(arg1v, DataType) && is_dt_const_field(idx)) + if !ismutable(arg1v) || isdefined(arg1v, idx) || isconst(typeof(arg1v), idx) return Const(isdefined(arg1v, idx)) end + elseif !isvatuple(a1) + fieldT = fieldtype(a1, idx) + if isa(fieldT, DataType) && isbitstype(fieldT) + return Const(true) + end end end + elseif isa(a1, Union) + return tmerge(isdefined_tfunc(a1.a, sym), + isdefined_tfunc(a1.b, sym)) end return Bool end -add_tfunc(isdefined, 2, 2, isdefined_tfunc, 1) +add_tfunc(isdefined, 2, 3, isdefined_tfunc, 1) function sizeof_nothrow(@nospecialize(x)) if isa(x, Const) - x = x.val - if !isa(x, Type) || x === DataType + if !isa(x.val, Type) || x.val === DataType return true end elseif isa(x, Conditional) return true end - if isa(x, Union) - return sizeof_nothrow(x.a) && sizeof_nothrow(x.b) + xu = unwrap_unionall(x) + if isa(xu, Union) + return sizeof_nothrow(rewrap_unionall(xu.a, x)) && + sizeof_nothrow(rewrap_unionall(xu.b, x)) end - t, exact = instanceof_tfunc(x) - if !exact - # Could always be bottom at runtime, which throws - return false - end - if t !== Bottom - t === DataType && return true - x = t - x = unwrap_unionall(x) - if isa(x, Union) - isinline, sz, _ = uniontype_layout(x) - return isinline - end - isa(x, DataType) || return false - x.layout == C_NULL && return false - (datatype_nfields(x) == 0 && !datatype_pointerfree(x)) && return false - return true - else + t, exact, isconcrete = instanceof_tfunc(x) + if t === Bottom + # x must be an instance (not a Type) or is the Bottom type object x = widenconst(x) - x === DataType && return false - return isconcretetype(x) || isprimitivetype(x) + return !hasintersect(x, Type) + end + x = unwrap_unionall(t) + if isconcrete + if isa(x, DataType) && x.layout != C_NULL + # there's just a few concrete types with an opaque layout + (datatype_nfields(x) == 0 && !datatype_pointerfree(x)) && return false + end + return true # these must always have a size of these + end + exact || return false # Could always be the type Bottom at runtime, for example, which throws + t === DataType && return true # DataType itself has a size + if isa(x, Union) + isinline = uniontype_layout(x)[1] + return isinline # even any subset of this union would have a size end + isa(x, DataType) || return false + x.layout == C_NULL && return false + (datatype_nfields(x) == 0 && !datatype_pointerfree(x)) && return false # is-layout-opaque + return true end function _const_sizeof(@nospecialize(x)) @@ -346,8 +368,10 @@ function sizeof_tfunc(@nospecialize(x),) isa(x, Const) && return _const_sizeof(x.val) isa(x, Conditional) && return _const_sizeof(Bool) isconstType(x) && return _const_sizeof(x.parameters[1]) - if isa(x, Union) - return tmerge(sizeof_tfunc(x.a), sizeof_tfunc(x.b)) + xu = unwrap_unionall(x) + if isa(xu, Union) + return tmerge(sizeof_tfunc(rewrap_unionall(xu.a, x)), + sizeof_tfunc(rewrap_unionall(xu.b, x))) end # Core.sizeof operates on either a type or a value. First check which # case we're in. @@ -356,9 +380,9 @@ function sizeof_tfunc(@nospecialize(x),) # The value corresponding to `x` at runtime could be a type. # Normalize the query to ask about that type. x = unwrap_unionall(t) - if isa(x, Union) - isinline, sz, _ = uniontype_layout(x) - return isinline ? Const(Int(sz)) : (exact ? Bottom : Int) + if exact && isa(x, Union) + isinline = uniontype_layout(x)[1] + return isinline ? Const(Int(Core.sizeof(x))) : Bottom end isa(x, DataType) || return Int (isconcretetype(x) || isprimitivetype(x)) && return _const_sizeof(x) @@ -375,7 +399,7 @@ function nfields_tfunc(@nospecialize(x)) isa(x, Conditional) && return Const(0) x = unwrap_unionall(widenconst(x)) isconstType(x) && return Const(nfields(x.parameters[1])) - if isa(x, DataType) && !x.abstract + if isa(x, DataType) && !isabstracttype(x) if !(x.name === Tuple.name && isvatuple(x)) && !(x.name === _NAMEDTUPLE_NAME && !isconcretetype(x)) return Const(isdefined(x, :types) ? length(x.types) : length(x.name.names)) @@ -396,7 +420,8 @@ function typevar_tfunc(@nospecialize(n), @nospecialize(lb_arg), @nospecialize(ub ub = Any ub_certain = lb_certain = true if isa(n, Const) - isa(n.val, Symbol) || return Union{} + nval = n.val + isa(nval, Symbol) || return Union{} if isa(lb_arg, Const) lb = lb_arg.val elseif isType(lb_arg) @@ -413,7 +438,7 @@ function typevar_tfunc(@nospecialize(n), @nospecialize(lb_arg), @nospecialize(ub else return TypeVar end - tv = TypeVar(n.val, lb, ub) + tv = TypeVar(nval, lb, ub) return PartialTypeVar(tv, lb_certain, ub_certain) end return TypeVar @@ -422,9 +447,7 @@ function typebound_nothrow(b) b = widenconst(b) (b ⊑ TypeVar) && return true if isType(b) - b = unwrap_unionall(b.parameters[1]) - b === Union{} && return true - return !isa(b, DataType) || b.name != _va_typename + return true end return false end @@ -437,26 +460,105 @@ end add_tfunc(Core._typevar, 3, 3, typevar_tfunc, 100) add_tfunc(applicable, 1, INT_INF, (@nospecialize(f), args...)->Bool, 100) add_tfunc(Core.Intrinsics.arraylen, 1, 1, @nospecialize(x)->Int, 4) -add_tfunc(arraysize, 2, 2, (@nospecialize(a), @nospecialize(d))->Int, 4) + +function arraysize_tfunc(@nospecialize(ary), @nospecialize(dim)) + hasintersect(widenconst(ary), Array) || return Bottom + hasintersect(widenconst(dim), Int) || return Bottom + return Int +end +add_tfunc(arraysize, 2, 2, arraysize_tfunc, 4) + +function arraysize_nothrow(argtypes::Vector{Any}) + length(argtypes) == 2 || return false + ary = argtypes[1] + dim = argtypes[2] + ary ⊑ Array || return false + if isa(dim, Const) + dimval = dim.val + return isa(dimval, Int) && dimval > 0 + end + return false +end + +struct MemoryOrder x::Cint end +const MEMORY_ORDER_UNSPECIFIED = MemoryOrder(-2) +const MEMORY_ORDER_INVALID = MemoryOrder(-1) +const MEMORY_ORDER_NOTATOMIC = MemoryOrder(0) +const MEMORY_ORDER_UNORDERED = MemoryOrder(1) +const MEMORY_ORDER_MONOTONIC = MemoryOrder(2) +const MEMORY_ORDER_CONSUME = MemoryOrder(3) +const MEMORY_ORDER_ACQUIRE = MemoryOrder(4) +const MEMORY_ORDER_RELEASE = MemoryOrder(5) +const MEMORY_ORDER_ACQ_REL = MemoryOrder(6) +const MEMORY_ORDER_SEQ_CST = MemoryOrder(7) + +function get_atomic_order(order::Symbol, loading::Bool, storing::Bool) + if order === :not_atomic + return MEMORY_ORDER_NOTATOMIC + elseif order === :unordered && (loading ⊻ storing) + return MEMORY_ORDER_UNORDERED + elseif order === :monotonic && (loading | storing) + return MEMORY_ORDER_MONOTONIC + elseif order === :acquire && loading + return MEMORY_ORDER_ACQUIRE + elseif order === :release && storing + return MEMORY_ORDER_RELEASE + elseif order === :acquire_release && (loading & storing) + return MEMORY_ORDER_ACQ_REL + elseif order === :sequentially_consistent + return MEMORY_ORDER_SEQ_CST + end + return MEMORY_ORDER_INVALID +end + function pointer_eltype(@nospecialize(ptr)) a = widenconst(ptr) - if a <: Ptr - if isa(a,DataType) && isa(a.parameters[1],Type) - return a.parameters[1] - elseif isa(a,UnionAll) && !has_free_typevars(a) - unw = unwrap_unionall(a) - if isa(unw,DataType) - return rewrap_unionall(unw.parameters[1], a) - end + if !has_free_typevars(a) + unw = unwrap_unionall(a) + if isa(unw, DataType) && unw.name === Ptr.body.name + T = unw.parameters[1] + valid_as_lattice(T) || return Bottom + return rewrap_unionall(T, a) end end return Any end -add_tfunc(pointerref, 3, 3, - function (@nospecialize(a), @nospecialize(i), @nospecialize(align)) - return pointer_eltype(a) - end, 4) -add_tfunc(pointerset, 4, 4, (@nospecialize(a), @nospecialize(v), @nospecialize(i), @nospecialize(align)) -> a, 5) +function atomic_pointermodify_tfunc(ptr, op, v, order) + @nospecialize + a = widenconst(ptr) + if !has_free_typevars(a) + unw = unwrap_unionall(a) + if isa(unw, DataType) && unw.name === Ptr.body.name + T = unw.parameters[1] + # note: we could sometimes refine this to a PartialStruct if we analyzed `op(T, T)::T` + valid_as_lattice(T) || return Bottom + return rewrap_unionall(Pair{T, T}, a) + end + end + return Pair +end +function atomic_pointerreplace_tfunc(ptr, x, v, success_order, failure_order) + @nospecialize + a = widenconst(ptr) + if !has_free_typevars(a) + unw = unwrap_unionall(a) + if isa(unw, DataType) && unw.name === Ptr.body.name + T = unw.parameters[1] + valid_as_lattice(T) || return Bottom + return rewrap_unionall(ccall(:jl_apply_cmpswap_type, Any, (Any,), T), a) + end + end + return ccall(:jl_apply_cmpswap_type, Any, (Any,), T) where T +end +add_tfunc(pointerref, 3, 3, (a, i, align) -> (@nospecialize; pointer_eltype(a)), 4) +add_tfunc(pointerset, 4, 4, (a, v, i, align) -> (@nospecialize; a), 5) +add_tfunc(atomic_fence, 1, 1, (order) -> (@nospecialize; Nothing), 4) +add_tfunc(atomic_pointerref, 2, 2, (a, order) -> (@nospecialize; pointer_eltype(a)), 4) +add_tfunc(atomic_pointerset, 3, 3, (a, v, order) -> (@nospecialize; a), 5) +add_tfunc(atomic_pointerswap, 3, 3, (a, v, order) -> (@nospecialize; pointer_eltype(a)), 5) +add_tfunc(atomic_pointermodify, 4, 4, atomic_pointermodify_tfunc, 5) +add_tfunc(atomic_pointerreplace, 5, 5, atomic_pointerreplace_tfunc, 5) +add_tfunc(donotdelete, 0, INT_INF, (@nospecialize args...)->Nothing, 0) # more accurate typeof_tfunc for vararg tuples abstract only in length function typeof_concrete_vararg(t::DataType) @@ -464,9 +566,8 @@ function typeof_concrete_vararg(t::DataType) for i = 1:np p = t.parameters[i] if i == np && isvarargtype(p) - pp = unwrap_unionall(p) - if isconcretetype(pp.parameters[1]) && pp.parameters[2] isa TypeVar - return rewrap_unionall(Type{Tuple{t.parameters[1:np-1]..., pp}}, p) + if isdefined(p, :T) && !isdefined(p, :N) && isconcretetype(p.T) + return Type{Tuple{t.parameters[1:np-1]..., Vararg{p.T, N}}} where N end elseif !isconcretetype(p) break @@ -496,14 +597,12 @@ function typeof_tfunc(@nospecialize(t)) return Type{<:t} end elseif isa(t, Union) - a = widenconst(typeof_tfunc(t.a)) - b = widenconst(typeof_tfunc(t.b)) + a = widenconst(_typeof_tfunc(t.a)) + b = widenconst(_typeof_tfunc(t.b)) return Union{a, b} - elseif isa(t, TypeVar) && !(Any === t.ub) - return typeof_tfunc(t.ub) elseif isa(t, UnionAll) u = unwrap_unionall(t) - if isa(u, DataType) && !u.abstract + if isa(u, DataType) && !isabstracttype(u) if u.name === Tuple.name uu = typeof_concrete_vararg(u) if uu !== nothing @@ -517,43 +616,19 @@ function typeof_tfunc(@nospecialize(t)) end return DataType # typeof(anything)::DataType end -add_tfunc(typeof, 1, 1, typeof_tfunc, 0) - -function typeassert_type_instance(@nospecialize(v), @nospecialize(t)) - if isa(v, Const) - if !has_free_typevars(t) && !isa(v.val, t) - return Bottom - end - return v - elseif isa(v, PartialStruct) - has_free_typevars(t) && return v - widev = widenconst(v) - if widev <: t - return v - elseif typeintersect(widev, t) === Bottom - return Bottom - end - @assert widev <: Tuple - new_fields = Vector{Any}(undef, length(v.fields)) - for i = 1:length(new_fields) - new_fields[i] = typeassert_type_instance(v.fields[i], getfield_tfunc(t, Const(i))) - if new_fields[i] === Bottom - return Bottom - end - end - return tuple_tfunc(new_fields) - elseif isa(v, Conditional) - if !(Bool <: t) - return Bottom - end - return v +# helper function of `typeof_tfunc`, which accepts `TypeVar` +function _typeof_tfunc(@nospecialize(t)) + if isa(t, TypeVar) + return t.ub !== Any ? _typeof_tfunc(t.ub) : DataType end - return typeintersect(widenconst(v), t) + return typeof_tfunc(t) end +add_tfunc(typeof, 1, 1, typeof_tfunc, 1) + function typeassert_tfunc(@nospecialize(v), @nospecialize(t)) t = instanceof_tfunc(t)[1] t === Any && return v - return typeassert_type_instance(v, t) + return tmeet(v, t) end add_tfunc(typeassert, 2, 2, typeassert_tfunc, 4) @@ -562,9 +637,7 @@ function isa_tfunc(@nospecialize(v), @nospecialize(tt)) if t === Bottom # check if t could be equivalent to typeof(Bottom), since that's valid in `isa`, but the set of `v` is empty # if `t` cannot have instances, it's also invalid on the RHS of isa - if typeintersect(widenconst(tt), Type) === Union{} - return Union{} - end + hasintersect(widenconst(tt), Type) || return Union{} return Const(false) end if !has_free_typevars(t) @@ -580,7 +653,7 @@ function isa_tfunc(@nospecialize(v), @nospecialize(tt)) end v = widenconst(v) isdispatchelem(v) && return Const(false) - if typeintersect(v, t) === Bottom + if !hasintersect(v, t) # similar to `isnotbrokensubtype` check above, `typeintersect(v, t)` # can't be trusted for kind types so we do an extra check here if !iskindtype(v) @@ -592,7 +665,7 @@ function isa_tfunc(@nospecialize(v), @nospecialize(tt)) # TODO: handle non-leaftype(t) by testing against lower and upper bounds return Bool end -add_tfunc(isa, 2, 2, isa_tfunc, 0) +add_tfunc(isa, 2, 2, isa_tfunc, 1) function subtype_tfunc(@nospecialize(a), @nospecialize(b)) a, isexact_a = instanceof_tfunc(a) @@ -603,31 +676,14 @@ function subtype_tfunc(@nospecialize(a), @nospecialize(b)) return Const(true) end else - if isexact_a || (b !== Bottom && typeintersect(a, b) === Union{}) + if isexact_a || (b !== Bottom && !hasintersect(a, b)) return Const(false) end end end return Bool end -add_tfunc(<:, 2, 2, subtype_tfunc, 0) - -is_dt_const_field(fld::Int) = ( - fld == DATATYPE_NAME_FIELDINDEX || - fld == DATATYPE_PARAMETERS_FIELDINDEX || - fld == DATATYPE_TYPES_FIELDINDEX || - fld == DATATYPE_SUPER_FIELDINDEX || - fld == DATATYPE_MUTABLE_FIELDINDEX || - fld == DATATYPE_INSTANCE_FIELDINDEX - ) -function const_datatype_getfield_tfunc(@nospecialize(sv), fld::Int) - if fld == DATATYPE_INSTANCE_FIELDINDEX - return isdefined(sv, fld) ? Const(getfield(sv, fld)) : Union{} - elseif is_dt_const_field(fld) && isdefined(sv, fld) - return Const(getfield(sv, fld)) - end - return nothing -end +add_tfunc(<:, 2, 2, subtype_tfunc, 10) function fieldcount_noerror(@nospecialize t) if t isa UnionAll || t isa Union @@ -636,7 +692,7 @@ function fieldcount_noerror(@nospecialize t) return nothing end t = t::DataType - elseif t == Union{} + elseif t === Union{} return 0 end if !(t isa DataType) @@ -652,7 +708,7 @@ function fieldcount_noerror(@nospecialize t) end abstr = true else - abstr = t.abstract || (t.name === Tuple.name && isvatuple(t)) + abstr = isabstracttype(t) || (t.name === Tuple.name && isvatuple(t)) end if abstr return nothing @@ -665,7 +721,8 @@ function try_compute_fieldidx(typ::DataType, @nospecialize(field)) if isa(field, Symbol) field = fieldindex(typ, field, false) field == 0 && return nothing - elseif isa(field, Integer) + elseif isa(field, Int) + # Numerical field name can only be of type `Int` max_fields = fieldcount_noerror(typ) max_fields === nothing && return nothing (1 <= field <= max_fields) || return nothing @@ -675,15 +732,36 @@ function try_compute_fieldidx(typ::DataType, @nospecialize(field)) return field end +function getfield_boundscheck(argtypes::Vector{Any}) # ::Union{Bool, Nothing, Type{Bool}} + if length(argtypes) == 2 + boundscheck = Bool + elseif length(argtypes) == 3 + boundscheck = argtypes[3] + if boundscheck === Const(:not_atomic) # TODO: this is assuming not atomic + boundscheck = Bool + end + elseif length(argtypes) == 4 + boundscheck = argtypes[4] + else + return nothing + end + widenconst(boundscheck) !== Bool && return nothing + boundscheck = widenconditional(boundscheck) + if isa(boundscheck, Const) + return boundscheck.val + else + return Bool + end +end + function getfield_nothrow(argtypes::Vector{Any}) - 2 <= length(argtypes) <= 3 || return false - length(argtypes) == 2 && return getfield_nothrow(argtypes[1], argtypes[2], Const(true)) - return getfield_nothrow(argtypes[1], argtypes[2], argtypes[3]) -end -function getfield_nothrow(@nospecialize(s00), @nospecialize(name), @nospecialize(inbounds)) - bounds_check_disabled = isa(inbounds, Const) && inbounds.val === false - # If we don't have invounds and don't know the field, don't even bother - if !bounds_check_disabled + boundscheck = getfield_boundscheck(argtypes) + boundscheck === nothing && return false + return getfield_nothrow(argtypes[1], argtypes[2], !(boundscheck === false)) +end +function getfield_nothrow(@nospecialize(s00), @nospecialize(name), boundscheck::Bool) + # If we don't have boundscheck and don't know the field, don't even bother + if boundscheck isa(name, Const) || return false end @@ -695,13 +773,14 @@ function getfield_nothrow(@nospecialize(s00), @nospecialize(name), @nospecialize sv = s00.val end if isa(name, Const) - if !isa(name.val, Symbol) + nval = name.val + if !isa(nval, Symbol) isa(sv, Module) && return false - isa(name.val, Int) || return false + isa(nval, Int) || return false end - return isdefined(sv, name.val) + return isdefined(sv, nval) end - if bounds_check_disabled && !isa(sv, Module) + if !boundscheck && !isa(sv, Module) # If bounds checking is disabled and all fields are assigned, # we may assume that we don't throw for i = 1:fieldcount(typeof(sv)) @@ -712,101 +791,113 @@ function getfield_nothrow(@nospecialize(s00), @nospecialize(name), @nospecialize return false end - s = unwrap_unionall(widenconst(s00)) + s0 = widenconst(s00) + s = unwrap_unionall(s0) if isa(s, Union) - return getfield_nothrow(rewrap(s.a, s00), name, inbounds) && - getfield_nothrow(rewrap(s.b, s00), name, inbounds) + return getfield_nothrow(rewrap_unionall(s.a, s00), name, boundscheck) && + getfield_nothrow(rewrap_unionall(s.b, s00), name, boundscheck) elseif isa(s, DataType) # Can't say anything about abstract types - s.abstract && return false + isabstracttype(s) && return false + s.name.atomicfields == C_NULL || return false # TODO: currently we're only testing for ordering == :not_atomic # If all fields are always initialized, and bounds check is disabled, we can assume # we don't throw - if bounds_check_disabled && !isvatuple(s) && s.name !== NamedTuple.body.body.name && fieldcount(s) == s.ninitialized + if !boundscheck && s.name.n_uninitialized == 0 return true end # Else we need to know what the field is isa(name, Const) || return false field = try_compute_fieldidx(s, name.val) field === nothing && return false - field <= s.ninitialized && return true + field <= datatype_min_ninitialized(s) && return true + # `try_compute_fieldidx` already check for field index bound. + !isvatuple(s) && isbitstype(fieldtype(s0, field)) && return true end return false end -getfield_tfunc(@nospecialize(s00), @nospecialize(name), @nospecialize(inbounds)) = - getfield_tfunc(s00, name) -function getfield_tfunc(@nospecialize(s00), @nospecialize(name)) - s = unwrap_unionall(s00) - if isa(s, Union) - return tmerge(getfield_tfunc(rewrap(s.a,s00), name), - getfield_tfunc(rewrap(s.b,s00), name)) - elseif isa(s, Conditional) +function getfield_tfunc(s00, name, boundscheck_or_order) + @nospecialize + t = isvarargtype(boundscheck_or_order) ? unwrapva(boundscheck_or_order) : + widenconst(boundscheck_or_order) + hasintersect(t, Symbol) || hasintersect(t, Bool) || return Bottom + return getfield_tfunc(s00, name) +end +function getfield_tfunc(s00, name, order, boundscheck) + @nospecialize + hasintersect(widenconst(order), Symbol) || return Bottom + if isvarargtype(boundscheck) + t = unwrapva(boundscheck) + hasintersect(t, Symbol) || hasintersect(t, Bool) || return Bottom + else + hasintersect(widenconst(boundscheck), Bool) || return Bottom + end + return getfield_tfunc(s00, name) +end +getfield_tfunc(@nospecialize(s00), @nospecialize(name)) = _getfield_tfunc(s00, name, false) +function _getfield_tfunc(@nospecialize(s00), @nospecialize(name), setfield::Bool) + if isa(s00, Conditional) return Bottom # Bool has no fields - elseif isa(s, Const) || isconstType(s) - if !isa(s, Const) - sv = s.parameters[1] + elseif isa(s00, Const) || isconstType(s00) + if !isa(s00, Const) + sv = s00.parameters[1] else - sv = s.val + sv = s00.val end if isa(name, Const) nv = name.val - if !(isa(nv,Symbol) || isa(nv,Int)) + if isa(sv, Module) + setfield && return Bottom + if isa(nv, Symbol) + return abstract_eval_global(sv, nv) + end return Bottom end - if isa(sv, UnionAll) - if nv === :var || nv === 1 - return Const(sv.var) - elseif nv === :body || nv === 2 - return Const(sv.body) - end - elseif isa(sv, DataType) - idx = nv - if isa(idx, Symbol) - idx = fieldindex(DataType, idx, false) - end - if isa(idx, Int) - t = const_datatype_getfield_tfunc(sv, idx) - t === nothing || return t - end - elseif isa(sv, Core.TypeName) - fld = isa(nv, Symbol) ? fieldindex(Core.TypeName, nv, false) : nv - if (fld == TYPENAME_NAME_FIELDINDEX || - fld == TYPENAME_MODULE_FIELDINDEX || - fld == TYPENAME_WRAPPER_FIELDINDEX) - return Const(getfield(sv, fld)) - end + if isa(nv, Symbol) + nv = fieldindex(typeof(sv), nv, false) end - if isa(sv, Module) && isa(nv, Symbol) - return abstract_eval_global(sv, nv) + if !isa(nv, Int) + return Bottom end - if (isa(sv, SimpleVector) || !ismutable(sv)) && isdefined(sv, nv) + if isa(sv, DataType) && nv == DATATYPE_TYPES_FIELDINDEX && isdefined(sv, nv) return Const(getfield(sv, nv)) end + if isconst(typeof(sv), nv) + if isdefined(sv, nv) + return Const(getfield(sv, nv)) + end + return Union{} + end end s = typeof(sv) - elseif isa(s, PartialStruct) + elseif isa(s00, PartialStruct) + s = widenconst(s00) + sty = unwrap_unionall(s)::DataType if isa(name, Const) nv = name.val if isa(nv, Symbol) - nv = fieldindex(widenconst(s), nv, false) + nv = fieldindex(sty, nv, false) end - if isa(nv, Int) && 1 <= nv <= length(s.fields) - return s.fields[nv] + if isa(nv, Int) && 1 <= nv <= length(s00.fields) + return unwrapva(s00.fields[nv]) end end - s = widenconst(s) + else + s = unwrap_unionall(s00) end - if isType(s) || !isa(s, DataType) || s.abstract - return Any + if isa(s, Union) + return tmerge(_getfield_tfunc(rewrap_unionall(s.a, s00), name, setfield), + _getfield_tfunc(rewrap_unionall(s.b, s00), name, setfield)) end - if s <: Tuple && name ⊑ Symbol + isa(s, DataType) || return Any + isabstracttype(s) && return Any + if s <: Tuple && !(Int <: widenconst(name)) return Bottom end if s <: Module - if name ⊑ Int - return Bottom - end + setfield && return Bottom + hasintersect(widenconst(name), Symbol) || return Bottom return Any end if s.name === _NAMEDTUPLE_NAME && !isconcretetype(s) @@ -819,33 +910,36 @@ function getfield_tfunc(@nospecialize(s00), @nospecialize(name)) elseif Symbol ⊑ name name = Int end - _ts = s.parameters[2] - while isa(_ts, TypeVar) - _ts = _ts.ub - end + _ts = unwraptv(s.parameters[2]) _ts = rewrap_unionall(_ts, s00) if !(_ts <: Tuple) return Any end - return getfield_tfunc(_ts, name) + return _getfield_tfunc(_ts, name, setfield) end ftypes = datatype_fieldtypes(s) - if isempty(ftypes) + nf = length(ftypes) + # If no value has this type, then this statement should be unreachable. + # Bail quickly now. + if !has_concrete_subtype(s) || nf == 0 return Bottom end if isa(name, Conditional) return Bottom # can't index fields with Bool end if !isa(name, Const) + name = widenconst(name) if !(Int <: name || Symbol <: name) return Bottom end - if length(ftypes) == 1 + if nf == 1 return rewrap_unionall(unwrapva(ftypes[1]), s00) end # union together types of all fields t = Bottom - for _ft in ftypes + for i in 1:nf + _ft = ftypes[i] + setfield && isconst(s, i) && continue t = tmerge(t, rewrap_unionall(unwrapva(_ft), s00)) t === Any && break end @@ -858,23 +952,13 @@ function getfield_tfunc(@nospecialize(s00), @nospecialize(name)) if !isa(fld, Int) return Bottom end - nf = length(ftypes) if s <: Tuple && fld >= nf && isvarargtype(ftypes[nf]) return rewrap_unionall(unwrapva(ftypes[nf]), s00) end if fld < 1 || fld > nf return Bottom - end - if isconstType(s00) - sp = s00.parameters[1] - elseif isa(s00, Const) - sp = s00.val - else - sp = nothing - end - if isa(sp, DataType) - t = const_datatype_getfield_tfunc(sp, fld) - t !== nothing && return t + elseif setfield && isconst(s, fld) + return Bottom end R = ftypes[fld] if isempty(s.parameters) @@ -882,10 +966,127 @@ function getfield_tfunc(@nospecialize(s00), @nospecialize(name)) end return rewrap_unionall(R, s00) end -add_tfunc(getfield, 2, 3, getfield_tfunc, 1) -add_tfunc(setfield!, 3, 3, (@nospecialize(o), @nospecialize(f), @nospecialize(v)) -> v, 3) -fieldtype_tfunc(@nospecialize(s0), @nospecialize(name), @nospecialize(inbounds)) = - fieldtype_tfunc(s0, name) + +function setfield!_tfunc(o, f, v, order) + @nospecialize + if !isvarargtype(order) + hasintersect(widenconst(order), Symbol) || return Bottom + end + return setfield!_tfunc(o, f, v) +end +function setfield!_tfunc(o, f, v) + @nospecialize + mutability_errorcheck(o) || return Bottom + ft = _getfield_tfunc(o, f, true) + ft === Bottom && return Bottom + hasintersect(widenconst(v), widenconst(ft)) || return Bottom + return v +end +function mutability_errorcheck(@nospecialize obj) + objt0 = widenconst(obj) + objt = unwrap_unionall(objt0) + if isa(objt, Union) + return mutability_errorcheck(rewrap_unionall(objt.a, objt0)) || + mutability_errorcheck(rewrap_unionall(objt.b, objt0)) + elseif isa(objt, DataType) + # Can't say anything about abstract types + isabstracttype(objt) && return true + return ismutabletype(objt) + end + return true +end + +function setfield!_nothrow(argtypes::Vector{Any}) + if length(argtypes) == 4 + order = argtypes[4] + order === Const(:not_atomic) || return false # currently setfield!_nothrow is assuming not atomic + else + length(argtypes) == 3 || return false + end + return setfield!_nothrow(argtypes[1], argtypes[2], argtypes[3]) +end +function setfield!_nothrow(s00, name, v) + @nospecialize + s0 = widenconst(s00) + s = unwrap_unionall(s0) + if isa(s, Union) + return setfield!_nothrow(rewrap_unionall(s.a, s00), name, v) && + setfield!_nothrow(rewrap_unionall(s.b, s00), name, v) + elseif isa(s, DataType) + # Can't say anything about abstract types + isabstracttype(s) && return false + ismutabletype(s) || return false + s.name.atomicfields == C_NULL || return false # TODO: currently we're only testing for ordering == :not_atomic + isa(name, Const) || return false + field = try_compute_fieldidx(s, name.val) + field === nothing && return false + # `try_compute_fieldidx` already check for field index bound. + isconst(s, field) && return false + v_expected = fieldtype(s0, field) + return v ⊑ v_expected + end + return false +end + +swapfield!_tfunc(o, f, v, order) = (@nospecialize; getfield_tfunc(o, f)) +swapfield!_tfunc(o, f, v) = (@nospecialize; getfield_tfunc(o, f)) +modifyfield!_tfunc(o, f, op, v, order) = (@nospecialize; modifyfield!_tfunc(o, f, op, v)) +function modifyfield!_tfunc(o, f, op, v) + @nospecialize + T = _fieldtype_tfunc(o, isconcretetype(o), f) + T === Bottom && return Bottom + PT = Const(Pair) + return instanceof_tfunc(apply_type_tfunc(PT, T, T))[1] +end +function abstract_modifyfield!(interp::AbstractInterpreter, argtypes::Vector{Any}, sv::InferenceState) + nargs = length(argtypes) + if !isempty(argtypes) && isvarargtype(argtypes[nargs]) + nargs - 1 <= 6 || return CallMeta(Bottom, false) + nargs > 3 || return CallMeta(Any, false) + else + 5 <= nargs <= 6 || return CallMeta(Bottom, false) + end + o = unwrapva(argtypes[2]) + f = unwrapva(argtypes[3]) + RT = modifyfield!_tfunc(o, f, Any, Any) + info = false + if nargs >= 5 && RT !== Bottom + # we may be able to refine this to a PartialStruct by analyzing `op(o.f, v)::T` + # as well as compute the info for the method matches + op = unwrapva(argtypes[4]) + v = unwrapva(argtypes[5]) + TF = getfield_tfunc(o, f) + push!(sv.ssavalue_uses[sv.currpc], sv.currpc) # temporarily disable `call_result_unused` check for this call + callinfo = abstract_call(interp, ArgInfo(nothing, Any[op, TF, v]), sv, #=max_methods=# 1) + pop!(sv.ssavalue_uses[sv.currpc], sv.currpc) + TF2 = tmeet(callinfo.rt, widenconst(TF)) + if TF2 === Bottom + RT = Bottom + elseif isconcretetype(RT) && has_nontrivial_const_info(TF2) # isconcrete condition required to form a PartialStruct + RT = PartialStruct(RT, Any[TF, TF2]) + end + info = callinfo.info + end + return CallMeta(RT, info) +end +replacefield!_tfunc(o, f, x, v, success_order, failure_order) = (@nospecialize; replacefield!_tfunc(o, f, x, v)) +replacefield!_tfunc(o, f, x, v, success_order) = (@nospecialize; replacefield!_tfunc(o, f, x, v)) +function replacefield!_tfunc(o, f, x, v) + @nospecialize + T = _fieldtype_tfunc(o, isconcretetype(o), f) + T === Bottom && return Bottom + PT = Const(ccall(:jl_apply_cmpswap_type, Any, (Any,), T) where T) + return instanceof_tfunc(apply_type_tfunc(PT, T))[1] +end + +# we could use tuple_tfunc instead of widenconst, but `o` is mutable, so that is unlikely to be beneficial + +add_tfunc(getfield, 2, 4, getfield_tfunc, 1) +add_tfunc(setfield!, 3, 4, setfield!_tfunc, 3) + +add_tfunc(swapfield!, 3, 4, swapfield!_tfunc, 3) +add_tfunc(modifyfield!, 4, 5, modifyfield!_tfunc, 3) +add_tfunc(replacefield!, 4, 6, replacefield!_tfunc, 3) function fieldtype_nothrow(@nospecialize(s0), @nospecialize(name)) s0 === Bottom && return true # unreachable @@ -919,7 +1120,7 @@ function _fieldtype_nothrow(@nospecialize(s), exact::Bool, name::Const) return exact ? (a || b) : (a && b) end u isa DataType || return false - u.abstract && return false + isabstracttype(u) && return false if u.name === _NAMEDTUPLE_NAME && !isconcretetype(u) # TODO: better approximate inference return false @@ -944,25 +1145,28 @@ function _fieldtype_nothrow(@nospecialize(s), exact::Bool, name::Const) return true end +fieldtype_tfunc(s0, name, boundscheck) = (@nospecialize; fieldtype_tfunc(s0, name)) function fieldtype_tfunc(@nospecialize(s0), @nospecialize(name)) if s0 === Bottom return Bottom end if s0 === Any || s0 === Type || DataType ⊑ s0 || UnionAll ⊑ s0 - return Type + # For a generic DataType, one of the fields could still be a TypeVar + # which is not a Type. Tuple{...} can also contain Symbols etc. + return Any end - # fieldtype only accepts Types, errors on `Module` - if isa(s0, Const) && (!(isa(s0.val, DataType) || isa(s0.val, UnionAll) || isa(s0.val, Union)) || s0.val === Module) + # fieldtype only accepts Types + if isa(s0, Const) && !(isa(s0.val, DataType) || isa(s0.val, UnionAll) || isa(s0.val, Union)) return Bottom end - if (s0 isa Type && (s0 == Type{Module} || s0 == Type{Union{}})) || isa(s0, Conditional) + if (s0 isa Type && s0 == Type{Union{}}) || isa(s0, Conditional) return Bottom end su = unwrap_unionall(s0) if isa(su, Union) - return tmerge(fieldtype_tfunc(rewrap(su.a, s0), name), - fieldtype_tfunc(rewrap(su.b, s0), name)) + return tmerge(fieldtype_tfunc(rewrap_unionall(su.a, s0), name), + fieldtype_tfunc(rewrap_unionall(su.b, s0), name)) end s, exact = instanceof_tfunc(s0) @@ -974,14 +1178,32 @@ function _fieldtype_tfunc(@nospecialize(s), exact::Bool, @nospecialize(name)) exact = exact && !has_free_typevars(s) u = unwrap_unionall(s) if isa(u, Union) - return tmerge(_fieldtype_tfunc(rewrap(u.a, s), exact, name), - _fieldtype_tfunc(rewrap(u.b, s), exact, name)) + ta0 = _fieldtype_tfunc(rewrap_unionall(u.a, s), exact, name) + tb0 = _fieldtype_tfunc(rewrap_unionall(u.b, s), exact, name) + ta0 ⊑ tb0 && return tb0 + tb0 ⊑ ta0 && return ta0 + ta, exacta, _, istypea = instanceof_tfunc(ta0) + tb, exactb, _, istypeb = instanceof_tfunc(tb0) + if exact && exacta && exactb + return Const(Union{ta, tb}) + end + if istypea && istypeb + return Type{<:Union{ta, tb}} + end + return Any + end + u isa DataType || return Any + if isabstracttype(u) + # Abstract types have no fields + exact && return Bottom + # Type{...} without free typevars has no subtypes, so it is actually + # exact, even if `exact` is false. + isType(u) && !has_free_typevars(u.parameters[1]) && return Bottom + return Any end - u isa DataType || return Type - u.abstract && return Type if u.name === _NAMEDTUPLE_NAME && !isconcretetype(u) # TODO: better approximate inference - return Type + return Union{Type, TypeVar} end ftypes = datatype_fieldtypes(u) if isempty(ftypes) @@ -1004,8 +1226,15 @@ function _fieldtype_tfunc(@nospecialize(s), exact::Bool, @nospecialize(name)) else ft1 = Type{ft1} end + elseif ft1 isa Type || ft1 isa TypeVar + if ft1 === Any && u.name === Tuple.name + # Tuple{:x} is possible in this case + ft1 = Any + else + ft1 = Type{ft} where ft<:ft1 + end else - ft1 = Type{ft} where ft<:ft1 + ft1 = Const(ft1) end t = tmerge(t, ft1) t === Any && break @@ -1028,6 +1257,9 @@ function _fieldtype_tfunc(@nospecialize(s), exact::Bool, @nospecialize(name)) else ft = ftypes[fld] end + if !isa(ft, Type) && !isa(ft, TypeVar) + return Const(ft) + end exactft = exact || (!has_free_typevars(ft) && u.name !== Tuple.name) ft = rewrap_unionall(ft, s) @@ -1037,10 +1269,30 @@ function _fieldtype_tfunc(@nospecialize(s), exact::Bool, @nospecialize(name)) end return Type{ft} end + if u.name === Tuple.name && ft === Any + # Tuple{:x} is possible + return Any + end return Type{<:ft} end add_tfunc(fieldtype, 2, 3, fieldtype_tfunc, 0) +# Like `valid_tparam`, but in the type domain. +function valid_tparam_type(T::DataType) + T === Symbol && return true + isbitstype(T) && return true + if T <: Tuple + isconcretetype(T) || return false + for P in T.parameters + (P === Symbol || isbitstype(P)) || return false + end + return true + end + return false +end +valid_tparam_type(U::Union) = valid_tparam_type(U.a) && valid_tparam_type(U.b) +valid_tparam_type(U::UnionAll) = valid_tparam_type(unwrap_unionall(U)) + function apply_type_nothrow(argtypes::Array{Any, 1}, @nospecialize(rt)) rt === Type && return false length(argtypes) >= 1 || return false @@ -1052,7 +1304,7 @@ function apply_type_nothrow(argtypes::Array{Any, 1}, @nospecialize(rt)) else return false end - # We know the apply_type is well formed. Oherwise our rt would have been + # We know the apply_type is well formed. Otherwise our rt would have been # Bottom (or Type). (headtype === Union) && return true isa(rt, Const) && return true @@ -1060,14 +1312,14 @@ function apply_type_nothrow(argtypes::Array{Any, 1}, @nospecialize(rt)) for i = 2:length(argtypes) isa(u, UnionAll) || return false ai = widenconditional(argtypes[i]) - if ai ⊑ TypeVar + if ai ⊑ TypeVar || ai === DataType # We don't know anything about the bounds of this typevar, but as # long as the UnionAll is not constrained, that's ok. if !(u.var.lb === Union{} && u.var.ub === Any) return false end - elseif isa(ai, Const) && isa(ai.val, Type) - ai = ai.val + elseif (isa(ai, Const) && isa(ai.val, Type)) || isconstType(ai) + ai = isa(ai, Const) ? ai.val : (ai::DataType).parameters[1] if has_free_typevars(u.var.lb) || has_free_typevars(u.var.ub) return false end @@ -1075,7 +1327,23 @@ function apply_type_nothrow(argtypes::Array{Any, 1}, @nospecialize(rt)) return false end else - return false + T, exact, _, istype = instanceof_tfunc(ai) + if T === Bottom + if !(u.var.lb === Union{} && u.var.ub === Any) + return false + end + if !valid_tparam_type(widenconst(ai)) + return false + end + else + istype || return false + if !(T <: u.var.ub) + return false + end + if exact ? !(u.var.lb <: T) : !(u.var.lb === Bottom) + return false + end + end end u = u.body end @@ -1092,10 +1360,10 @@ function apply_type_tfunc(@nospecialize(headtypetype), @nospecialize args...) elseif isconstType(headtypetype) headtype = headtypetype.parameters[1] else - return Type + return Any end if !isempty(args) && isvarargtype(args[end]) - return Type + return isvarargtype(headtype) ? TypeofVararg : Type end largs = length(args) if headtype === Union @@ -1113,7 +1381,7 @@ function apply_type_tfunc(@nospecialize(headtypetype), @nospecialize args...) end else if !isType(ai) - if !isa(ai, Type) || typeintersect(ai, Type) !== Bottom || typeintersect(ai, TypeVar) !== Bottom + if !isa(ai, Type) || hasintersect(ai, Type) || hasintersect(ai, TypeVar) hasnonType = true else return Bottom @@ -1121,7 +1389,11 @@ function apply_type_tfunc(@nospecialize(headtypetype), @nospecialize args...) end end end - largs == 1 && return isa(args[1], Type) ? typeintersect(args[1], Type) : Type + if largs == 1 # Union{T} --> T + u1 = typeintersect(widenconst(args[1]), Type) + valid_as_lattice(u1) || return Bottom + return u1 + end hasnonType && return Type ty = Union{} allconst = true @@ -1137,16 +1409,15 @@ function apply_type_tfunc(@nospecialize(headtypetype), @nospecialize args...) end return allconst ? Const(ty) : Type{ty} end - istuple = (headtype == Tuple) - if !istuple && !isa(headtype, UnionAll) + istuple = isa(headtype, Type) && (headtype == Tuple) + if !istuple && !isa(headtype, UnionAll) && !isvarargtype(headtype) return Union{} end uw = unwrap_unionall(headtype) - isnamedtuple = isa(uw, DataType) && uw.name === _NAMEDTUPLE_NAME uncertain = false canconst = true tparams = Any[] - outervars = Any[] + outervars = TypeVar[] varnamectr = 1 ua = headtype for i = 1:largs @@ -1155,7 +1426,8 @@ function apply_type_tfunc(@nospecialize(headtypetype), @nospecialize args...) aip1 = ai.parameters[1] canconst &= !has_free_typevars(aip1) push!(tparams, aip1) - elseif isa(ai, Const) && (isa(ai.val, Type) || isa(ai.val, TypeVar) || valid_tparam(ai.val)) + elseif isa(ai, Const) && (isa(ai.val, Type) || isa(ai.val, TypeVar) || + valid_tparam(ai.val) || (istuple && isvarargtype(ai.val))) push!(tparams, ai.val) elseif isa(ai, PartialTypeVar) canconst = false @@ -1192,7 +1464,7 @@ function apply_type_tfunc(@nospecialize(headtypetype), @nospecialize args...) # end else # Is this the second parameter to a NamedTuple? - if isnamedtuple && isa(ua, UnionAll) && uw.parameters[2] === ua.var + if isa(uw, DataType) && uw.name === _NAMEDTUPLE_NAME && isa(ua, UnionAll) && uw.parameters[2] === ua.var # If the names are known, keep the upper bound, but otherwise widen to Tuple. # This is a widening heuristic to avoid keeping type information # that's unlikely to be useful. @@ -1221,11 +1493,11 @@ function apply_type_tfunc(@nospecialize(headtypetype), @nospecialize args...) catch ex # type instantiation might fail if one of the type parameters # doesn't match, which could happen if a type estimate is too coarse - return Type{<:headtype} + return isvarargtype(headtype) ? TypeofVararg : Type{<:headtype} end !uncertain && canconst && return Const(appl) - if isvarargtype(headtype) - return Type + if isvarargtype(appl) + return TypeofVararg end if istuple return Type{<:appl} @@ -1238,58 +1510,58 @@ function apply_type_tfunc(@nospecialize(headtypetype), @nospecialize args...) end add_tfunc(apply_type, 1, INT_INF, apply_type_tfunc, 10) -function invoke_tfunc(interp::AbstractInterpreter, @nospecialize(ft), @nospecialize(types), @nospecialize(argtype), sv::InferenceState) - argtype = typeintersect(types, argtype) - argtype === Bottom && return Bottom - argtype isa DataType || return Any # other cases are not implemented below - isdispatchelem(ft) || return Any # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below - types = rewrap_unionall(Tuple{ft, unwrap_unionall(types).parameters...}, types) - argtype = Tuple{ft, argtype.parameters...} - result = findsup(types, method_table(interp)) - if result === nothing - return Any - end - method, valid_worlds = result - update_valid_age!(sv, valid_worlds) - (ti, env) = ccall(:jl_type_intersection_with_env, Any, (Any, Any), argtype, method.sig)::SimpleVector - rt, edge = typeinf_edge(interp, method, ti, env, sv) - edge !== nothing && add_backedge!(edge::MethodInstance, sv) - return rt +function has_struct_const_info(x) + isa(x, PartialTypeVar) && return true + isa(x, Conditional) && return true + return has_nontrivial_const_info(x) end # convert the dispatch tuple type argtype to the real (concrete) type of # the tuple of those values -function tuple_tfunc(atypes::Vector{Any}) - atypes = anymap(widenconditional, atypes) +function tuple_tfunc(argtypes::Vector{Any}) + argtypes = anymap(widenconditional, argtypes) all_are_const = true - for i in 1:length(atypes) - if !isa(atypes[i], Const) + for i in 1:length(argtypes) + if !isa(argtypes[i], Const) all_are_const = false break end end if all_are_const - return Const(ntuple(i -> atypes[i].val, length(atypes))) + return Const(ntuple(i -> argtypes[i].val, length(argtypes))) end - params = Vector{Any}(undef, length(atypes)) + params = Vector{Any}(undef, length(argtypes)) anyinfo = false - for i in 1:length(atypes) - x = atypes[i] - # TODO ignore singleton Const (don't forget to update cache logic if you implement this) - if !anyinfo - anyinfo = !isa(x, Type) || isType(x) + for i in 1:length(argtypes) + x = argtypes[i] + if has_struct_const_info(x) + anyinfo = true + else + if !isvarargtype(x) + x = widenconst(x) + end + argtypes[i] = x end if isa(x, Const) params[i] = typeof(x.val) else - x = widenconst(x) + x = isvarargtype(x) ? x : widenconst(x) + # since there don't exist any values whose runtime type are `Tuple{Type{...}}`, + # here we should turn such `Type{...}`-parameters to valid parameters, e.g. + # (::Type{Int},) -> Tuple{DataType} (or PartialStruct for more accuracy) + # (::Union{Type{Int32},Type{Int64}}) -> Tuple{Type} if isType(x) + anyinfo = true xparam = x.parameters[1] if hasuniquerep(xparam) || xparam === Bottom params[i] = typeof(xparam) else params[i] = Type end + elseif iskindtype(x) + params[i] = x + elseif !isvarargtype(x) && hasintersect(x, Type) + params[i] = Union{x, Type} else params[i] = x end @@ -1298,72 +1570,136 @@ function tuple_tfunc(atypes::Vector{Any}) typ = Tuple{params...} # replace a singleton type with its equivalent Const object isdefined(typ, :instance) && return Const(typ.instance) - return anyinfo ? PartialStruct(typ, atypes) : typ -end - -function arrayref_tfunc(@nospecialize(boundscheck), @nospecialize(a), @nospecialize i...) - a = widenconst(a) - if a <: Array - if isa(a, DataType) && (isa(a.parameters[1], Type) || isa(a.parameters[1], TypeVar)) - # TODO: the TypeVar case should not be needed here - a = a.parameters[1] - return isa(a, TypeVar) ? a.ub : a - elseif isa(a, UnionAll) && !has_free_typevars(a) - unw = unwrap_unionall(a) - if isa(unw, DataType) - return rewrap_unionall(unw.parameters[1], a) - end + return anyinfo ? PartialStruct(typ, argtypes) : typ +end + +arrayref_tfunc(@nospecialize(boundscheck), @nospecialize(ary), @nospecialize idxs...) = + _arrayref_tfunc(boundscheck, ary, idxs) +function _arrayref_tfunc(@nospecialize(boundscheck), @nospecialize(ary), + @nospecialize idxs::Tuple) + isempty(idxs) && return Bottom + array_builtin_common_errorcheck(boundscheck, ary, idxs) || return Bottom + return array_elmtype(ary) +end +add_tfunc(arrayref, 3, INT_INF, arrayref_tfunc, 20) +add_tfunc(const_arrayref, 3, INT_INF, arrayref_tfunc, 20) + +function arrayset_tfunc(@nospecialize(boundscheck), @nospecialize(ary), @nospecialize(item), + @nospecialize idxs...) + hasintersect(widenconst(item), _arrayref_tfunc(boundscheck, ary, idxs)) || return Bottom + return ary +end +add_tfunc(arrayset, 4, INT_INF, arrayset_tfunc, 20) + +function array_builtin_common_errorcheck(@nospecialize(boundscheck), @nospecialize(ary), + @nospecialize idxs::Tuple) + hasintersect(widenconst(boundscheck), Bool) || return false + hasintersect(widenconst(ary), Array) || return false + for i = 1:length(idxs) + idx = getfield(idxs, i) + idx = isvarargtype(idx) ? unwrapva(idx) : widenconst(idx) + hasintersect(idx, Int) || return false + end + return true +end + +function array_elmtype(@nospecialize ary) + a = widenconst(ary) + if !has_free_typevars(a) && a <: Array + a0 = a + if isa(a, UnionAll) + a = unwrap_unionall(a0) + end + if isa(a, DataType) + T = a.parameters[1] + valid_as_lattice(T) || return Bottom + return rewrap_unionall(T, a0) end end return Any end -add_tfunc(arrayref, 3, INT_INF, arrayref_tfunc, 20) -add_tfunc(const_arrayref, 3, INT_INF, arrayref_tfunc, 20) -add_tfunc(arrayset, 4, INT_INF, (@nospecialize(boundscheck), @nospecialize(a), @nospecialize(v), @nospecialize i...)->a, 20) -function array_type_undefable(@nospecialize(a)) - if isa(a, Union) - return array_type_undefable(a.a) || array_type_undefable(a.b) - elseif isa(a, UnionAll) +function _opaque_closure_tfunc(@nospecialize(arg), @nospecialize(lb), @nospecialize(ub), + @nospecialize(source), env::Vector{Any}, linfo::MethodInstance) + + argt, argt_exact = instanceof_tfunc(arg) + lbt, lb_exact = instanceof_tfunc(lb) + if !lb_exact + lbt = Union{} + end + + ubt, ub_exact = instanceof_tfunc(ub) + + t = (argt_exact ? Core.OpaqueClosure{argt, T} : Core.OpaqueClosure{<:argt, T}) where T + t = lbt == ubt ? t{ubt} : (t{T} where lbt <: T <: ubt) + + (isa(source, Const) && isa(source.val, Method)) || return t + + return PartialOpaque(t, tuple_tfunc(env), linfo, source.val) +end + +# whether getindex for the elements can potentially throw UndefRef +function array_type_undefable(@nospecialize(arytype)) + if isa(arytype, Union) + return array_type_undefable(arytype.a) || array_type_undefable(arytype.b) + elseif isa(arytype, UnionAll) return true else - etype = (a::DataType).parameters[1] - return !(etype isa Type && (isbitstype(etype) || isbitsunion(etype))) + elmtype = (arytype::DataType).parameters[1] + return !(elmtype isa Type && (isbitstype(elmtype) || isbitsunion(elmtype))) end end -function array_builtin_common_nothrow(argtypes::Array{Any,1}, first_idx_idx::Int) +function array_builtin_common_nothrow(argtypes::Vector{Any}, first_idx_idx::Int) length(argtypes) >= 4 || return false - atype = argtypes[2] - (argtypes[1] ⊑ Bool && atype ⊑ Array) || return false - for i = first_idx_idx:length(argtypes) - argtypes[i] ⊑ Int || return false - end + boundscheck = argtypes[1] + arytype = argtypes[2] + array_builtin_common_typecheck(boundscheck, arytype, argtypes, first_idx_idx) || return false # If we could potentially throw undef ref errors, bail out now. - atype = widenconst(atype) - array_type_undefable(atype) && return false + arytype = widenconst(arytype) + array_type_undefable(arytype) && return false # If we have @inbounds (first argument is false), we're allowed to assume # we don't throw bounds errors. - (isa(argtypes[1], Const) && !argtypes[1].val) && return true + if isa(boundscheck, Const) + !(boundscheck.val::Bool) && return true + end # Else we can't really say anything here # TODO: In the future we may be able to track the shapes of arrays though # inference. return false end +function array_builtin_common_typecheck( + @nospecialize(boundscheck), @nospecialize(arytype), + argtypes::Vector{Any}, first_idx_idx::Int) + (boundscheck ⊑ Bool && arytype ⊑ Array) || return false + for i = first_idx_idx:length(argtypes) + argtypes[i] ⊑ Int || return false + end + return true +end + +function arrayset_typecheck(@nospecialize(arytype), @nospecialize(elmtype)) + # Check that we can determine the element type + arytype = widenconst(arytype) + isa(arytype, DataType) || return false + elmtype_expected = arytype.parameters[1] + isa(elmtype_expected, Type) || return false + # Check that the element type is compatible with the element we're assigning + elmtype ⊑ elmtype_expected || return false + return true +end + # Query whether the given builtin is guaranteed not to throw given the argtypes function _builtin_nothrow(@nospecialize(f), argtypes::Array{Any,1}, @nospecialize(rt)) if f === arrayset array_builtin_common_nothrow(argtypes, 4) || return true # Additionally check element type compatibility - a = widenconst(argtypes[2]) - # Check that we can determine the element type - (isa(a, DataType) && isa(a.parameters[1], Type)) || return false - # Check that the element type is compatible with the element we're assigning - (argtypes[3] ⊑ a.parameters[1]::Type) || return false - return true + return arrayset_typecheck(argtypes[2], argtypes[3]) elseif f === arrayref || f === const_arrayref return array_builtin_common_nothrow(argtypes, 3) + elseif f === arraysize + return arraysize_nothrow(argtypes) elseif f === Core._expr length(argtypes) >= 1 || return false return argtypes[1] ⊑ Symbol @@ -1399,10 +1735,107 @@ function _builtin_nothrow(@nospecialize(f), argtypes::Array{Any,1}, @nospecializ elseif f === Core.ifelse length(argtypes) == 3 || return false return argtypes[1] ⊑ Bool + elseif f === typeassert + length(argtypes) == 2 || return false + a3 = argtypes[2] + if (isType(a3) && !has_free_typevars(a3) && argtypes[1] ⊑ a3.parameters[1]) || + (isa(a3, Const) && isa(a3.val, Type) && argtypes[1] ⊑ a3.val) + return true + end + return false + elseif f === getglobal + return getglobal_nothrow(argtypes) + elseif f === Core.get_binding_type + length(argtypes) == 2 || return false + return argtypes[1] ⊑ Module && argtypes[2] ⊑ Symbol + elseif f === donotdelete + return true end return false end +# known to be always effect-free (in particular nothrow) +const _PURE_BUILTINS = Any[tuple, svec, ===, typeof, nfields] + +# known to be effect-free (but not necessarily nothrow) +const _EFFECT_FREE_BUILTINS = [ + fieldtype, apply_type, isa, UnionAll, + getfield, arrayref, const_arrayref, isdefined, Core.sizeof, + Core.kwfunc, Core.ifelse, Core._typevar, (<:), + typeassert, throw, arraysize, getglobal, +] + +const _CONSISTENT_BUILTINS = Any[ + tuple, # tuple is immutable, thus tuples of egal arguments are egal + ===, + typeof, + nfields, + fieldtype, + apply_type, + isa, + UnionAll, + Core.sizeof, + Core.kwfunc, + Core.ifelse, + (<:), + typeassert, + throw +] + +const _SPECIAL_BUILTINS = Any[ + Core._apply_iterate +] + +function builtin_effects(f::Builtin, argtypes::Vector{Any}, rt) + if isa(f, IntrinsicFunction) + return intrinsic_effects(f, argtypes) + end + + @assert !contains_is(_SPECIAL_BUILTINS, f) + + nothrow = false + if (f === Core.getfield || f === Core.isdefined) && length(argtypes) >= 3 + # consistent if the argtype is immutable + if isvarargtype(argtypes[2]) + return Effects(; effect_free=ALWAYS_TRUE, terminates=ALWAYS_TRUE, nonoverlayed=true) + end + s = widenconst(argtypes[2]) + if isType(s) || !isa(s, DataType) || isabstracttype(s) + return Effects(; effect_free=ALWAYS_TRUE, terminates=ALWAYS_TRUE, nonoverlayed=true) + end + s = s::DataType + ipo_consistent = !ismutabletype(s) + nothrow = false + if f === Core.getfield && !isvarargtype(argtypes[end]) && + getfield_boundscheck(argtypes[2:end]) !== true + # If we cannot independently prove inboundsness, taint consistency. + # The inbounds-ness assertion requires dynamic reachability, while + # :consistent needs to be true for all input values. + # N.B. We do not taint for `--check-bounds=no` here -that happens in + # InferenceState. + nothrow = getfield_nothrow(argtypes[2], argtypes[3], true) + ipo_consistent &= nothrow + else + nothrow = isvarargtype(argtypes[end]) ? false : + builtin_nothrow(f, argtypes[2:end], rt) + end + effect_free = true + elseif f === getglobal && length(argtypes) >= 3 + nothrow = getglobal_nothrow(argtypes[2:end]) + ipo_consistent = nothrow && isconst((argtypes[2]::Const).val, (argtypes[3]::Const).val) + effect_free = true + else + ipo_consistent = contains_is(_CONSISTENT_BUILTINS, f) + effect_free = contains_is(_EFFECT_FREE_BUILTINS, f) || contains_is(_PURE_BUILTINS, f) + nothrow = isvarargtype(argtypes[end]) ? false : builtin_nothrow(f, argtypes[2:end], rt) + end + + return Effects(EFFECTS_TOTAL; + consistent = ipo_consistent ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + effect_free = effect_free ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + nothrow = nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN) +end + function builtin_nothrow(@nospecialize(f), argtypes::Array{Any, 1}, @nospecialize(rt)) rt === Bottom && return false contains_is(_PURE_BUILTINS, f) && return true @@ -1413,30 +1846,10 @@ function builtin_tfunction(interp::AbstractInterpreter, @nospecialize(f), argtyp sv::Union{InferenceState,Nothing}) if f === tuple return tuple_tfunc(argtypes) - elseif f === invoke - if length(argtypes) > 1 && sv !== nothing && (isa(argtypes[1], Const) || isa(argtypes[1], Type)) - if isa(argtypes[1], Const) - ft = Core.Typeof(argtypes[1].val) - else - ft = argtypes[1] - end - sig = argtypes[2] - if isa(sig, Const) - sigty = sig.val - elseif isType(sig) - sigty = sig.parameters[1] - else - sigty = nothing - end - if isa(sigty, Type) && !has_free_typevars(sigty) && sigty <: Tuple - return invoke_tfunc(interp, ft, sigty, argtypes_to_type(argtypes[3:end]), sv) - end - end - return Any end if isa(f, IntrinsicFunction) if is_pure_intrinsic_infer(f) && _all(@nospecialize(a) -> isa(a, Const), argtypes) - argvals = anymap(a::Const -> a.val, argtypes) + argvals = anymap(@nospecialize(a) -> (a::Const).val, argtypes) try return Const(f(argvals...)) catch @@ -1465,7 +1878,7 @@ function builtin_tfunction(interp::AbstractInterpreter, @nospecialize(f), argtyp if length(argtypes) - 1 == tf[2] argtypes = argtypes[1:end-1] else - vatype = argtypes[end] + vatype = argtypes[end]::TypeofVararg argtypes = argtypes[1:end-1] while length(argtypes) < tf[1] push!(argtypes, unwrapva(vatype)) @@ -1483,6 +1896,10 @@ end # Query whether the given intrinsic is nothrow +_iszero(x) = x === Intrinsics.xor_int(x, x) +_isneg1(x) = _iszero(Intrinsics.not_int(x)) +_istypemin(x) = !_iszero(x) && Intrinsics.neg_int(x) === x + function intrinsic_nothrow(f::IntrinsicFunction, argtypes::Array{Any, 1}) # First check that we have the correct number of arguments iidx = Int(reinterpret(Int32, f::IntrinsicFunction)) + 1 @@ -1509,11 +1926,10 @@ function intrinsic_nothrow(f::IntrinsicFunction, argtypes::Array{Any, 1}) return false end den_val = argtypes[2].val - den_val !== zero(typeof(den_val)) || return false + _iszero(den_val) && return false f !== Intrinsics.checked_sdiv_int && return true # Nothrow as long as we additionally don't do typemin(T)/-1 - return den_val !== -1 || (isa(argtypes[1], Const) && - argtypes[1].val !== typemin(typeof(den_val))) + return !_isneg1(den_val) || (isa(argtypes[1], Const) && !_istypemin(argtypes[1].val)) end if f === Intrinsics.pointerref # Nothrow as long as the types are ok. N.B.: dereferencability is not @@ -1531,18 +1947,22 @@ function intrinsic_nothrow(f::IntrinsicFunction, argtypes::Array{Any, 1}) return argtypes[1] ⊑ Array end if f === Intrinsics.bitcast - ty = instanceof_tfunc(argtypes[1])[1] + ty, isexact, isconcrete = instanceof_tfunc(argtypes[1]) xty = widenconst(argtypes[2]) - return isprimitivetype(ty) && isprimitivetype(xty) && ty.size === xty.size + return isconcrete && isprimitivetype(ty) && isprimitivetype(xty) && Core.sizeof(ty) === Core.sizeof(xty) end if f in (Intrinsics.sext_int, Intrinsics.zext_int, Intrinsics.trunc_int, Intrinsics.fptoui, Intrinsics.fptosi, Intrinsics.uitofp, Intrinsics.sitofp, Intrinsics.fptrunc, Intrinsics.fpext) - # If !isexact, `ty` may be Union{} at runtime even if we have + # If !isconcrete, `ty` may be Union{} at runtime even if we have # isprimitivetype(ty). - ty, isexact = instanceof_tfunc(argtypes[1]) + ty, isexact, isconcrete = instanceof_tfunc(argtypes[1]) xty = widenconst(argtypes[2]) - return isexact && isprimitivetype(ty) && isprimitivetype(xty) + return isconcrete && isprimitivetype(ty) && isprimitivetype(xty) + end + if f === Intrinsics.have_fma + ty, isexact, isconcrete = instanceof_tfunc(argtypes[1]) + return isconcrete && isprimitivetype(ty) end # The remaining intrinsics are math/bits/comparison intrinsics. They work on all # primitive types of the same type. @@ -1558,6 +1978,42 @@ function intrinsic_nothrow(f::IntrinsicFunction, argtypes::Array{Any, 1}) return true end +# whether `f` is pure for inference +function is_pure_intrinsic_infer(f::IntrinsicFunction) + return !(f === Intrinsics.pointerref || # this one is volatile + f === Intrinsics.pointerset || # this one is never effect-free + f === Intrinsics.llvmcall || # this one is never effect-free + f === Intrinsics.arraylen || # this one is volatile + f === Intrinsics.sqrt_llvm_fast || # this one may differ at runtime (by a few ulps) + f === Intrinsics.have_fma || # this one depends on the runtime environment + f === Intrinsics.cglobal) # cglobal lookup answer changes at runtime +end + +# whether `f` is effect free if nothrow +intrinsic_effect_free_if_nothrow(f) = f === Intrinsics.pointerref || + f === Intrinsics.have_fma || is_pure_intrinsic_infer(f) + +function intrinsic_effects(f::IntrinsicFunction, argtypes::Vector{Any}) + if f === Intrinsics.llvmcall + # llvmcall can do arbitrary things + return Effects() + end + + ipo_consistent = !( + f === Intrinsics.pointerref || # this one is volatile + f === Intrinsics.arraylen || # this one is volatile + f === Intrinsics.sqrt_llvm_fast || # this one may differ at runtime (by a few ulps) + f === Intrinsics.have_fma || # this one depends on the runtime environment + f === Intrinsics.cglobal) # cglobal lookup answer changes at runtime + effect_free = !(f === Intrinsics.pointerset) + nothrow = !isvarargtype(argtypes[end]) && intrinsic_nothrow(f, argtypes[2:end]) + + return Effects(EFFECTS_TOTAL; + consistent = ipo_consistent ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + effect_free = effect_free ? ALWAYS_TRUE : TRISTATE_UNKNOWN, + nothrow = nothrow ? ALWAYS_TRUE : TRISTATE_UNKNOWN) +end + # TODO: this function is a very buggy and poor model of the return_type function # since abstract_call_gf_by_type is a very inaccurate model of _method and of typeinf_type, # while this assumes that it is an absolutely precise and accurate and exact model of both @@ -1568,37 +2024,49 @@ function return_type_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, s aft = argtypes[2] if isa(aft, Const) || (isType(aft) && !has_free_typevars(aft)) || (isconcretetype(aft) && !(aft <: Builtin)) - af_argtype = isa(tt, Const) ? tt.val : tt.parameters[1] + af_argtype = isa(tt, Const) ? tt.val : (tt::DataType).parameters[1] if isa(af_argtype, DataType) && af_argtype <: Tuple argtypes_vec = Any[aft, af_argtype.parameters...] if contains_is(argtypes_vec, Union{}) - return Const(Union{}) + return CallMeta(Const(Union{}), false) end - rt = abstract_call(interp, nothing, argtypes_vec, sv, -1).rt + # Run the abstract_call without restricting abstract call + # sites. Otherwise, our behavior model of abstract_call + # below will be wrong. + old_restrict = sv.restrict_abstract_call_sites + sv.restrict_abstract_call_sites = false + call = abstract_call(interp, ArgInfo(nothing, argtypes_vec), sv, -1) + sv.restrict_abstract_call_sites = old_restrict + info = verbose_stmt_info(interp) ? ReturnTypeCallInfo(call.info) : false + rt = widenconditional(call.rt) if isa(rt, Const) # output was computed to be constant - return Const(typeof(rt.val)) + return CallMeta(Const(typeof(rt.val)), info) + end + rt = widenconst(rt) + if rt === Bottom || (isconcretetype(rt) && !iskindtype(rt)) + # output cannot be improved so it is known for certain + return CallMeta(Const(rt), info) + elseif !isempty(sv.pclimitations) + # conservatively express uncertainty of this result + # in two ways: both as being a subtype of this, and + # because of LimitedAccuracy causes + return CallMeta(Type{<:rt}, info) + elseif (isa(tt, Const) || isconstType(tt)) && + (isa(aft, Const) || isconstType(aft)) + # input arguments were known for certain + # XXX: this doesn't imply we know anything about rt + return CallMeta(Const(rt), info) + elseif isType(rt) + return CallMeta(Type{rt}, info) else - rt = widenconst(rt) - if hasuniquerep(rt) || rt === Bottom - # output type was known for certain - return Const(rt) - elseif (isa(tt, Const) || isconstType(tt)) && - (isa(aft, Const) || isconstType(aft)) - # input arguments were known for certain - # XXX: this doesn't imply we know anything about rt - return Const(rt) - elseif isType(rt) - return Type{rt} - else - return Type{<:rt} - end + return CallMeta(Type{<:rt}, info) end end end end end - return nothing + return CallMeta(Type, false) end # N.B.: typename maps type equivalence classes to a single value @@ -1609,4 +2077,66 @@ function typename_static(@nospecialize(t)) return isType(t) ? _typename(t.parameters[1]) : Core.TypeName end +function global_order_nothrow(@nospecialize(o), loading::Bool, storing::Bool) + o isa Const || return false + sym = o.val + if sym isa Symbol + order = get_atomic_order(sym, loading, storing) + return order !== MEMORY_ORDER_INVALID && order !== MEMORY_ORDER_NOTATOMIC + end + return false +end +function getglobal_nothrow(argtypes::Vector{Any}) + 2 ≤ length(argtypes) ≤ 3 || return false + if length(argtypes) == 3 + global_order_nothrow(argtypes[3], true, false) || return false + end + M, s = argtypes + if M isa Const && s isa Const + M, s = M.val, s.val + if M isa Module && s isa Symbol + return isdefined(M, s) + end + end + return false +end +function getglobal_tfunc(@nospecialize(M), @nospecialize(s), @nospecialize(_=Symbol)) + if M isa Const && s isa Const + M, s = M.val, s.val + if M isa Module && s isa Symbol + return abstract_eval_global(M, s) + end + return Bottom + elseif !(hasintersect(widenconst(M), Module) && hasintersect(widenconst(s), Symbol)) + return Bottom + end + return Any +end +function setglobal!_tfunc(@nospecialize(M), @nospecialize(s), @nospecialize(v), + @nospecialize(_=Symbol)) + if !(hasintersect(widenconst(M), Module) && hasintersect(widenconst(s), Symbol)) + return Bottom + end + return v +end +add_tfunc(getglobal, 2, 3, getglobal_tfunc, 1) +add_tfunc(setglobal!, 3, 4, setglobal!_tfunc, 3) + +function get_binding_type_effect_free(@nospecialize(M), @nospecialize(s)) + if M isa Const && s isa Const + M, s = M.val, s.val + if M isa Module && s isa Symbol + return ccall(:jl_binding_type, Any, (Any, Any), M, s) !== nothing + end + end + return false +end +function get_binding_type_tfunc(@nospecialize(M), @nospecialize(s)) + if get_binding_type_effect_free(M, s) + return Const(Core.get_binding_type((M::Const).val, (s::Const).val)) + end + return Type +end +add_tfunc(Core.get_binding_type, 2, 2, get_binding_type_tfunc, 0) + @specialize diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index 2cd89d0442fdb9..fefa2669972faa 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -1,109 +1,324 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -# build (and start inferring) the inference frame for the linfo -function typeinf(interp::AbstractInterpreter, result::InferenceResult, cached::Bool) - frame = InferenceState(result, cached, interp) +# Tracking of newly-inferred MethodInstances during precompilation +const track_newly_inferred = RefValue{Bool}(false) +const newly_inferred = MethodInstance[] + +# build (and start inferring) the inference frame for the top-level MethodInstance +function typeinf(interp::AbstractInterpreter, result::InferenceResult, cache::Symbol) + frame = InferenceState(result, cache, interp) frame === nothing && return false - cached && lock_mi_inference(interp, result.linfo) + cache === :global && lock_mi_inference(interp, result.linfo) return typeinf(interp, frame) end +""" +The module `Core.Compiler.Timings` provides a simple implementation of nested timers that +can be used to measure the exclusive time spent inferring each method instance that is +recursively inferred during type inference. + +This is meant to be internal to the compiler, and makes some specific assumptions about +being used for this purpose alone. +""" +module Timings + +using Core.Compiler: -, +, :, Vector, length, first, empty!, push!, pop!, @inline, + @inbounds, copy, backtrace + +# What we record for any given frame we infer during type inference. +struct InferenceFrameInfo + mi::Core.MethodInstance + world::UInt64 + sptypes::Vector{Any} + slottypes::Vector{Any} + nargs::Int +end + +function _typeinf_identifier(frame::Core.Compiler.InferenceState) + mi_info = InferenceFrameInfo( + frame.linfo, + frame.world, + copy(frame.sptypes), + copy(frame.slottypes), + length(frame.result.argtypes), + ) + return mi_info +end + +""" + Core.Compiler.Timing(mi_info, start_time, ...) + +Internal type containing the timing result for running type inference on a single +MethodInstance. +""" +struct Timing + mi_info::InferenceFrameInfo + start_time::UInt64 + cur_start_time::UInt64 + time::UInt64 + children::Core.Array{Timing,1} + bt # backtrace collected upon initial entry to typeinf +end +Timing(mi_info, start_time, cur_start_time, time, children) = Timing(mi_info, start_time, cur_start_time, time, children, nothing) +Timing(mi_info, start_time) = Timing(mi_info, start_time, start_time, UInt64(0), Timing[]) + +_time_ns() = ccall(:jl_hrtime, UInt64, ()) # Re-implemented here because Base not yet available. + +# We keep a stack of the Timings for each of the MethodInstances currently being timed. +# Since type inference currently operates via a depth-first search (during abstract +# evaluation), this vector operates like a call stack. The last node in _timings is the +# node currently being inferred, and its parent is directly before it, etc. +# Each Timing also contains its own vector for all of its children, so that the tree +# call structure through type inference is recorded. (It's recorded as a tree, not a graph, +# because we create a new node for duplicates.) +const _timings = Timing[] +# ROOT() is an empty function used as the top-level Timing node to measure all time spent +# *not* in type inference during a given recording trace. It is used as a "dummy" node. +function ROOT() end +const ROOTmi = Core.Compiler.specialize_method( + first(Core.Compiler.methods(ROOT)), Tuple{typeof(ROOT)}, Core.svec()) +""" + Core.Compiler.reset_timings() + +Empty out the previously recorded type inference timings (`Core.Compiler._timings`), and +start the ROOT() timer again. `ROOT()` measures all time spent _outside_ inference. +""" +function reset_timings() + empty!(_timings) + push!(_timings, Timing( + # The MethodInstance for ROOT(), and default empty values for other fields. + InferenceFrameInfo(ROOTmi, 0x0, Any[], Any[Core.Const(ROOT)], 1), + _time_ns())) + return nothing +end +reset_timings() + +# (This is split into a function so that it can be called both in this module, at the top +# of `enter_new_timer()`, and once at the Very End of the operation, by whoever started +# the operation and called `reset_timings()`.) +# NOTE: the @inline annotations here are not to make it faster, but to reduce the gap between +# timer manipulations and the tasks we're timing. +@inline function close_current_timer() + stop_time = _time_ns() + parent_timer = _timings[end] + accum_time = stop_time - parent_timer.cur_start_time + + # Add in accum_time ("modify" the immutable struct) + @inbounds begin + _timings[end] = Timing( + parent_timer.mi_info, + parent_timer.start_time, + parent_timer.cur_start_time, + parent_timer.time + accum_time, + parent_timer.children, + parent_timer.bt, + ) + end + return nothing +end + +@inline function enter_new_timer(frame) + # Very first thing, stop the active timer: get the current time and add in the + # time since it was last started to its aggregate exclusive time. + close_current_timer() + + mi_info = _typeinf_identifier(frame) + + # Start the new timer right before returning + push!(_timings, Timing(mi_info, UInt64(0))) + len = length(_timings) + new_timer = @inbounds _timings[len] + # Set the current time _after_ appending the node, to try to exclude the + # overhead from measurement. + start = _time_ns() + + @inbounds begin + _timings[len] = Timing( + new_timer.mi_info, + start, + start, + new_timer.time, + new_timer.children, + ) + end + + return nothing +end + +# _expected_frame_ is not needed within this function; it is used in the `@assert`, to +# assert that indeed we are always returning to a parent after finishing all of its +# children (that is, asserting that inference proceeds via depth-first-search). +@inline function exit_current_timer(_expected_frame_) + # Finish the new timer + stop_time = _time_ns() + + expected_mi_info = _typeinf_identifier(_expected_frame_) + + # Grab the new timer again because it might have been modified in _timings + # (since it's an immutable struct) + # And remove it from the current timings stack + new_timer = pop!(_timings) + Core.Compiler.@assert new_timer.mi_info.mi === expected_mi_info.mi + + # Prepare to unwind one level of the stack and record in the parent + parent_timer = _timings[end] + + accum_time = stop_time - new_timer.cur_start_time + # Add in accum_time ("modify" the immutable struct) + new_timer = Timing( + new_timer.mi_info, + new_timer.start_time, + new_timer.cur_start_time, + new_timer.time + accum_time, + new_timer.children, + parent_timer.mi_info.mi === ROOTmi ? backtrace() : nothing, + ) + # Record the final timing with the original parent timer + push!(parent_timer.children, new_timer) + + # And finally restart the parent timer: + len = length(_timings) + @inbounds begin + _timings[len] = Timing( + parent_timer.mi_info, + parent_timer.start_time, + _time_ns(), + parent_timer.time, + parent_timer.children, + parent_timer.bt, + ) + end + + return nothing +end + +end # module Timings + +""" + Core.Compiler.__set_measure_typeinf(onoff::Bool) + +If set to `true`, record per-method-instance timings within type inference in the Compiler. +""" +__set_measure_typeinf(onoff::Bool) = __measure_typeinf__[] = onoff +const __measure_typeinf__ = fill(false) + +# Wrapper around _typeinf that optionally records the exclusive time for each invocation. function typeinf(interp::AbstractInterpreter, frame::InferenceState) + if __measure_typeinf__[] + Timings.enter_new_timer(frame) + v = _typeinf(interp, frame) + Timings.exit_current_timer(frame) + return v + else + return _typeinf(interp, frame) + end +end + +function finish!(interp::AbstractInterpreter, caller::InferenceResult) + # If we didn't transform the src for caching, we may have to transform + # it anyway for users like typeinf_ext. Do that here. + opt = caller.src + if opt isa OptimizationState # implies `may_optimize(interp) === true` + if opt.ir !== nothing + caller.src = ir_to_codeinf!(opt) + end + end + return caller.src +end + +function _typeinf(interp::AbstractInterpreter, frame::InferenceState) typeinf_nocycle(interp, frame) || return false # frame is now part of a higher cycle # with no active ip's, frame is done frames = frame.callers_in_cycle isempty(frames) && push!(frames, frame) + valid_worlds = WorldRange() for caller in frames @assert !(caller.dont_work_on_me) caller.dont_work_on_me = true + # might might not fully intersect these earlier, so do that now + valid_worlds = intersect(caller.valid_worlds, valid_worlds) end for caller in frames + caller.valid_worlds = valid_worlds finish(caller, interp) + # finalize and record the linfo result + caller.inferred = true end # collect results for the new expanded frame - results = Tuple{InferenceResult, Bool}[ ( frames[i].result, - frames[i].cached || frames[i].parent !== nothing ) for i in 1:length(frames) ] - # empty!(frames) - valid_worlds = frame.valid_worlds - cached = frame.cached - if cached || frame.parent !== nothing - for (caller, doopt) in results - opt = caller.src - if opt isa OptimizationState - run_optimizer = doopt && may_optimize(interp) - if run_optimizer - optimize(opt, OptimizationParams(interp), caller.result) - finish(opt.src, interp) - # finish updating the result struct - validate_code_in_debug_mode(opt.linfo, opt.src, "optimized") - if opt.const_api - if caller.result isa Const - caller.src = caller.result - else - @assert isconstType(caller.result) - caller.src = Const(caller.result.parameters[1]) - end - elseif opt.src.inferred - caller.src = opt.src::CodeInfo # stash a copy of the code (for inlining) - else - caller.src = nothing - end - end - # As a hack the et reuses frame_edges[1] to push any optimization - # edges into, so we don't need to handle them specially here - valid_worlds = intersect(valid_worlds, opt.inlining.et.valid_worlds[]) + results = Tuple{InferenceResult, Vector{Any}, Bool}[ + ( frames[i].result, + frames[i].stmt_edges[1]::Vector{Any}, + frames[i].cached ) + for i in 1:length(frames) ] + empty!(frames) + for (caller, _, _) in results + opt = caller.src + if opt isa OptimizationState # implies `may_optimize(interp) === true` + analyzed = optimize(interp, opt, OptimizationParams(interp), caller) + if isa(analyzed, ConstAPI) + # XXX: The work in ir_to_codeinf! is essentially wasted. The only reason + # we're doing it is so that code_llvm can return the code + # for the `return ...::Const` (which never runs anyway). We should do this + # as a post processing step instead. + ir_to_codeinf!(opt) + caller.src = analyzed end + caller.valid_worlds = (opt.inlining.et::EdgeTracker).valid_worlds[] end end - if last(valid_worlds) == get_world_counter() - valid_worlds = WorldRange(first(valid_worlds), typemax(UInt)) - end - for caller in frames - caller.valid_worlds = valid_worlds - caller.src.min_world = first(valid_worlds) - caller.src.max_world = last(valid_worlds) - if cached - cache_result!(interp, caller.result, valid_worlds) - end - if last(valid_worlds) == typemax(UInt) + for (caller, edges, cached) in results + valid_worlds = caller.valid_worlds + if last(valid_worlds) >= get_world_counter() # if we aren't cached, we don't need this edge # but our caller might, so let's just make it anyways - for caller in frames - store_backedges(caller) - end + store_backedges(caller, edges) end - # finalize and record the linfo result - caller.inferred = true + if cached + cache_result!(interp, caller) + end + finish!(interp, caller) end return true end -function CodeInstance(result::InferenceResult, @nospecialize(inferred_result::Any), - valid_worlds::WorldRange) +function CodeInstance( + result::InferenceResult, @nospecialize(inferred_result), valid_worlds::WorldRange) local const_flags::Int32 - if inferred_result isa Const + result_type = result.result + @assert !(result_type isa LimitedAccuracy) + if inferred_result isa ConstAPI # use constant calling convention - rettype_const = (result.src::Const).val + rettype_const = inferred_result.val const_flags = 0x3 inferred_result = nothing else - if isa(result.result, Const) - rettype_const = (result.result::Const).val + if isa(result_type, Const) + rettype_const = result_type.val const_flags = 0x2 - elseif isconstType(result.result) - rettype_const = result.result.parameters[1] + elseif isa(result_type, PartialOpaque) + rettype_const = result_type const_flags = 0x2 - elseif isa(result.result, PartialStruct) - rettype_const = (result.result::PartialStruct).fields + elseif isconstType(result_type) + rettype_const = result_type.parameters[1] + const_flags = 0x2 + elseif isa(result_type, PartialStruct) + rettype_const = result_type.fields + const_flags = 0x2 + elseif isa(result_type, InterConditional) + rettype_const = result_type const_flags = 0x2 else rettype_const = nothing const_flags = 0x00 end end + relocatability = isa(inferred_result, Vector{UInt8}) ? inferred_result[end] : UInt8(0) return CodeInstance(result.linfo, - widenconst(result.result), rettype_const, inferred_result, - const_flags, first(valid_worlds), last(valid_worlds)) + widenconst(result_type), rettype_const, inferred_result, + const_flags, first(valid_worlds), last(valid_worlds), + # TODO: Actually do something with non-IPO effects + encode_effects(result.ipo_effects), encode_effects(result.ipo_effects), result.argescapes, + relocatability) end # For the NativeInterpreter, we don't need to do an actual cache query to know @@ -121,15 +336,17 @@ function maybe_compress_codeinfo(interp::AbstractInterpreter, linfo::MethodInsta if toplevel return ci end - cache_the_tree = !may_discard_trees(interp) || (ci.inferred && - (ci.inlineable || - ccall(:jl_isa_compileable_sig, Int32, (Any, Any), linfo.specTypes, def) != 0)) + if may_discard_trees(interp) + cache_the_tree = ci.inferred && (ci.inlineable || isa_compileable_sig(linfo.specTypes, def)) + else + cache_the_tree = true + end if cache_the_tree if may_compress(interp) nslots = length(ci.slotflags) - resize!(ci.slottypes, nslots) + resize!(ci.slottypes::Vector{Any}, nslots) resize!(ci.slotnames, nslots) - return ccall(:jl_compress_ir, Any, (Any, Any), def, ci) + return ccall(:jl_compress_ir, Vector{UInt8}, (Any, Any), def, ci) else return ci end @@ -139,81 +356,181 @@ function maybe_compress_codeinfo(interp::AbstractInterpreter, linfo::MethodInsta end function transform_result_for_cache(interp::AbstractInterpreter, linfo::MethodInstance, - @nospecialize(inferred_result)) - local const_flags::Int32 + valid_worlds::WorldRange, @nospecialize(inferred_result), + ipo_effects::Effects) # If we decided not to optimize, drop the OptimizationState now. # External interpreters can override as necessary to cache additional information if inferred_result isa OptimizationState - inferred_result = inferred_result.src + inferred_result = ir_to_codeinf!(inferred_result) end if inferred_result isa CodeInfo + inferred_result.min_world = first(valid_worlds) + inferred_result.max_world = last(valid_worlds) inferred_result = maybe_compress_codeinfo(interp, linfo, inferred_result) end # The global cache can only handle objects that codegen understands - if !isa(inferred_result, Union{CodeInfo, Vector{UInt8}, Const}) + if !isa(inferred_result, Union{CodeInfo, Vector{UInt8}, ConstAPI}) inferred_result = nothing end return inferred_result end -function cache_result!(interp::AbstractInterpreter, result::InferenceResult, valid_worlds::WorldRange) +function cache_result!(interp::AbstractInterpreter, result::InferenceResult) + valid_worlds = result.valid_worlds + if last(valid_worlds) == get_world_counter() + # if we've successfully recorded all of the backedges in the global reverse-cache, + # we can now widen our applicability in the global cache too + valid_worlds = WorldRange(first(valid_worlds), typemax(UInt)) + end # check if the existing linfo metadata is also sufficient to describe the current inference result # to decide if it is worth caching this - already_inferred = already_inferred_quick_test(interp, result.linfo) - if !already_inferred && haskey(WorldView(code_cache(interp), valid_worlds), result.linfo) + linfo = result.linfo + already_inferred = already_inferred_quick_test(interp, linfo) + if !already_inferred && haskey(WorldView(code_cache(interp), valid_worlds), linfo) already_inferred = true end # TODO: also don't store inferred code if we've previously decided to interpret this function if !already_inferred - inferred_result = transform_result_for_cache(interp, result.linfo, result.src) - code_cache(interp)[result.linfo] = CodeInstance(result, inferred_result, valid_worlds) + inferred_result = transform_result_for_cache(interp, linfo, valid_worlds, result.src, result.ipo_effects) + code_cache(interp)[linfo] = CodeInstance(result, inferred_result, valid_worlds) + if track_newly_inferred[] + m = linfo.def + if isa(m, Method) + m.module != Core && push!(newly_inferred, linfo) + end + end end - unlock_mi_inference(interp, result.linfo) + unlock_mi_inference(interp, linfo) nothing end +function cycle_fix_limited(@nospecialize(typ), sv::InferenceState) + if typ isa LimitedAccuracy + if sv.parent === nothing + # when part of a cycle, we might have unintentionally introduced a limit marker + @assert !isempty(sv.callers_in_cycle) + return typ.typ + end + causes = copy(typ.causes) + delete!(causes, sv) + for caller in sv.callers_in_cycle + delete!(causes, caller) + end + if isempty(causes) + return typ.typ + end + if length(causes) != length(typ.causes) + return LimitedAccuracy(typ.typ, causes) + end + end + return typ +end + +function adjust_effects(sv::InferenceState) + ipo_effects = Effects(sv) + + # Always throwing an error counts or never returning both count as consistent, + # but we don't currently model idempontency using dataflow, so we don't notice. + # Fix that up here to improve precision. + if !ipo_effects.inbounds_taints_consistency && sv.bestguess === Union{} + ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE) + end + + # override the analyzed effects using manually annotated effect settings + def = sv.linfo.def + if isa(def, Method) + override = decode_effects_override(def.purity) + if is_effect_overridden(override, :consistent) + ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE) + end + if is_effect_overridden(override, :effect_free) + ipo_effects = Effects(ipo_effects; effect_free=ALWAYS_TRUE) + end + if is_effect_overridden(override, :nothrow) + ipo_effects = Effects(ipo_effects; nothrow=ALWAYS_TRUE) + end + if is_effect_overridden(override, :terminates_globally) + ipo_effects = Effects(ipo_effects; terminates=ALWAYS_TRUE) + end + end + + return ipo_effects +end + # inference completed on `me` # update the MethodInstance function finish(me::InferenceState, interp::AbstractInterpreter) # prepare to run optimization passes on fulltree - if me.limited && me.cached && me.parent !== nothing - # a top parent will be cached still, but not this intermediate work + s_edges = me.stmt_edges[1] + if s_edges === nothing + s_edges = me.stmt_edges[1] = [] + end + for edges in me.stmt_edges + edges === nothing && continue + edges === s_edges && continue + append!(s_edges, edges) + empty!(edges) + end + if me.src.edges !== nothing + append!(s_edges, me.src.edges::Vector) + me.src.edges = nothing + end + # inspect whether our inference had a limited result accuracy, + # else it may be suitable to cache + me.bestguess = cycle_fix_limited(me.bestguess, me) + limited_ret = me.bestguess isa LimitedAccuracy + limited_src = false + if !limited_ret + gt = me.src.ssavaluetypes::Vector{Any} + for j = 1:length(gt) + gt[j] = gtj = cycle_fix_limited(gt[j], me) + if gtj isa LimitedAccuracy && me.parent !== nothing + limited_src = true + break + end + end + end + if limited_ret + # a parent may be cached still, but not this intermediate work: # we can throw everything else away now + me.result.src = nothing me.cached = false + me.src.inlineable = false unlock_mi_inference(interp, me.linfo) + elseif limited_src + # a type result will be cached still, but not this intermediate work: + # we can throw everything else away now + me.result.src = nothing me.src.inlineable = false else - # annotate fulltree with type information - type_annotate!(me) - me.result.src = OptimizationState(me, OptimizationParams(interp), interp) + # annotate fulltree with type information, + # either because we are the outermost code, or we might use this later + doopt = (me.cached || me.parent !== nothing) + type_annotate!(me, doopt) + if doopt && may_optimize(interp) + me.result.src = OptimizationState(me, OptimizationParams(interp), interp) + else + me.result.src = me.src::CodeInfo # stash a convenience copy of the code (e.g. for reflection) + end end + me.result.valid_worlds = me.valid_worlds me.result.result = me.bestguess - nothing -end - -function finish(src::CodeInfo, interp::AbstractInterpreter) - # convert all type information into the form consumed by the cache for inlining and code-generation - widen_all_consts!(src) - src.inferred = true + me.ipo_effects = me.result.ipo_effects = adjust_effects(me) + validate_code_in_debug_mode(me.linfo, me.src, "inferred") nothing end # record the backedges -function store_backedges(frame::InferenceState) +function store_backedges(frame::InferenceResult, edges::Vector{Any}) toplevel = !isa(frame.linfo.def, Method) - if !toplevel && (frame.cached || frame.parent !== nothing) - caller = frame.result.linfo - for edges in frame.stmt_edges - store_backedges(caller, edges) - end - store_backedges(caller, frame.src.edges) - frame.src.edges = nothing + if !toplevel + store_backedges(frame.linfo, edges) end + nothing end -store_backedges(caller, edges::Nothing) = nothing -function store_backedges(caller, edges::Vector) +function store_backedges(caller::MethodInstance, edges::Vector{Any}) i = 1 while i <= length(edges) to = edges[i] @@ -231,8 +548,9 @@ end # widen all Const elements in type annotations function widen_all_consts!(src::CodeInfo) - for i = 1:length(src.ssavaluetypes) - src.ssavaluetypes[i] = widenconst(src.ssavaluetypes[i]) + ssavaluetypes = src.ssavaluetypes::Vector{Any} + for i = 1:length(ssavaluetypes) + ssavaluetypes[i] = widenconst(ssavaluetypes[i]) end for i = 1:length(src.code) @@ -260,7 +578,7 @@ function annotate_slot_load!(e::Expr, vtypes::VarTable, sv::InferenceState, unde subex = e.args[i] if isa(subex, Expr) annotate_slot_load!(subex, vtypes, sv, undefs) - elseif isa(subex, Slot) + elseif isa(subex, SlotNumber) e.args[i] = visit_slot_load!(subex, vtypes, sv, undefs) end end @@ -269,16 +587,16 @@ end function annotate_slot_load(@nospecialize(e), vtypes::VarTable, sv::InferenceState, undefs::Array{Bool,1}) if isa(e, Expr) annotate_slot_load!(e, vtypes, sv, undefs) - elseif isa(e, Slot) + elseif isa(e, SlotNumber) return visit_slot_load!(e, vtypes, sv, undefs) end return e end -function visit_slot_load!(sl::Slot, vtypes::VarTable, sv::InferenceState, undefs::Array{Bool,1}) +function visit_slot_load!(sl::SlotNumber, vtypes::VarTable, sv::InferenceState, undefs::Array{Bool,1}) id = slot_id(sl) s = vtypes[id] - vt = widenconditional(s.typ) + vt = widenconditional(ignorelimited(s.typ)) if s.undef # find used-undef variables undefs[id] = true @@ -297,6 +615,7 @@ function record_slot_assign!(sv::InferenceState) states = sv.stmt_types body = sv.src.code::Vector{Any} slottypes = sv.slottypes::Vector{Any} + ssavaluetypes = sv.src.ssavaluetypes::Vector{Any} for i = 1:length(body) expr = body[i] st_i = states[i] @@ -304,8 +623,8 @@ function record_slot_assign!(sv::InferenceState) if isa(st_i, VarTable) && isa(expr, Expr) && expr.head === :(=) lhs = expr.args[1] rhs = expr.args[2] - if isa(lhs, Slot) - vt = widenconst(sv.src.ssavaluetypes[i]) + if isa(lhs, SlotNumber) + vt = widenconst(ssavaluetypes[i]) if vt !== Bottom id = slot_id(lhs) otherTy = slottypes[id] @@ -323,51 +642,55 @@ function record_slot_assign!(sv::InferenceState) end # annotate types of all symbols in AST -function type_annotate!(sv::InferenceState) - # delete dead statements only if we're building this IR to cache it - # (otherwise, we'll run the optimization passes later, outside of inference) - run_optimizer = (sv.cached || sv.parent !== nothing) +function type_annotate!(sv::InferenceState, run_optimizer::Bool) + # as an optimization, we delete dead statements immediately if we're going to run the optimizer + # (otherwise, we'll perhaps run the optimization passes later, outside of inference) # remove all unused ssa values - gt = sv.src.ssavaluetypes - for j = 1:length(gt) - if gt[j] === NOT_FOUND - gt[j] = Union{} - end - gt[j] = widenconditional(gt[j]) + src = sv.src + ssavaluetypes = src.ssavaluetypes::Vector{Any} + for j = 1:length(ssavaluetypes) + t = ssavaluetypes[j] + ssavaluetypes[j] = t === NOT_FOUND ? Union{} : widenconditional(t) end # compute the required type for each slot # to hold all of the items assigned into it record_slot_assign!(sv) sv.src.slottypes = sv.slottypes + @assert !(sv.bestguess isa LimitedAccuracy) sv.src.rettype = sv.bestguess # annotate variables load types # remove dead code optimization # and compute which variables may be used undef - src = sv.src states = sv.stmt_types - nargs = sv.nargs - nslots = length(states[1]::Array{Any,1}) + nslots = length(states[1]::VarTable) undefs = fill(false, nslots) body = src.code::Array{Any,1} nexpr = length(body) - # replace GotoIfNot with its condition if the branch target is unreachable - for i = 1:nexpr - expr = body[i] - if isa(expr, GotoIfNot) - if !isa(states[expr.dest], VarTable) - body[i] = expr.cond + # eliminate GotoIfNot if either of branch target is unreachable + if run_optimizer + for idx = 1:nexpr + stmt = body[idx] + if isa(stmt, GotoIfNot) && widenconst(argextype(stmt.cond, src, sv.sptypes)) === Bool + # replace live GotoIfNot with: + # - GotoNode if the fallthrough target is unreachable + # - no-op if the branch target is unreachable + if states[idx+1] === nothing + body[idx] = GotoNode(stmt.dest) + elseif states[stmt.dest] === nothing + body[idx] = nothing + end end end end + # dead code elimination for unreachable regions i = 1 oldidx = 0 changemap = fill(0, nexpr) - while i <= nexpr oldidx += 1 st_i = states[i] @@ -380,7 +703,7 @@ function type_annotate!(sv::InferenceState) body[i] = ReturnNode(annotate_slot_load(expr.val, st_i, sv, undefs)) elseif isa(expr, GotoIfNot) body[i] = GotoIfNot(annotate_slot_load(expr.cond, st_i, sv, undefs), expr.dest) - elseif isa(expr, Slot) + elseif isa(expr, SlotNumber) body[i] = visit_slot_load!(expr, st_i, sv, undefs) end else @@ -389,13 +712,12 @@ function type_annotate!(sv::InferenceState) elseif run_optimizer deleteat!(body, i) deleteat!(states, i) - deleteat!(src.ssavaluetypes, i) + deleteat!(ssavaluetypes, i) deleteat!(src.codelocs, i) deleteat!(sv.stmt_info, i) + deleteat!(src.ssaflags, i) nexpr -= 1 - if oldidx < length(changemap) - changemap[oldidx + 1] = -1 - end + changemap[oldidx] = -1 continue else body[i] = Const(expr) # annotate that this statement actually is dead @@ -403,7 +725,6 @@ function type_annotate!(sv::InferenceState) end i += 1 end - if run_optimizer renumber_ir_elements!(body, changemap) end @@ -436,22 +757,21 @@ function union_caller_cycle!(a::InferenceState, b::InferenceState) return end -function merge_call_chain!(parent::InferenceState, ancestor::InferenceState, child::InferenceState, limited::Bool) +function merge_call_chain!(parent::InferenceState, ancestor::InferenceState, child::InferenceState) # add backedge of parent <- child # then add all backedges of parent <- parent.parent # and merge all of the callers into ancestor.callers_in_cycle # and ensure that walking the parent list will get the same result (DAG) from everywhere + # Also taint the termination effect, because we can no longer guarantee the absence + # of recursion. + tristate_merge!(parent, Effects(EFFECTS_TOTAL; terminates=TRISTATE_UNKNOWN)) while true add_cycle_backedge!(child, parent, parent.currpc) union_caller_cycle!(ancestor, child) + tristate_merge!(child, Effects(EFFECTS_TOTAL; terminates=TRISTATE_UNKNOWN)) child = parent - parent = child.parent child === ancestor && break - end - if limited - for caller in ancestor.callers_in_cycle - caller.limited = true - end + parent = child.parent::InferenceState end end @@ -459,6 +779,11 @@ function is_same_frame(interp::AbstractInterpreter, linfo::MethodInstance, frame return linfo === frame.linfo end +function poison_callstack(infstate::InferenceState, topmost::InferenceState) + push!(infstate.pclimitations, topmost) + nothing +end + # Walk through `linfo`'s upstream call chain, starting at `parent`. If a parent # frame matching `linfo` is encountered, then there is a cycle in the call graph # (i.e. `linfo` is a descendant callee of itself). Upon encountering this cycle, @@ -469,28 +794,26 @@ end function resolve_call_cycle!(interp::AbstractInterpreter, linfo::MethodInstance, parent::InferenceState) frame = parent uncached = false - limited = false while isa(frame, InferenceState) uncached |= !frame.cached # ensure we never add an uncached frame to a cycle - limited |= frame.limited if is_same_frame(interp, linfo, frame) if uncached # our attempt to speculate into a constant call lead to an undesired self-cycle # that cannot be converged: poison our call-stack (up to the discovered duplicate frame) # with the limited flag and abort (set return type to Any) now - poison_callstack(parent, frame, false) + poison_callstack(parent, frame) return true end - merge_call_chain!(parent, frame, frame, limited) + merge_call_chain!(parent, frame, frame) return frame end for caller in frame.callers_in_cycle if is_same_frame(interp, linfo, caller) if uncached - poison_callstack(parent, frame, false) + poison_callstack(parent, frame) return true end - merge_call_chain!(parent, frame, caller, limited) + merge_call_chain!(parent, frame, caller) return caller end end @@ -499,24 +822,56 @@ function resolve_call_cycle!(interp::AbstractInterpreter, linfo::MethodInstance, return false end +generating_sysimg() = ccall(:jl_generating_output, Cint, ()) != 0 && JLOptions().incremental == 0 + +ipo_effects(code::CodeInstance) = decode_effects(code.ipo_purity_bits) + +struct EdgeCallResult + rt #::Type + edge::Union{Nothing,MethodInstance} + edge_effects::Effects + function EdgeCallResult(@nospecialize(rt), + edge::Union{Nothing,MethodInstance}, + edge_effects::Effects) + return new(rt, edge, edge_effects) + end +end + # compute (and cache) an inferred AST and return the current best estimate of the result type -function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize(atypes), sparams::SimpleVector, caller::InferenceState) - mi = specialize_method(method, atypes, sparams)::MethodInstance +function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize(atype), sparams::SimpleVector, caller::InferenceState) + mi = specialize_method(method, atype, sparams)::MethodInstance code = get(code_cache(interp), mi, nothing) if code isa CodeInstance # return existing rettype if the code is already inferred - update_valid_age!(caller, WorldRange(min_world(code), max_world(code))) - if isdefined(code, :rettype_const) - if isa(code.rettype_const, Vector{Any}) && !(Vector{Any} <: code.rettype) - return PartialStruct(code.rettype, code.rettype_const), mi - else - return Const(code.rettype_const), mi - end + if code.inferred === nothing && is_stmt_inline(get_curr_ssaflag(caller)) + # we already inferred this edge before and decided to discard the inferred code, + # nevertheless we re-infer it here again and keep it around in the local cache + # since the inliner will request to use it later + cache = :local else - return code.rettype, mi + effects = ipo_effects(code) + update_valid_age!(caller, WorldRange(min_world(code), max_world(code))) + rettype = code.rettype + if isdefined(code, :rettype_const) + rettype_const = code.rettype_const + # the second subtyping conditions are necessary to distinguish usual cases + # from rare cases when `Const` wrapped those extended lattice type objects + if isa(rettype_const, Vector{Any}) && !(Vector{Any} <: rettype) + rettype = PartialStruct(rettype, rettype_const) + elseif isa(rettype_const, PartialOpaque) && rettype <: Core.OpaqueClosure + rettype = rettype_const + elseif isa(rettype_const, InterConditional) && !(InterConditional <: rettype) + rettype = rettype_const + else + rettype = Const(rettype_const) + end + end + return EdgeCallResult(rettype, mi, effects) end + else + cache = :global # cache edge targets by default end - if ccall(:jl_get_module_infer, Cint, (Any,), method.module) == 0 - return Any, nothing + if ccall(:jl_get_module_infer, Cint, (Any,), method.module) == 0 && !generating_sysimg() + return EdgeCallResult(Any, nothing, Effects()) end if !caller.cached && caller.parent === nothing # this caller exists to return to the user @@ -529,51 +884,51 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize # completely new lock_mi_inference(interp, mi) result = InferenceResult(mi) - frame = InferenceState(result, #=cached=#true, interp) # always use the cache for edge targets + frame = InferenceState(result, cache, interp) # always use the cache for edge targets if frame === nothing # can't get the source for this, so we know nothing unlock_mi_inference(interp, mi) - return Any, nothing + return EdgeCallResult(Any, nothing, Effects()) end - if caller.cached || caller.limited # don't involve uncached functions in cycle resolution + if caller.cached || caller.parent !== nothing # don't involve uncached functions in cycle resolution frame.parent = caller end typeinf(interp, frame) update_valid_age!(frame, caller) - return widenconst_bestguess(frame.bestguess), frame.inferred ? mi : nothing + edge = frame.inferred ? mi : nothing + return EdgeCallResult(frame.bestguess, edge, Effects(frame)) # effects are adjusted already within `finish` elseif frame === true # unresolvable cycle - return Any, nothing + return EdgeCallResult(Any, nothing, Effects()) end # return the current knowledge about this cycle frame = frame::InferenceState update_valid_age!(frame, caller) - return widenconst_bestguess(frame.bestguess), nothing -end - -function widenconst_bestguess(bestguess) - !isa(bestguess, Const) && !isa(bestguess, PartialStruct) && !isa(bestguess, Type) && return widenconst(bestguess) - return bestguess + return EdgeCallResult(frame.bestguess, nothing, adjust_effects(frame)) end #### entry points for inferring a MethodInstance given a type signature #### # compute an inferred AST and return type -function typeinf_code(interp::AbstractInterpreter, method::Method, @nospecialize(atypes), sparams::SimpleVector, run_optimizer::Bool) - mi = specialize_method(method, atypes, sparams)::MethodInstance +function typeinf_code(interp::AbstractInterpreter, method::Method, @nospecialize(atype), sparams::SimpleVector, run_optimizer::Bool) + frame = typeinf_frame(interp, method, atype, sparams, run_optimizer) + frame === nothing && return nothing, Any + frame.inferred || return nothing, Any + code = frame.src + rt = widenconst(ignorelimited(frame.result.result)) + return code, rt +end + +# compute an inferred frame +function typeinf_frame(interp::AbstractInterpreter, method::Method, @nospecialize(atype), sparams::SimpleVector, run_optimizer::Bool) + mi = specialize_method(method, atype, sparams)::MethodInstance ccall(:jl_typeinf_begin, Cvoid, ()) result = InferenceResult(mi) - frame = InferenceState(result, false, interp) - frame === nothing && return (nothing, Any) - if typeinf(interp, frame) && run_optimizer - opt_params = OptimizationParams(interp) - opt = OptimizationState(frame, opt_params, interp) - optimize(opt, opt_params, result.result) - opt.src.inferred = true - end + frame = InferenceState(result, run_optimizer ? :global : :no, interp) + frame === nothing && return nothing + typeinf(interp, frame) ccall(:jl_typeinf_end, Cvoid, ()) - frame.inferred || return (nothing, Any) - return (frame.src, widenconst(result.result)) + return frame end # compute (and cache) an inferred AST and return type @@ -585,22 +940,23 @@ function typeinf_ext(interp::AbstractInterpreter, mi::MethodInstance) if code isa CodeInstance # see if this code already exists in the cache inf = code.inferred - if invoke_api(code) == 2 + if use_const_api(code) i == 2 && ccall(:jl_typeinf_end, Cvoid, ()) tree = ccall(:jl_new_code_info_uninit, Ref{CodeInfo}, ()) - tree.code = Any[ ReturnNode(quoted(code.rettype_const)) ] + rettype_const = code.rettype_const + tree.code = Any[ ReturnNode(quoted(rettype_const)) ] nargs = Int(method.nargs) tree.slotnames = ccall(:jl_uncompress_argnames, Vector{Symbol}, (Any,), method.slot_syms) - tree.slotflags = fill(0x00, nargs) + tree.slotflags = fill(IR_FLAG_NULL, nargs) tree.ssavaluetypes = 1 tree.codelocs = Int32[1] - tree.linetable = [LineInfoNode(method.module, method.name, method.file, Int(method.line), 0)] + tree.linetable = [LineInfoNode(method.module, method.name, method.file, method.line, Int32(0))] tree.inferred = true tree.ssaflags = UInt8[0] tree.pure = true tree.inlineable = true tree.parent = mi - tree.rettype = Core.Typeof(code.rettype_const) + tree.rettype = Core.Typeof(rettype_const) tree.min_world = code.min_world tree.max_world = code.max_world return tree @@ -622,11 +978,11 @@ function typeinf_ext(interp::AbstractInterpreter, mi::MethodInstance) end end end - if ccall(:jl_get_module_infer, Cint, (Any,), method.module) == 0 + if ccall(:jl_get_module_infer, Cint, (Any,), method.module) == 0 && !generating_sysimg() return retrieve_code_info(mi) end lock_mi_inference(interp, mi) - frame = InferenceState(InferenceResult(mi), #=cached=#true, interp) + frame = InferenceState(InferenceResult(mi), #=cache=#:global, interp) frame === nothing && return nothing typeinf(interp, frame) ccall(:jl_typeinf_end, Cvoid, ()) @@ -635,11 +991,11 @@ function typeinf_ext(interp::AbstractInterpreter, mi::MethodInstance) end # compute (and cache) an inferred AST and return the inferred return type -function typeinf_type(interp::AbstractInterpreter, method::Method, @nospecialize(atypes), sparams::SimpleVector) - if contains_is(unwrap_unionall(atypes).parameters, Union{}) +function typeinf_type(interp::AbstractInterpreter, method::Method, @nospecialize(atype), sparams::SimpleVector) + if contains_is(unwrap_unionall(atype).parameters, Union{}) return Union{} # don't ask: it does weird and unnecessary things, if it occurs during bootstrap end - mi = specialize_method(method, atypes, sparams)::MethodInstance + mi = specialize_method(method, atype, sparams)::MethodInstance for i = 1:2 # test-and-lock-and-test i == 2 && ccall(:jl_typeinf_begin, Cvoid, ()) code = get(code_cache(interp), mi, nothing) @@ -649,11 +1005,11 @@ function typeinf_type(interp::AbstractInterpreter, method::Method, @nospecialize return code.rettype end end - frame = InferenceResult(mi) - typeinf(interp, frame, true) + result = InferenceResult(mi) + typeinf(interp, result, :global) ccall(:jl_typeinf_end, Cvoid, ()) - frame.result isa InferenceState && return nothing - return widenconst(frame.result) + result.result isa InferenceState && return nothing + return widenconst(ignorelimited(result.result)) end # This is a bridge for the C code calling `jl_typeinf_func()` @@ -669,7 +1025,7 @@ function typeinf_ext_toplevel(interp::AbstractInterpreter, linfo::MethodInstance ccall(:jl_typeinf_begin, Cvoid, ()) if !src.inferred result = InferenceResult(linfo) - frame = InferenceState(result, src, #=cached=#true, interp) + frame = InferenceState(result, src, #=cache=#:global, interp) typeinf(interp, frame) @assert frame.inferred # TODO: deal with this better src = frame.src @@ -680,25 +1036,37 @@ function typeinf_ext_toplevel(interp::AbstractInterpreter, linfo::MethodInstance return src end +function return_type(@nospecialize(f), t::DataType) # this method has a special tfunc + world = ccall(:jl_get_tls_world_age, UInt, ()) + args = Any[_return_type, NativeInterpreter(world), Tuple{Core.Typeof(f), t.parameters...}] + return ccall(:jl_call_in_typeinf_world, Any, (Ptr{Ptr{Cvoid}}, Cint), args, length(args)) +end -function return_type(@nospecialize(f), @nospecialize(t)) +function return_type(@nospecialize(f), t::DataType, world::UInt) + return return_type(Tuple{Core.Typeof(f), t.parameters...}, world) +end + +function return_type(t::DataType) world = ccall(:jl_get_tls_world_age, UInt, ()) - return ccall(:jl_call_in_typeinf_world, Any, (Ptr{Ptr{Cvoid}}, Cint), Any[_return_type, f, t, world], 4) + return return_type(t, world) end -_return_type(@nospecialize(f), @nospecialize(t), world) = _return_type(NativeInterpreter(world), f, t) +function return_type(t::DataType, world::UInt) + args = Any[_return_type, NativeInterpreter(world), t] + return ccall(:jl_call_in_typeinf_world, Any, (Ptr{Ptr{Cvoid}}, Cint), args, length(args)) +end -function _return_type(interp::AbstractInterpreter, @nospecialize(f), @nospecialize(t)) +function _return_type(interp::AbstractInterpreter, t::DataType) rt = Union{} + f = singleton_type(t.parameters[1]) if isa(f, Builtin) - rt = builtin_tfunction(interp, f, Any[t.parameters...], nothing) - if isa(rt, TypeVar) - rt = rt.ub - else - rt = widenconst(rt) - end + args = Any[t.parameters...] + popfirst!(args) + rt = builtin_tfunction(interp, f, args, nothing) + rt = widenconst(rt) else - for match in _methods(f, t, -1, get_world_counter(interp)) + for match in _methods_by_ftype(t, -1, get_world_counter(interp))::Vector + match = match::MethodMatch ty = typeinf_type(interp, match.method, match.spec_types, match.sparams) ty === nothing && return Any rt = tmerge(rt, ty) diff --git a/base/compiler/typelattice.jl b/base/compiler/typelattice.jl index 17a444e840b770..79db3b6cf20b65 100644 --- a/base/compiler/typelattice.jl +++ b/base/compiler/typelattice.jl @@ -4,7 +4,7 @@ # structs/constants # ##################### -# N.B.: Const/PartialStruct are defined in Core, to allow them to be used +# N.B.: Const/PartialStruct/InterConditional are defined in Core, to allow them to be used # inside the global code cache. # # # The type of a value might be constant @@ -18,9 +18,8 @@ # end import Core: Const, PartialStruct - # The type of this value might be Bool. -# However, to enable a limited amount of back-propagagation, +# However, to enable a limited amount of back-propagation, # we also keep some information about how this Bool value was created. # In particular, if you branch on this value, then may assume that in # the true branch, the type of `var` will be limited by `vtype` and in @@ -34,7 +33,7 @@ import Core: Const, PartialStruct # end # ``` struct Conditional - var::Slot + var::SlotNumber vtype elsetype function Conditional( @@ -45,6 +44,18 @@ struct Conditional end end +# # Similar to `Conditional`, but conveys inter-procedural constraints imposed on call arguments. +# # This is separate from `Conditional` to catch logic errors: the lattice element name is InterConditional +# # while processing a call, then Conditional everywhere else. Thus InterConditional does not appear in +# # CompilerTypes—these type's usages are disjoint—though we define the lattice for InterConditional. +# struct InterConditional +# slot::Int +# vtype +# elsetype +# end +import Core: InterConditional +const AnyConditional = Union{Conditional,InterConditional} + struct PartialTypeVar tv::TypeVar # N.B.: Currently unused, but would allow turning something back @@ -56,27 +67,40 @@ end # Wraps a type and represents that the value may also be undef at this point. # (only used in optimize, not abstractinterpret) +# N.B. in the lattice, this is epsilon bigger than `typ` (even Any) struct MaybeUndef typ MaybeUndef(@nospecialize(typ)) = new(typ) end -# The type of a variable load is either a value or an UndefVarError -# (only used in abstractinterpret, doesn't appear in optimize) -struct VarState - typ - undef::Bool - VarState(@nospecialize(typ), undef::Bool) = new(typ, undef) -end - -const VarTable = Array{Any,1} - struct StateUpdate - var::Union{Slot,SSAValue} + var::SlotNumber vtype::VarState state::VarTable + conditional::Bool end +# Represent that the type estimate has been approximated, due to "causes" +# (only used in abstract interpretion, doesn't appear in optimization) +# N.B. in the lattice, this is epsilon smaller than `typ` (except Union{}) +struct LimitedAccuracy + typ + causes::IdSet{InferenceState} + function LimitedAccuracy(@nospecialize(typ), causes::IdSet{InferenceState}) + @assert !isa(typ, LimitedAccuracy) "malformed LimitedAccuracy" + return new(typ, causes) + end +end + +""" + struct NotFound end + const NOT_FOUND = NotFound() + +A special sigleton that represents a variable has not been analyzed yet. +Particularly, all SSA value types are initialized as `NOT_FOUND` when creating a new `InferenceState`. +Note that this is only used for `smerge`, which updates abstract state `VarTable`, +and thus we don't define the lattice for this. +""" struct NotFound end const NOT_FOUND = NotFound() @@ -90,11 +114,10 @@ const CompilerTypes = Union{MaybeUndef, Const, Conditional, NotFound, PartialStr # lattice logic # ################# -function issubconditional(a::Conditional, b::Conditional) - avar = a.var - bvar = b.var - if (isa(avar, Slot) && isa(bvar, Slot) && slot_id(avar) === slot_id(bvar)) || - (isa(avar, SSAValue) && isa(bvar, SSAValue) && avar === bvar) +# `Conditional` and `InterConditional` are valid in opposite contexts +# (i.e. local inference and inter-procedural call), as such they will never be compared +function issubconditional(a::C, b::C) where {C<:AnyConditional} + if is_same_conditionals(a, b) if a.vtype ⊑ b.vtype if a.elsetype ⊑ b.elsetype return true @@ -104,32 +127,54 @@ function issubconditional(a::Conditional, b::Conditional) return false end -maybe_extract_const_bool(c::Const) = isa(c.val, Bool) ? c.val : nothing -function maybe_extract_const_bool(c::Conditional) +is_same_conditionals(a::Conditional, b::Conditional) = slot_id(a.var) === slot_id(b.var) +is_same_conditionals(a::InterConditional, b::InterConditional) = a.slot === b.slot + +is_lattice_bool(@nospecialize(typ)) = typ !== Bottom && typ ⊑ Bool + +maybe_extract_const_bool(c::Const) = (val = c.val; isa(val, Bool)) ? val : nothing +function maybe_extract_const_bool(c::AnyConditional) (c.vtype === Bottom && !(c.elsetype === Bottom)) && return false (c.elsetype === Bottom && !(c.vtype === Bottom)) && return true nothing end maybe_extract_const_bool(@nospecialize c) = nothing -function ⊑(@nospecialize(a), @nospecialize(b)) +""" + a ⊑ b -> Bool + +The non-strict partial order over the type inference lattice. +""" +@nospecialize(a) ⊑ @nospecialize(b) = begin + if isa(b, LimitedAccuracy) + if !isa(a, LimitedAccuracy) + return false + end + if b.causes ⊈ a.causes + return false + end + b = b.typ + end + isa(a, LimitedAccuracy) && (a = a.typ) if isa(a, MaybeUndef) && !isa(b, MaybeUndef) return false end isa(a, MaybeUndef) && (a = a.typ) isa(b, MaybeUndef) && (b = b.typ) - (a === NOT_FOUND || b === Any) && return true - (a === Any || b === NOT_FOUND) && return false + b === Any && return true + a === Any && return false a === Union{} && return true b === Union{} && return false - if isa(a, Conditional) - if isa(b, Conditional) + @assert !isa(a, TypeVar) "invalid lattice item" + @assert !isa(b, TypeVar) "invalid lattice item" + if isa(a, AnyConditional) + if isa(b, AnyConditional) return issubconditional(a, b) elseif isa(b, Const) && isa(b.val, Bool) return maybe_extract_const_bool(a) === b.val end a = Bool - elseif isa(b, Conditional) + elseif isa(b, AnyConditional) return false end if isa(a, PartialStruct) @@ -155,13 +200,21 @@ function ⊑(@nospecialize(a), @nospecialize(b)) end for i in 1:nfields(a.val) # XXX: let's handle varargs later - isdefined(a.val, i) || return false + isdefined(a.val, i) || continue # since ∀ T Union{} ⊑ T ⊑(Const(getfield(a.val, i)), b.fields[i]) || return false end return true end return false end + if isa(a, PartialOpaque) + if isa(b, PartialOpaque) + (a.parent === b.parent && a.source === b.source) || return false + return (widenconst(a) <: widenconst(b)) && + ⊑(a.env, b.env) + end + return widenconst(a) ⊑ b + end if isa(a, Const) if isa(b, Const) return a.val === b.val @@ -177,14 +230,29 @@ function ⊑(@nospecialize(a), @nospecialize(b)) return false elseif isa(a, PartialTypeVar) && b === TypeVar return true - elseif !(isa(a, Type) || isa(a, TypeVar)) || - !(isa(b, Type) || isa(b, TypeVar)) - return a === b - else + elseif isa(a, Type) && isa(b, Type) return a <: b + else # handle this conservatively in the remaining cases + return a === b end end +""" + a ⊏ b -> Bool + +The strict partial order over the type inference lattice. +This is defined as the irreflexive kernel of `⊑`. +""" +@nospecialize(a) ⊏ @nospecialize(b) = a ⊑ b && !⊑(b, a) + +""" + a ⋤ b -> Bool + +This order could be used as a slightly more efficient version of the strict order `⊏`, +where we can safely assume `a ⊑ b` holds. +""" +@nospecialize(a) ⋤ @nospecialize(b) = !⊑(b, a) + # Check if two lattice elements are partial order equivalent. This is basically # `a ⊑ b && b ⊑ a` but with extra performance optimizations. function is_lattice_equal(@nospecialize(a), @nospecialize(b)) @@ -199,27 +267,80 @@ function is_lattice_equal(@nospecialize(a), @nospecialize(b)) return true end isa(b, PartialStruct) && return false - a isa Const && return false - b isa Const && return false + if a isa Const + if issingletontype(b) + return a.val === b.instance + end + return false + end + if b isa Const + if issingletontype(a) + return a.instance === b.val + end + return false + end + if isa(a, PartialOpaque) + isa(b, PartialOpaque) || return false + widenconst(a) == widenconst(b) || return false + a.source === b.source || return false + a.parent === b.parent || return false + return is_lattice_equal(a.env, b.env) + end return a ⊑ b && b ⊑ a end -widenconst(c::Conditional) = Bool -function widenconst(c::Const) - if isa(c.val, Type) - if isvarargtype(c.val) - return Type +# compute typeintersect over the extended inference lattice, +# as precisely as we can, +# where v is in the extended lattice, and t is a Type. +function tmeet(@nospecialize(v), @nospecialize(t)) + if isa(v, Const) + if !has_free_typevars(t) && !isa(v.val, t) + return Bottom + end + return v + elseif isa(v, PartialStruct) + has_free_typevars(t) && return v + widev = widenconst(v) + if widev <: t + return v + end + ti = typeintersect(widev, t) + valid_as_lattice(ti) || return Bottom + @assert widev <: Tuple + new_fields = Vector{Any}(undef, length(v.fields)) + for i = 1:length(new_fields) + vfi = v.fields[i] + if isvarargtype(vfi) + new_fields[i] = vfi + else + new_fields[i] = tmeet(vfi, widenconst(getfield_tfunc(t, Const(i)))) + if new_fields[i] === Bottom + return Bottom + end + end end - return Type{c.val} - else - return typeof(c.val) + return tuple_tfunc(new_fields) + elseif isa(v, Conditional) + if !(Bool <: t) + return Bottom + end + return v end + ti = typeintersect(widenconst(v), t) + valid_as_lattice(ti) || return Bottom + return ti end + +widenconst(c::AnyConditional) = Bool +widenconst((; val)::Const) = isa(val, Type) ? Type{val} : typeof(val) widenconst(m::MaybeUndef) = widenconst(m.typ) widenconst(c::PartialTypeVar) = TypeVar widenconst(t::PartialStruct) = t.typ +widenconst(t::PartialOpaque) = t.typ widenconst(t::Type) = t -widenconst(t::TypeVar) = t +widenconst(t::TypeVar) = error("unhandled TypeVar") +widenconst(t::TypeofVararg) = error("unhandled Vararg") +widenconst(t::LimitedAccuracy) = error("unhandled LimitedAccuracy") issubstate(a::VarState, b::VarState) = (a.typ ⊑ b.typ && a.undef <= b.undef) @@ -233,31 +354,42 @@ function smerge(sa::Union{NotFound,VarState}, sb::Union{NotFound,VarState}) end @inline tchanged(@nospecialize(n), @nospecialize(o)) = o === NOT_FOUND || (n !== NOT_FOUND && !(n ⊑ o)) -@inline schanged(@nospecialize(n), @nospecialize(o)) = (n !== o) && (o === NOT_FOUND || (n !== NOT_FOUND && !issubstate(n, o))) - -widenconditional(@nospecialize typ) = typ -function widenconditional(typ::Conditional) - if typ.vtype === Union{} - return Const(false) - elseif typ.elsetype === Union{} - return Const(true) - else - return Bool +@inline schanged(@nospecialize(n), @nospecialize(o)) = (n !== o) && (o === NOT_FOUND || (n !== NOT_FOUND && !issubstate(n::VarState, o::VarState))) + +function widenconditional(@nospecialize typ) + if isa(typ, AnyConditional) + if typ.vtype === Union{} + return Const(false) + elseif typ.elsetype === Union{} + return Const(true) + else + return Bool + end end + return typ end +widenconditional(t::LimitedAccuracy) = error("unhandled LimitedAccuracy") + +widenwrappedconditional(@nospecialize(typ)) = widenconditional(typ) +widenwrappedconditional(typ::LimitedAccuracy) = LimitedAccuracy(widenconditional(typ.typ), typ.causes) + +ignorelimited(@nospecialize typ) = typ +ignorelimited(typ::LimitedAccuracy) = typ.typ function stupdate!(state::Nothing, changes::StateUpdate) newst = copy(changes.state) - if isa(changes.var, Slot) - changeid = slot_id(changes.var::Slot) - newst[changeid] = changes.vtype - # remove any Conditional for this Slot from the vtable + changeid = slot_id(changes.var) + newst[changeid] = changes.vtype + # remove any Conditional for this slot from the vtable + # (unless this change is came from the conditional) + if !changes.conditional for i = 1:length(newst) newtype = newst[i] if isa(newtype, VarState) - newtypetyp = newtype.typ + newtypetyp = ignorelimited(newtype.typ) if isa(newtypetyp, Conditional) && slot_id(newtypetyp.var) == changeid - newst[i] = VarState(widenconditional(newtypetyp), newtype.undef) + newtypetyp = widenwrappedconditional(newtype.typ) + newst[i] = VarState(newtypetyp, newtype.undef) end end end @@ -266,11 +398,8 @@ function stupdate!(state::Nothing, changes::StateUpdate) end function stupdate!(state::VarTable, changes::StateUpdate) - if !isa(changes.var, Slot) - return stupdate!(state, changes.state) - end - newstate = false - changeid = slot_id(changes.var::Slot) + newstate = nothing + changeid = slot_id(changes.var) for i = 1:length(state) if i == changeid newtype = changes.vtype @@ -278,11 +407,13 @@ function stupdate!(state::VarTable, changes::StateUpdate) newtype = changes.state[i] end oldtype = state[i] - # remove any Conditional for this Slot from the vtable - if isa(newtype, VarState) - newtypetyp = newtype.typ + # remove any Conditional for this slot from the vtable + # (unless this change is came from the conditional) + if !changes.conditional && isa(newtype, VarState) + newtypetyp = ignorelimited(newtype.typ) if isa(newtypetyp, Conditional) && slot_id(newtypetyp.var) == changeid - newtype = VarState(widenconditional(newtypetyp), newtype.undef) + newtypetyp = widenwrappedconditional(newtype.typ) + newtype = VarState(newtypetyp, newtype.undef) end end if schanged(newtype, oldtype) @@ -294,7 +425,7 @@ function stupdate!(state::VarTable, changes::StateUpdate) end function stupdate!(state::VarTable, changes::VarTable) - newstate = false + newstate = nothing for i = 1:length(state) newtype = changes[i] oldtype = state[i] @@ -308,20 +439,24 @@ end stupdate!(state::Nothing, changes::VarTable) = copy(changes) -stupdate!(state::Nothing, changes::Nothing) = false +stupdate!(state::Nothing, changes::Nothing) = nothing function stupdate1!(state::VarTable, change::StateUpdate) - if !isa(change.var, Slot) - return false - end - changeid = slot_id(change.var::Slot) - # remove any Conditional for this Slot from the catch block vtable - for i = 1:length(state) - oldtype = state[i] - if isa(oldtype, VarState) - oldtypetyp = oldtype.typ - if isa(oldtypetyp, Conditional) && slot_id(oldtypetyp.var) == changeid - state[i] = VarState(widenconditional(oldtypetyp), oldtype.undef) + changeid = slot_id(change.var) + # remove any Conditional for this slot from the catch block vtable + # (unless this change is came from the conditional) + if !change.conditional + for i = 1:length(state) + oldtype = state[i] + if isa(oldtype, VarState) + oldtypetyp = ignorelimited(oldtype.typ) + if isa(oldtypetyp, Conditional) && slot_id(oldtypetyp.var) == changeid + oldtypetyp = widenconditional(oldtypetyp) + if oldtype.typ isa LimitedAccuracy + oldtypetyp = LimitedAccuracy(oldtypetyp, (oldtype.typ::LimitedAccuracy).causes) + end + state[i] = VarState(oldtypetyp, oldtype.undef) + end end end end diff --git a/base/compiler/typelimits.jl b/base/compiler/typelimits.jl index 22be265287fa51..2c5adb92e5a09c 100644 --- a/base/compiler/typelimits.jl +++ b/base/compiler/typelimits.jl @@ -21,7 +21,7 @@ function limit_type_size(@nospecialize(t), @nospecialize(compare), @nospecialize type_more_complex(t, compare, source, 1, allowed_tupledepth, allowed_tuplelen) || return t r = _limit_type_size(t, compare, source, 1, allowed_tuplelen) #@assert t <: r # this may fail if t contains a typevar in invariant and multiple times - # in covariant position and r looses the occurence in invariant position (see #36407) + # in covariant position and r looses the occurrence in invariant position (see #36407) if !(t <: r) # ideally, this should never happen # widen to minimum complexity to obtain a valid result r = _limit_type_size(t, Any, source, 1, allowed_tuplelen) @@ -39,6 +39,8 @@ function is_derived_type(@nospecialize(t), @nospecialize(c), mindepth::Int) if t === c return mindepth <= 1 end + isvarargtype(t) && (t = unwrapva(t)) + isvarargtype(c) && (c = unwrapva(c)) if isa(c, Union) # see if it is one of the elements of the union return is_derived_type(t, c.a, mindepth) || is_derived_type(t, c.b, mindepth) @@ -77,6 +79,7 @@ end # The goal of this function is to return a type of greater "size" and less "complexity" than # both `t` or `c` over the lattice defined by `sources`, `depth`, and `allowed_tuplelen`. function _limit_type_size(@nospecialize(t), @nospecialize(c), sources::SimpleVector, depth::Int, allowed_tuplelen::Int) + @assert isa(t, Type) && isa(c, Type) "unhandled TypeVar / Vararg" if t === c return t # quick egal test elseif t === Union{} @@ -85,7 +88,7 @@ function _limit_type_size(@nospecialize(t), @nospecialize(c), sources::SimpleVec return t # fast path: unparameterized are always simple else ut = unwrap_unionall(t) - if isa(ut, DataType) && ut.name !== _va_typename && isa(c, Type) && c !== Union{} && c <: t + if isa(ut, DataType) && isa(c, Type) && c !== Union{} && c <: t # TODO: need to check that the UnionAll bounds on t are limited enough too return t # t is already wider than the comparison in the type lattice elseif is_derived_type_from_any(ut, sources, depth) @@ -96,41 +99,35 @@ function _limit_type_size(@nospecialize(t), @nospecialize(c), sources::SimpleVec # first attempt to turn `c` into a type that contributes meaningful information # by peeling off meaningless non-matching wrappers of comparison one at a time # then unwrap `t` - if isa(c, TypeVar) - if isa(t, TypeVar) && t.ub === c.ub && (t.lb === Union{} || t.lb === c.lb) - return t # it's ok to change the name, or widen `lb` to Union{}, so we can handle this immediately here - end - return _limit_type_size(t, c.ub, sources, depth, allowed_tuplelen) - end + # NOTE that `TypeVar` / `Vararg` are handled separately to catch the logic errors if isa(c, UnionAll) - return _limit_type_size(t, c.body, sources, depth, allowed_tuplelen) + return __limit_type_size(t, c.body, sources, depth, allowed_tuplelen)::Type end if isa(t, UnionAll) - tbody = _limit_type_size(t.body, c, sources, depth, allowed_tuplelen) + tbody = __limit_type_size(t.body, c, sources, depth, allowed_tuplelen) tbody === t.body && return t - return UnionAll(t.var, tbody) - elseif isa(t, TypeVar) - # don't have a matching TypeVar in comparison, so we keep just the upper bound - return _limit_type_size(t.ub, c, sources, depth, allowed_tuplelen) + return UnionAll(t.var, tbody)::Type elseif isa(t, Union) if isa(c, Union) - a = _limit_type_size(t.a, c.a, sources, depth, allowed_tuplelen) - b = _limit_type_size(t.b, c.b, sources, depth, allowed_tuplelen) + a = __limit_type_size(t.a, c.a, sources, depth, allowed_tuplelen) + b = __limit_type_size(t.b, c.b, sources, depth, allowed_tuplelen) return Union{a, b} end elseif isa(t, DataType) - if isa(c, DataType) + if isType(t) # see equivalent case in type_more_complex + tt = unwrap_unionall(t.parameters[1]) + if isa(tt, Union) || isa(tt, TypeVar) || isType(tt) + is_derived_type_from_any(tt, sources, depth + 1) && return t + else + isType(c) && (c = unwrap_unionall(c.parameters[1])) + type_more_complex(tt, c, sources, depth, 0, 0) || return t + end + return Type + elseif isa(c, DataType) tP = t.parameters cP = c.parameters if t.name === c.name && !isempty(cP) - if isvarargtype(t) - VaT = _limit_type_size(tP[1], cP[1], sources, depth + 1, 0) - N = tP[2] - if isa(N, TypeVar) || N === cP[2] - return Vararg{VaT, N} - end - return Vararg{VaT} - elseif t.name === Tuple.name + if t.name === Tuple.name # for covariant datatypes (Tuple), # apply type-size limit element-wise ltP = length(tP) @@ -151,25 +148,12 @@ function _limit_type_size(@nospecialize(t), @nospecialize(c), sources::SimpleVec else cPi = Any end - Q[i] = _limit_type_size(Q[i], cPi, sources, depth + 1, 0) + Q[i] = __limit_type_size(Q[i], cPi, sources, depth + 1, 0) end return Tuple{Q...} end - elseif isvarargtype(c) - # Tuple{Vararg{T}} --> Tuple{T} is OK - return _limit_type_size(t, cP[1], sources, depth, 0) end end - if isType(t) # allow taking typeof as Type{...}, but ensure it doesn't start nesting - tt = unwrap_unionall(t.parameters[1]) - if isa(tt, DataType) && !isType(tt) - is_derived_type_from_any(tt, sources, depth) && return t - end - end - if isvarargtype(t) - # never replace Vararg with non-Vararg - return Vararg - end if allowed_tuplelen < 1 && t.name === Tuple.name return Any end @@ -185,6 +169,38 @@ function _limit_type_size(@nospecialize(t), @nospecialize(c), sources::SimpleVec return Any end +# helper function of `_limit_type_size`, which has the right to take and return `TypeVar` / `Vararg` +function __limit_type_size(@nospecialize(t), @nospecialize(c), sources::SimpleVector, depth::Int, allowed_tuplelen::Int) + cN = 0 + if isvarargtype(c) # Tuple{Vararg{T}} --> Tuple{T} is OK + isdefined(c, :N) && (cN = c.N) + c = unwrapva(c) + end + if isa(c, TypeVar) + if isa(t, TypeVar) && t.ub === c.ub && (t.lb === Union{} || t.lb === c.lb) + return t # it's ok to change the name, or widen `lb` to Union{}, so we can handle this immediately here + end + return __limit_type_size(t, c.ub, sources, depth, allowed_tuplelen) + elseif isa(t, TypeVar) + # don't have a matching TypeVar in comparison, so we keep just the upper bound + return __limit_type_size(t.ub, c, sources, depth, allowed_tuplelen) + elseif isvarargtype(t) + # Tuple{Vararg{T,N}} --> Tuple{Vararg{S,M}} is OK + # Tuple{T} --> Tuple{Vararg{T}} is OK + # but S must be more limited than T, and must not introduce a new number for M + VaT = __limit_type_size(unwrapva(t), c, sources, depth + 1, 0) + if isdefined(t, :N) + tN = t.N + if isa(tN, TypeVar) || tN === cN + return Vararg{VaT, tN} + end + end + return Vararg{VaT} + else + return _limit_type_size(t, c, sources, depth, allowed_tuplelen) + end +end + function type_more_complex(@nospecialize(t), @nospecialize(c), sources::SimpleVector, depth::Int, tupledepth::Int, allowed_tuplelen::Int) # detect cases where the comparison is trivial if t === c @@ -200,6 +216,8 @@ function type_more_complex(@nospecialize(t), @nospecialize(c), sources::SimpleVe return false # t isn't something new end # peel off wrappers + isvarargtype(t) && (t = unwrapva(t)) + isvarargtype(c) && (c = unwrapva(c)) if isa(c, UnionAll) # allow wrapping type with fewer UnionAlls than comparison if in a covariant context if !isa(t, UnionAll) && tupledepth == 0 @@ -230,7 +248,18 @@ function type_more_complex(@nospecialize(t), @nospecialize(c), sources::SimpleVe # base case for data types if isa(t, DataType) tP = t.parameters - if isa(c, DataType) && t.name === c.name + if isType(t) + # Treat Type{T} and T as equivalent to allow taking typeof any + # source type (DataType) anywhere as Type{...}, as long as it isn't + # nesting as Type{Type{...}} + tt = unwrap_unionall(t.parameters[1]) + if isa(tt, Union) || isa(tt, TypeVar) || isType(tt) + return !is_derived_type_from_any(tt, sources, depth + 1) + else + isType(c) && (c = unwrap_unionall(c.parameters[1])) + return type_more_complex(tt, c, sources, depth, 0, 0) + end + elseif isa(c, DataType) && t.name === c.name cP = c.parameters length(cP) < length(tP) && return true length(cP) > length(tP) && !isvarargtype(tP[end]) && depth == 1 && return false @@ -238,7 +267,7 @@ function type_more_complex(@nospecialize(t), @nospecialize(c), sources::SimpleVe # allow creating variation within a nested tuple, but only so deep if t.name === Tuple.name && tupledepth > 0 tupledepth -= 1 - elseif !isvarargtype(t) + else tupledepth = 0 end isgenerator = (t.name.name === :Generator && t.name.module === _topmod(t.name.module)) @@ -249,7 +278,7 @@ function type_more_complex(@nospecialize(t), @nospecialize(c), sources::SimpleVe let tPi = unwrap_unionall(tPi), cPi = unwrap_unionall(cPi) if isa(tPi, DataType) && isa(cPi, DataType) && - !tPi.abstract && !cPi.abstract && + !isabstracttype(tPi) && !isabstracttype(cPi) && sym_isless(cPi.name.name, tPi.name.name) # allow collect on (anonymous) Generators to nest, provided that their functions are appropriately ordered # TODO: is there a better way? @@ -260,15 +289,6 @@ function type_more_complex(@nospecialize(t), @nospecialize(c), sources::SimpleVe type_more_complex(tPi, cPi, sources, depth + 1, tupledepth, 0) && return true end return false - elseif isvarargtype(c) - return type_more_complex(t, unwrapva(c), sources, depth, tupledepth, 0) - end - if isType(t) # allow taking typeof any source type anywhere as Type{...}, as long as it isn't nesting Type{Type{...}} - tt = unwrap_unionall(t.parameters[1]) - if isa(tt, DataType) && !isType(tt) - is_derived_type_from_any(tt, sources, depth) || return true - return false - end end end return true @@ -278,7 +298,55 @@ union_count_abstract(x::Union) = union_count_abstract(x.a) + union_count_abstrac union_count_abstract(@nospecialize(x)) = !isdispatchelem(x) function issimpleenoughtype(@nospecialize t) - return unionlen(t)+union_count_abstract(t) <= MAX_TYPEUNION_LENGTH && unioncomplexity(t) <= MAX_TYPEUNION_COMPLEXITY + return unionlen(t) + union_count_abstract(t) <= MAX_TYPEUNION_LENGTH && + unioncomplexity(t) <= MAX_TYPEUNION_COMPLEXITY +end + +# A simplified type_more_complex query over the extended lattice +# (assumes typeb ⊑ typea) +function issimplertype(@nospecialize(typea), @nospecialize(typeb)) + typea = ignorelimited(typea) + typeb = ignorelimited(typeb) + typea isa MaybeUndef && (typea = typea.typ) # n.b. does not appear in inference + typeb isa MaybeUndef && (typeb = typeb.typ) # n.b. does not appear in inference + typea === typeb && return true + if typea isa PartialStruct + aty = widenconst(typea) + for i = 1:length(typea.fields) + ai = unwrapva(typea.fields[i]) + bi = fieldtype(aty, i) + is_lattice_equal(ai, bi) && continue + tni = _typename(widenconst(ai)) + if tni isa Const + bi = (tni.val::Core.TypeName).wrapper + is_lattice_equal(ai, bi) && continue + end + bi = getfield_tfunc(typeb, Const(i)) + is_lattice_equal(ai, bi) && continue + # It is not enough for ai to be simpler than bi: it must exactly equal + # (for this, an invariant struct field, by contrast to + # type_more_complex above which handles covariant tuples). + return false + end + elseif typea isa Type + return issimpleenoughtype(typea) + # elseif typea isa Const # fall-through good + elseif typea isa Conditional # follow issubconditional query + typeb isa Const && return true + typeb isa Conditional || return false + is_same_conditionals(typea, typeb) || return false + issimplertype(typea.vtype, typeb.vtype) || return false + issimplertype(typea.elsetype, typeb.elsetype) || return false + elseif typea isa InterConditional # ibid + typeb isa Const && return true + typeb isa InterConditional || return false + is_same_conditionals(typea, typeb) || return false + issimplertype(typea.vtype, typeb.vtype) || return false + issimplertype(typea.elsetype, typeb.elsetype) || return false + elseif typea isa PartialOpaque + # TODO + end + return true end # pick a wider type that contains both typea and typeb, @@ -288,12 +356,32 @@ end function tmerge(@nospecialize(typea), @nospecialize(typeb)) typea === Union{} && return typeb typeb === Union{} && return typea + typea === typeb && return typea + suba = typea ⊑ typeb - suba && issimpleenoughtype(typeb) && return typeb + suba && issimplertype(typeb, typea) && return typeb subb = typeb ⊑ typea suba && subb && return typea - subb && issimpleenoughtype(typea) && return typea + subb && issimplertype(typea, typeb) && return typea + # type-lattice for LimitedAccuracy wrapper + # the merge create a slightly narrower type than needed, but we can't + # represent the precise intersection of causes and don't attempt to + # enumerate some of these cases where we could + if isa(typea, LimitedAccuracy) && isa(typeb, LimitedAccuracy) + if typea.causes ⊆ typeb.causes + causes = typeb.causes + elseif typeb.causes ⊆ typea.causes + causes = typea.causes + else + causes = union!(copy(typea.causes), typeb.causes) + end + return LimitedAccuracy(tmerge(typea.typ, typeb.typ), causes) + elseif isa(typea, LimitedAccuracy) + return LimitedAccuracy(tmerge(typea.typ, typeb), typea.causes) + elseif isa(typeb, LimitedAccuracy) + return LimitedAccuracy(tmerge(typea, typeb.typ), typeb.causes) + end # type-lattice for MaybeUndef wrapper if isa(typea, MaybeUndef) || isa(typeb, MaybeUndef) return MaybeUndef(tmerge( @@ -316,10 +404,10 @@ function tmerge(@nospecialize(typea), @nospecialize(typeb)) end end if isa(typea, Conditional) && isa(typeb, Conditional) - if typea.var === typeb.var + if is_same_conditionals(typea, typeb) vtype = tmerge(typea.vtype, typeb.vtype) elsetype = tmerge(typea.elsetype, typeb.elsetype) - if vtype != elsetype + if vtype !== elsetype return Conditional(typea.var, vtype, elsetype) end end @@ -329,26 +417,93 @@ function tmerge(@nospecialize(typea), @nospecialize(typeb)) end return Bool end - if (isa(typea, PartialStruct) || isa(typea, Const)) && - (isa(typeb, PartialStruct) || isa(typeb, Const)) && - widenconst(typea) === widenconst(typeb) - - typea_nfields = nfields_tfunc(typea) - typeb_nfields = nfields_tfunc(typeb) - if !isa(typea_nfields, Const) || !isa(typeb_nfields, Const) || typea_nfields.val !== typeb_nfields.val + # type-lattice for InterConditional wrapper, InterConditional will never be merged with Conditional + if isa(typea, InterConditional) && isa(typeb, Const) + if typeb.val === true + typeb = InterConditional(typea.slot, Any, Union{}) + elseif typeb.val === false + typeb = InterConditional(typea.slot, Union{}, Any) + end + end + if isa(typeb, InterConditional) && isa(typea, Const) + if typea.val === true + typea = InterConditional(typeb.slot, Any, Union{}) + elseif typea.val === false + typea = InterConditional(typeb.slot, Union{}, Any) + end + end + if isa(typea, InterConditional) && isa(typeb, InterConditional) + if is_same_conditionals(typea, typeb) + vtype = tmerge(typea.vtype, typeb.vtype) + elsetype = tmerge(typea.elsetype, typeb.elsetype) + if vtype !== elsetype + return InterConditional(typea.slot, vtype, elsetype) + end + end + val = maybe_extract_const_bool(typea) + if val isa Bool && val === maybe_extract_const_bool(typeb) + return Const(val) + end + return Bool + end + # type-lattice for Const and PartialStruct wrappers + if ((isa(typea, PartialStruct) || isa(typea, Const)) && + (isa(typeb, PartialStruct) || isa(typeb, Const))) + aty = widenconst(typea) + bty = widenconst(typeb) + if aty === bty + # must have egal here, since we do not create PartialStruct for non-concrete types + typea_nfields = nfields_tfunc(typea) + typeb_nfields = nfields_tfunc(typeb) + isa(typea_nfields, Const) || return aty + isa(typeb_nfields, Const) || return aty + type_nfields = typea_nfields.val::Int + type_nfields === typeb_nfields.val::Int || return aty + type_nfields == 0 && return aty + fields = Vector{Any}(undef, type_nfields) + anyrefine = false + for i = 1:type_nfields + ai = getfield_tfunc(typea, Const(i)) + bi = getfield_tfunc(typeb, Const(i)) + ft = fieldtype(aty, i) + if is_lattice_equal(ai, bi) || is_lattice_equal(ai, ft) + # Since ai===bi, the given type has no restrictions on complexity. + # and can be used to refine ft + tyi = ai + elseif is_lattice_equal(bi, ft) + tyi = bi + else + # Otherwise choose between using the fieldtype or some other simple merged type. + # The wrapper type never has restrictions on complexity, + # so try to use that to refine the estimated type too. + tni = _typename(widenconst(ai)) + if tni isa Const && tni === _typename(widenconst(bi)) + # A tmeet call may cause tyi to become complex, but since the inputs were + # strictly limited to being egal, this has no restrictions on complexity. + # (Otherwise, we would need to use <: and take the narrower one without + # intersection. See the similar comment in abstract_call_method.) + tyi = typeintersect(ft, (tni.val::Core.TypeName).wrapper) + else + # Since aty===bty, the fieldtype has no restrictions on complexity. + tyi = ft + end + end + fields[i] = tyi + if !anyrefine + anyrefine = has_nontrivial_const_info(tyi) || # constant information + tyi ⋤ ft # just a type-level information, but more precise than the declared type + end + end + return anyrefine ? PartialStruct(aty, fields) : aty + end + end + if isa(typea, PartialOpaque) && isa(typeb, PartialOpaque) && widenconst(typea) == widenconst(typeb) + if !(typea.source === typeb.source && + typea.parent === typeb.parent) return widenconst(typea) - end - - type_nfields = typea_nfields.val::Int - fields = Vector{Any}(undef, type_nfields) - anyconst = false - for i = 1:type_nfields - fields[i] = tmerge(getfield_tfunc(typea, Const(i)), - getfield_tfunc(typeb, Const(i))) - anyconst |= has_nontrivial_const_info(fields[i]) - end - return anyconst ? PartialStruct(widenconst(typea), fields) : - widenconst(typea) + end + return PartialOpaque(typea.typ, tmerge(typea.env, typeb.env), + typea.parent, typea.source) end # no special type-inference lattice, join the types typea, typeb = widenconst(typea), widenconst(typeb) @@ -372,9 +527,14 @@ function tmerge(@nospecialize(typea), @nospecialize(typeb)) # bail if everything isn't a well-formed DataType ti = types[i] uw = unwrap_unionall(ti) - (uw isa DataType && ti <: uw.name.wrapper) || return Any + uw isa DataType || return Any + ti <: uw.name.wrapper || return Any typenames[i] = uw.name end + u = Union{types...} + if issimpleenoughtype(u) + return u + end # see if any of the union elements have the same TypeName # in which case, simplify this tmerge by replacing it with # the widest possible version of itself (the wrapper) diff --git a/base/compiler/types.jl b/base/compiler/types.jl index 45a5bfcb121697..e594c233353d92 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -6,16 +6,184 @@ An abstract base class that allows multiple dispatch to determine the method of executing Julia code. The native Julia LLVM pipeline is enabled by using the `NativeInterpreter` concrete instantiation of this abstract class, others can be -swapped in as long as they follow the AbstractInterpreter API. +swapped in as long as they follow the `AbstractInterpreter` API. + +If `interp` is an `AbstractInterpreter`, it is expected to provide at least the following methods: +- `InferenceParams(interp)` - return an `InferenceParams` instance +- `OptimizationParams(interp)` - return an `OptimizationParams` instance +- `get_world_counter(interp)` - return the world age for this interpreter +- `get_inference_cache(interp)` - return the runtime inference cache +- `code_cache(interp)` - return the global inference cache +""" +abstract type AbstractInterpreter end + +struct ArgInfo + fargs::Union{Nothing,Vector{Any}} + argtypes::Vector{Any} +end + +struct TriState; state::UInt8; end +const ALWAYS_FALSE = TriState(0x00) +const ALWAYS_TRUE = TriState(0x01) +const TRISTATE_UNKNOWN = TriState(0x02) -All AbstractInterpreters are expected to provide at least the following methods: +function tristate_merge(old::TriState, new::TriState) + (old === ALWAYS_FALSE || new === ALWAYS_FALSE) && return ALWAYS_FALSE + old === TRISTATE_UNKNOWN && return old + return new +end -- InferenceParams(interp) - return an `InferenceParams` instance -- OptimizationParams(interp) - return an `OptimizationParams` instance -- get_world_counter(interp) - return the world age for this interpreter -- get_inference_cache(interp) - return the runtime inference cache """ -abstract type AbstractInterpreter; end + effects::Effects + +Represents computational effects of a method call. + +The effects are composed of the following set of different properties: +- `effects.consistent::TriState`: this method is guaranteed to return or terminate consistently +- `effect_free::TriState`: this method is free from externally semantically visible side effects +- `nothrow::TriState`: this method is guaranteed to not throw an exception +- `terminates::TriState`: this method is guaranteed to terminate +- `nonoverlayed::Bool`: indicates that any methods that may be called within this method + are not defined in an [overlayed method table](@ref OverlayMethodTable) +See [`Base.@assume_effects`](@ref) for more detailed explanation on the definitions of these properties. + +Along the abstract interpretation, `Effects` at each statement are analyzed locally and +they are merged into the single global `Effects` that represents the entire effects of +the analyzed method (see `tristate_merge!`). +Each effect property is represented as tri-state and managed separately. +The tri-state consists of `ALWAYS_TRUE`, `TRISTATE_UNKNOWN` and `ALWAYS_FALSE`. +An effect property is initialized with `ALWAYS_TRUE` and then transitioned towards +`TRISTATE_UNKNOWN` or `ALWAYS_FALSE`. When we find a statement that has some effect, +`ALWAYS_TRUE` is propagated if that effect is known to _always_ happen, otherwise +`TRISTATE_UNKNOWN` is propagated. If a property is known to be `ALWAYS_FALSE`, +there is no need to do additional analysis as it can not be refined anyway. +Note that however, within the current data-flow analysis design, it is hard to derive a global +conclusion from a local analysis on each statement, and as a result, the effect analysis +usually propagates `TRISTATE_UNKNOWN` currently. +""" +struct Effects + consistent::TriState + effect_free::TriState + nothrow::TriState + terminates::TriState + nonoverlayed::Bool + # This effect is currently only tracked in inference and modified + # :consistent before caching. We may want to track it in the future. + inbounds_taints_consistency::Bool +end +function Effects( + consistent::TriState, + effect_free::TriState, + nothrow::TriState, + terminates::TriState, + nonoverlayed::Bool) + return Effects( + consistent, + effect_free, + nothrow, + terminates, + nonoverlayed, + false) +end + +const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, true) +const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, TRISTATE_UNKNOWN, ALWAYS_TRUE, true) +const EFFECTS_UNKNOWN = Effects(TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, true) # mostly unknown, but it's not overlayed at least (e.g. it's not a call) +const EFFECTS_UNKNOWN′ = Effects(TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, TRISTATE_UNKNOWN, false) # unknown, really + +function Effects(e::Effects = EFFECTS_UNKNOWN′; + consistent::TriState = e.consistent, + effect_free::TriState = e.effect_free, + nothrow::TriState = e.nothrow, + terminates::TriState = e.terminates, + nonoverlayed::Bool = e.nonoverlayed, + inbounds_taints_consistency::Bool = e.inbounds_taints_consistency) + return Effects( + consistent, + effect_free, + nothrow, + terminates, + nonoverlayed, + inbounds_taints_consistency) +end + +is_consistent(effects::Effects) = effects.consistent === ALWAYS_TRUE +is_effect_free(effects::Effects) = effects.effect_free === ALWAYS_TRUE +is_nothrow(effects::Effects) = effects.nothrow === ALWAYS_TRUE +is_terminates(effects::Effects) = effects.terminates === ALWAYS_TRUE +is_nonoverlayed(effects::Effects) = effects.nonoverlayed + +is_concrete_eval_eligible(effects::Effects) = + is_consistent(effects) && + is_effect_free(effects) && + is_terminates(effects) + +is_total(effects::Effects) = + is_concrete_eval_eligible(effects) && + is_nothrow(effects) + +is_removable_if_unused(effects::Effects) = + is_effect_free(effects) && + is_terminates(effects) && + is_nothrow(effects) + +function encode_effects(e::Effects) + return (e.consistent.state << 0) | + (e.effect_free.state << 2) | + (e.nothrow.state << 4) | + (e.terminates.state << 6) | + (UInt32(e.nonoverlayed) << 8) +end +function decode_effects(e::UInt32) + return Effects( + TriState((e >> 0) & 0x03), + TriState((e >> 2) & 0x03), + TriState((e >> 4) & 0x03), + TriState((e >> 6) & 0x03), + _Bool( (e >> 8) & 0x01), + false) +end + +function tristate_merge(old::Effects, new::Effects) + return Effects( + tristate_merge( + old.consistent, new.consistent), + tristate_merge( + old.effect_free, new.effect_free), + tristate_merge( + old.nothrow, new.nothrow), + tristate_merge( + old.terminates, new.terminates), + old.nonoverlayed & new.nonoverlayed, + old.inbounds_taints_consistency | new.inbounds_taints_consistency) +end + +struct EffectsOverride + consistent::Bool + effect_free::Bool + nothrow::Bool + terminates_globally::Bool + terminates_locally::Bool +end + +function encode_effects_override(eo::EffectsOverride) + e = 0x00 + eo.consistent && (e |= 0x01) + eo.effect_free && (e |= 0x02) + eo.nothrow && (e |= 0x04) + eo.terminates_globally && (e |= 0x08) + eo.terminates_locally && (e |= 0x10) + return e +end + +function decode_effects_override(e::UInt8) + return EffectsOverride( + (e & 0x01) != 0x00, + (e & 0x02) != 0x00, + (e & 0x04) != 0x00, + (e & 0x08) != 0x00, + (e & 0x10) != 0x00) +end """ InferenceResult @@ -26,15 +194,20 @@ mutable struct InferenceResult linfo::MethodInstance argtypes::Vector{Any} overridden_by_const::BitVector - result # ::Type, or InferenceState if WIP - src #::Union{CodeInfo, OptimizationState, Nothing} # if inferred copy is available - function InferenceResult(linfo::MethodInstance, given_argtypes = nothing) - argtypes, overridden_by_const = matching_cache_argtypes(linfo, given_argtypes) - return new(linfo, argtypes, overridden_by_const, Any, nothing) + result # ::Type, or InferenceState if WIP + src # ::Union{CodeInfo, OptimizationState} if inferred copy is available, nothing otherwise + valid_worlds::WorldRange # if inference and optimization is finished + ipo_effects::Effects # if inference is finished + effects::Effects # if optimization is finished + argescapes # ::ArgEscapeCache if optimized, nothing otherwise + function InferenceResult(linfo::MethodInstance, + arginfo#=::Union{Nothing,Tuple{ArgInfo,InferenceState}}=# = nothing) + argtypes, overridden_by_const = matching_cache_argtypes(linfo, arginfo) + return new(linfo, argtypes, overridden_by_const, Any, nothing, + WorldRange(), Effects(), Effects(), nothing) end end - """ OptimizationParams @@ -44,27 +217,27 @@ struct OptimizationParams inlining::Bool # whether inlining is enabled inline_cost_threshold::Int # number of CPU cycles beyond which it's not worth inlining inline_nonleaf_penalty::Int # penalty for dynamic dispatch - inline_tupleret_bonus::Int # extra willingness for non-isbits tuple return types + inline_tupleret_bonus::Int # extra inlining willingness for non-concrete tuple return types (in hopes of splitting it up) inline_error_path_cost::Int # cost of (un-optimized) calls in blocks that throw + trust_inference::Bool + # Duplicating for now because optimizer inlining requires it. # Keno assures me this will be removed in the near future MAX_METHODS::Int MAX_TUPLE_SPLAT::Int MAX_UNION_SPLITTING::Int - unoptimize_throw_blocks::Bool - function OptimizationParams(; inlining::Bool = inlining_enabled(), inline_cost_threshold::Int = 100, inline_nonleaf_penalty::Int = 1000, - inline_tupleret_bonus::Int = 400, + inline_tupleret_bonus::Int = 250, inline_error_path_cost::Int = 20, max_methods::Int = 3, tuple_splat::Int = 32, union_splitting::Int = 4, - unoptimize_throw_blocks::Bool = true, + trust_inference::Bool = false ) return new( inlining, @@ -72,10 +245,10 @@ struct OptimizationParams inline_nonleaf_penalty, inline_tupleret_bonus, inline_error_path_cost, + trust_inference, max_methods, tuple_splat, - union_splitting, - unoptimize_throw_blocks, + union_splitting ) end end @@ -101,14 +274,14 @@ struct InferenceParams # before computing the set of matching methods MAX_UNION_SPLITTING::Int # the maximum number of union-tuples to swap / expand - # when inferring a call to _apply + # when inferring a call to _apply_iterate MAX_APPLY_UNION_ENUM::Int # parameters limiting large (tuple) types TUPLE_COMPLEXITY_LIMIT_DEPTH::Int - # when attempting to inlining _apply, abort the optimization if the tuple - # contains more than this many elements + # when attempting to inline _apply_iterate, abort the optimization if the + # tuple contains more than this many elements MAX_TUPLE_SPLAT::Int function InferenceParams(; @@ -164,7 +337,6 @@ struct NativeInterpreter <: AbstractInterpreter # incorrect, fail out loudly. @assert world <= get_world_counter() - return new( # Initially empty cache Vector{InferenceResult}(), @@ -184,30 +356,70 @@ InferenceParams(ni::NativeInterpreter) = ni.inf_params OptimizationParams(ni::NativeInterpreter) = ni.opt_params get_world_counter(ni::NativeInterpreter) = ni.world get_inference_cache(ni::NativeInterpreter) = ni.cache - -code_cache(ni::NativeInterpreter) = WorldView(GLOBAL_CI_CACHE, ni.world) +code_cache(ni::NativeInterpreter) = WorldView(GLOBAL_CI_CACHE, get_world_counter(ni)) """ lock_mi_inference(ni::NativeInterpreter, mi::MethodInstance) -Hint that `mi` is in inference to help accelerate bootstrapping. This helps limit the amount of wasted work we might do when inference is working on initially inferring itself by letting us detect when inference is already in progress and not running a second copy on it. This creates a data-race, but the entry point into this code from C (jl_type_infer) already includes detection and restriction on recursion, so it is hopefully mostly a benign problem (since it should really only happen during the first phase of bootstrapping that we encounter this flag). +Hint that `mi` is in inference to help accelerate bootstrapping. +This helps us limit the amount of wasted work we might do when inference is working on initially inferring itself +by letting us detect when inference is already in progress and not running a second copy on it. +This creates a data-race, but the entry point into this code from C (`jl_type_infer`) already includes detection and restriction on recursion, +so it is hopefully mostly a benign problem (since it should really only happen during the first phase of bootstrapping that we encounter this flag). """ -lock_mi_inference(ni::NativeInterpreter, mi::MethodInstance) = (mi.inInference = true; nothing) +lock_mi_inference(::NativeInterpreter, mi::MethodInstance) = (mi.inInference = true; nothing) +lock_mi_inference(::AbstractInterpreter, ::MethodInstance) = return """ - See lock_mi_inference +See `lock_mi_inference`. """ -unlock_mi_inference(ni::NativeInterpreter, mi::MethodInstance) = (mi.inInference = false; nothing) +unlock_mi_inference(::NativeInterpreter, mi::MethodInstance) = (mi.inInference = false; nothing) +unlock_mi_inference(::AbstractInterpreter, ::MethodInstance) = return """ -Emit an analysis remark during inference for the current line (`sv.pc`). These annotations are ignored -by the native interpreter, but can be used by external tooling to annotate -inference results. +Emit an analysis remark during inference for the current line (`sv.pc`). +These annotations are ignored by the native interpreter, but can be used by external tooling +to annotate inference results. """ -add_remark!(ni::NativeInterpreter, sv, s) = nothing +add_remark!(::AbstractInterpreter, sv#=::InferenceState=#, s) = return -may_optimize(ni::NativeInterpreter) = true -may_compress(ni::NativeInterpreter) = true -may_discard_trees(ni::NativeInterpreter) = true +may_optimize(::AbstractInterpreter) = true +may_compress(::AbstractInterpreter) = true +may_discard_trees(::AbstractInterpreter) = true +verbose_stmt_info(::AbstractInterpreter) = false -method_table(ai::AbstractInterpreter) = InternalMethodTable(get_world_counter(ai)) +""" + method_table(interp::AbstractInterpreter) -> MethodTableView + +Returns a method table this `interp` uses for method lookup. +External `AbstractInterpreter` can optionally return `OverlayMethodTable` here +to incorporate customized dispatches for the overridden methods. +""" +method_table(interp::AbstractInterpreter) = InternalMethodTable(get_world_counter(interp)) + +""" +By default `AbstractInterpreter` implements the following inference bail out logic: +- `bail_out_toplevel_call(::AbstractInterpreter, sig, ::InferenceState)`: bail out from inter-procedural inference when inferring top-level and non-concrete call site `callsig` +- `bail_out_call(::AbstractInterpreter, rt, ::InferenceState)`: bail out from inter-procedural inference when return type `rt` grows up to `Any` +- `bail_out_apply(::AbstractInterpreter, rt, ::InferenceState)`: bail out from `_apply_iterate` inference when return type `rt` grows up to `Any` + +It also bails out from local statement/frame inference when any lattice element gets down to `Bottom`, +but `AbstractInterpreter` doesn't provide a specific interface for configuring it. +""" +bail_out_toplevel_call(::AbstractInterpreter, @nospecialize(callsig), sv#=::InferenceState=#) = + return sv.restrict_abstract_call_sites && !isdispatchtuple(callsig) +bail_out_call(::AbstractInterpreter, @nospecialize(rt), sv#=::InferenceState=#) = + return rt === Any +bail_out_apply(::AbstractInterpreter, @nospecialize(rt), sv#=::InferenceState=#) = + return rt === Any + +""" + infer_compilation_signature(::AbstractInterpreter)::Bool + +For some call sites (for example calls to varargs methods), the signature to be compiled +and executed at run time can differ from the argument types known at the call site. +This flag controls whether we should always infer the compilation signature in addition +to the call site signature. +""" +infer_compilation_signature(::AbstractInterpreter) = false +infer_compilation_signature(::NativeInterpreter) = true diff --git a/base/compiler/typeutils.jl b/base/compiler/typeutils.jl index d6eb7305b1c8d9..75675e60e1ca42 100644 --- a/base/compiler/typeutils.jl +++ b/base/compiler/typeutils.jl @@ -4,13 +4,6 @@ # lattice utilities # ##################### -function rewrap(@nospecialize(t), @nospecialize(u)) - if isa(t, TypeVar) || isa(t, Type) - return rewrap_unionall(t, u) - end - return t -end - isType(@nospecialize t) = isa(t, DataType) && t.name === _TYPE_NAME # true if Type{T} is inlineable as constant T @@ -34,20 +27,81 @@ end function has_nontrivial_const_info(@nospecialize t) isa(t, PartialStruct) && return true - return isa(t, Const) && !isdefined(typeof(t.val), :instance) && !(isa(t.val, Type) && hasuniquerep(t.val)) + isa(t, PartialOpaque) && return true + isa(t, Const) || return false + val = t.val + return !isdefined(typeof(val), :instance) && !(isa(val, Type) && hasuniquerep(val)) end +has_const_info(@nospecialize x) = (!isa(x, Type) && !isvarargtype(x)) || isType(x) + # Subtyping currently intentionally answers certain queries incorrectly for kind types. For # some of these queries, this check can be used to somewhat protect against making incorrect # decisions based on incorrect subtyping. Note that this check, itself, is broken for # certain combinations of `a` and `b` where one/both isa/are `Union`/`UnionAll` type(s)s. -isnotbrokensubtype(@nospecialize(a), @nospecialize(b)) = (!iskindtype(b) || !isType(a) || hasuniquerep(a.parameters[1])) +isnotbrokensubtype(@nospecialize(a), @nospecialize(b)) = (!iskindtype(b) || !isType(a) || hasuniquerep(a.parameters[1]) || b <: a) -argtypes_to_type(argtypes::Array{Any,1}) = Tuple{anymap(widenconst, argtypes)...} +argtypes_to_type(argtypes::Array{Any,1}) = Tuple{anymap(@nospecialize(a) -> isvarargtype(a) ? a : widenconst(a), argtypes)...} function isknownlength(t::DataType) isvatuple(t) || return true - return length(t.parameters) > 0 && isa(unwrap_unionall(t.parameters[end]).parameters[2], Int) + va = t.parameters[end] + return isdefined(va, :N) && va.N isa Int +end + +# Compute the minimum number of initialized fields for a particular datatype +# (therefore also a lower bound on the number of fields) +function datatype_min_ninitialized(t::DataType) + isabstracttype(t) && return 0 + if t.name === NamedTuple_typename + names, types = t.parameters[1], t.parameters[2] + if names isa Tuple + return length(names) + end + t = argument_datatype(types) + t isa DataType || return 0 + t.name === Tuple.name || return 0 + end + if t.name === Tuple.name + n = length(t.parameters) + n == 0 && return 0 + va = t.parameters[n] + if isvarargtype(va) + n -= 1 + if isdefined(va, :N) + va = va.N + if va isa Int + n += va + end + end + end + return n + end + return length(t.name.names) - t.name.n_uninitialized +end + +has_concrete_subtype(d::DataType) = d.flags & 0x20 == 0x20 # n.b. often computed only after setting the type and layout fields + +# determine whether x is a valid lattice element tag +# For example, Type{v} is not valid if v is a value +# Accepts TypeVars also, since it assumes the user will rewrap it correctly +function valid_as_lattice(@nospecialize(x)) + x === Bottom && false + x isa TypeVar && return valid_as_lattice(x.ub) + x isa UnionAll && (x = unwrap_unionall(x)) + if x isa Union + # the Union constructor ensures this (and we'll recheck after + # operations that might remove the Union itself) + return true + end + if x isa DataType + if isType(x) + p = x.parameters[1] + p isa Type || p isa TypeVar || return false + end + return true + end + return false end # test if non-Type, non-TypeVar `x` can be used to parameterize a type @@ -61,21 +115,59 @@ function valid_tparam(@nospecialize(x)) return isa(x, Symbol) || isbits(x) end +function compatible_vatuple(a::DataType, b::DataType) + vaa = a.parameters[end] + vab = a.parameters[end] + if !(isvarargtype(vaa) && isvarargtype(vab)) + return isvarargtype(vaa) == isvarargtype(vab) + end + (isdefined(vaa, :N) == isdefined(vab, :N)) || return false + !isdefined(vaa, :N) && return true + return vaa.N === vab.N +end + # return an upper-bound on type `a` with type `b` removed # such that `return <: a` && `Union{return, b} == Union{a, b}` -function typesubtract(@nospecialize(a), @nospecialize(b)) +function typesubtract(@nospecialize(a), @nospecialize(b), MAX_UNION_SPLITTING::Int) if a <: b && isnotbrokensubtype(a, b) return Bottom end - if isa(a, Union) - return Union{typesubtract(a.a, b), - typesubtract(a.b, b)} + ua = unwrap_unionall(a) + if isa(ua, Union) + uua = typesubtract(rewrap_unionall(ua.a, a), b, MAX_UNION_SPLITTING) + uub = typesubtract(rewrap_unionall(ua.b, a), b, MAX_UNION_SPLITTING) + return Union{valid_as_lattice(uua) ? uua : Union{}, + valid_as_lattice(uub) ? uub : Union{}} elseif a isa DataType - if b isa DataType - if a.name === b.name === Tuple.name && length(a.types) == length(b.types) - ta = switchtupleunion(a) - if length(ta) > 1 - return typesubtract(Union{ta...}, b) + ub = unwrap_unionall(b) + if ub isa DataType + if a.name === ub.name === Tuple.name && + length(a.parameters) == length(ub.parameters) + if 1 < unionsplitcost(a.parameters) <= MAX_UNION_SPLITTING + ta = switchtupleunion(a) + return typesubtract(Union{ta...}, b, 0) + elseif b isa DataType + if !compatible_vatuple(a, b) + return a + end + # if exactly one element is not bottom after calling typesubtract + # then the result is all of the elements as normal except that one + notbottom = fill(false, length(a.parameters)) + for i = 1:length(notbottom) + ap = unwrapva(a.parameters[i]) + bp = unwrapva(b.parameters[i]) + notbottom[i] = !(ap <: bp && isnotbrokensubtype(ap, bp)) + end + let i = findfirst(notbottom) + if i !== nothing && findnext(notbottom, i + 1) === nothing + ta = collect(a.parameters) + ap = a.parameters[i] + bp = b.parameters[i] + (isvarargtype(ap) || isvarargtype(bp)) && return a + ta[i] = typesubtract(ap, bp, min(2, MAX_UNION_SPLITTING)) + return Tuple{ta...} + end + end end end end @@ -83,12 +175,7 @@ function typesubtract(@nospecialize(a), @nospecialize(b)) return a # TODO: improve this bound? end -function tvar_extent(@nospecialize t) - while t isa TypeVar - t = t.ub - end - return t -end +hasintersect(@nospecialize(a), @nospecialize(b)) = typeintersect(a, b) !== Bottom _typename(@nospecialize a) = Union{} _typename(a::TypeVar) = Core.TypeName @@ -107,19 +194,28 @@ function tuple_tail_elem(@nospecialize(init), ct::Vector{Any}) t = init for x in ct # FIXME: this is broken: it violates subtyping relations and creates invalid types with free typevars - t = tmerge(t, tvar_extent(unwrapva(x))) + t = tmerge(t, unwraptv(unwrapva(x))) end return Vararg{widenconst(t)} end -function countunionsplit(atypes::Union{SimpleVector,Vector{Any}}) +# Gives a cost function over the effort to switch a tuple-union representation +# as a cartesian product, relative to the size of the original representation. +# Thus, we count the longest element as being roughly invariant to being inside +# or outside of the Tuple/Union nesting, though somewhat more expensive to be +# outside than inside because the representation is larger (because and it +# informs the callee whether any splitting is possible). +function unionsplitcost(argtypes::Union{SimpleVector,Vector{Any}}) nu = 1 - for ti in atypes + max = 2 + for ti in argtypes if isa(ti, Union) - nu, ovf = Core.Intrinsics.checked_smul_int(nu, unionlen(ti::Union)) - if ovf - return typemax(Int) + nti = unionlen(ti) + if nti > max + max, nti = nti, max end + nu, ovf = Core.Intrinsics.checked_smul_int(nu, nti) + ovf && return typemax(Int) end end return nu @@ -133,10 +229,16 @@ function switchtupleunion(@nospecialize(ty)) return _switchtupleunion(Any[tparams...], length(tparams), [], ty) end +switchtupleunion(argtypes::Vector{Any}) = _switchtupleunion(argtypes, length(argtypes), [], nothing) + function _switchtupleunion(t::Vector{Any}, i::Int, tunion::Vector{Any}, @nospecialize(origt)) if i == 0 - tpl = rewrap_unionall(Tuple{t...}, origt) - push!(tunion, tpl) + if origt === nothing + push!(tunion, copy(t)) + else + tpl = rewrap_unionall(Tuple{t...}, origt) + push!(tunion, tpl) + end else ti = t[i] if isa(ti, Union) @@ -154,25 +256,47 @@ end # unioncomplexity estimates the number of calls to `tmerge` to obtain the given type by # counting the Union instances, taking also into account those hidden in a Tuple or UnionAll -function unioncomplexity(u::Union) - return unioncomplexity(u.a) + unioncomplexity(u.b) + 1 -end -function unioncomplexity(t::DataType) - t.name === Tuple.name || isvarargtype(t) || return 0 - c = 0 - for ti in t.parameters - c = max(c, unioncomplexity(ti)) +unioncomplexity(@nospecialize x) = _unioncomplexity(x)::Int +function _unioncomplexity(@nospecialize x) + if isa(x, DataType) + x.name === Tuple.name || isvarargtype(x) || return 0 + c = 0 + for ti in x.parameters + c = max(c, unioncomplexity(ti)) + end + return c + elseif isa(x, Union) + return unioncomplexity(x.a) + unioncomplexity(x.b) + 1 + elseif isa(x, UnionAll) + return max(unioncomplexity(x.body), unioncomplexity(x.var.ub)) + elseif isa(x, TypeofVararg) + return isdefined(x, :T) ? unioncomplexity(x.T) : 0 + else + return 0 end - return c end -unioncomplexity(u::UnionAll) = max(unioncomplexity(u.body), unioncomplexity(u.var.ub)) -unioncomplexity(@nospecialize(x)) = 0 -function improvable_via_constant_propagation(@nospecialize(t)) - if isconcretetype(t) && t <: Tuple - for p in t.parameters - p === DataType && return true +# convert a Union of Tuple types to a Tuple of Unions +function unswitchtupleunion(u::Union) + ts = uniontypes(u) + n = -1 + for t in ts + if t isa DataType && t.name === Tuple.name && length(t.parameters) != 0 && !isvarargtype(t.parameters[end]) + if n == -1 + n = length(t.parameters) + elseif n != length(t.parameters) + return u + end + else + return u end end - return false + Tuple{Any[ Union{Any[(t::DataType).parameters[i] for t in ts]...} for i in 1:n ]...} +end + +function unwraptv(@nospecialize t) + while isa(t, TypeVar) + t = t.ub + end + return t end diff --git a/base/compiler/utilities.jl b/base/compiler/utilities.jl index 834d2759f3b753..07281a353dbb6b 100644 --- a/base/compiler/utilities.jl +++ b/base/compiler/utilities.jl @@ -4,7 +4,7 @@ # generic # ########### -if !isdefined(@__MODULE__, Symbol("@timeit")) +if !@isdefined(var"@timeit") # This is designed to allow inserting timers when loading a second copy # of inference for performing performance experiments. macro timeit(args...) @@ -19,6 +19,8 @@ function _any(@nospecialize(f), a) end return false end +any(@nospecialize(f), itr) = _any(f, itr) +any(itr) = _any(identity, itr) function _all(@nospecialize(f), a) for x in a @@ -26,6 +28,8 @@ function _all(@nospecialize(f), a) end return true end +all(@nospecialize(f), itr) = _all(f, itr) +all(itr) = _all(identity, itr) function contains_is(itr, @nospecialize(x)) for y in itr @@ -48,7 +52,7 @@ function istopfunction(@nospecialize(f), name::Symbol) tn = typeof(f).name if tn.mt.name === name top = _topmod(tn.module) - return isdefined(top, name) && isconst(top, name) && f === getfield(top, name) + return isdefined(top, name) && isconst(top, name) && f === getglobal(top, name) end return false end @@ -59,7 +63,7 @@ end # Meta expression head, these generally can't be deleted even when they are # in a dead branch but can be ignored when analyzing uses/liveness. -is_meta_expr_head(head::Symbol) = (head === :inbounds || head === :boundscheck || head === :meta || head === :loopinfo) +is_meta_expr_head(head::Symbol) = head === :boundscheck || head === :meta || head === :loopinfo sym_isless(a::Symbol, b::Symbol) = ccall(:strcmp, Int32, (Ptr{UInt8}, Ptr{UInt8}), a, b) < 0 @@ -72,26 +76,43 @@ function quoted(@nospecialize(x)) return is_self_quoting(x) ? x : QuoteNode(x) end -function is_inlineable_constant(@nospecialize(x)) - if x isa Type || x isa Symbol - return true +function count_const_size(@nospecialize(x), count_self::Bool = true) + (x isa Type || x isa Symbol) && return 0 + ismutable(x) && return MAX_INLINE_CONST_SIZE + 1 + isbits(x) && return Core.sizeof(x) + dt = typeof(x) + sz = count_self ? sizeof(dt) : 0 + sz > MAX_INLINE_CONST_SIZE && return MAX_INLINE_CONST_SIZE + 1 + dtfd = DataTypeFieldDesc(dt) + for i = 1:nfields(x) + isdefined(x, i) || continue + f = getfield(x, i) + if !dtfd[i].isptr && datatype_pointerfree(typeof(f)) + continue + end + sz += count_const_size(f, dtfd[i].isptr) + sz > MAX_INLINE_CONST_SIZE && return MAX_INLINE_CONST_SIZE + 1 end - return isbits(x) && Core.sizeof(x) <= MAX_INLINE_CONST_SIZE + return sz +end + +function is_inlineable_constant(@nospecialize(x)) + return count_const_size(x) <= MAX_INLINE_CONST_SIZE end ########################### # MethodInstance/CodeInfo # ########################### -function invoke_api(li::CodeInstance) - return ccall(:jl_invoke_api, Cint, (Any,), li) -end +invoke_api(li::CodeInstance) = ccall(:jl_invoke_api, Cint, (Any,), li) +use_const_api(li::CodeInstance) = invoke_api(li) == 2 -function get_staged(li::MethodInstance) - may_invoke_generator(li) || return nothing +function get_staged(mi::MethodInstance) + may_invoke_generator(mi) || return nothing try # user code might throw errors – ignore them - return ccall(:jl_code_for_staged, Any, (Any,), li)::CodeInfo + ci = ccall(:jl_code_for_staged, Any, (Any,), mi)::CodeInfo + return ci catch return nothing end @@ -116,92 +137,92 @@ function retrieve_code_info(linfo::MethodInstance) c.parent = linfo return c end + return nothing end -# Get at the nonfunction_mt, which happens to be the mt of SimpleVector -const nonfunction_mt = typename(SimpleVector).mt - -function get_compileable_sig(method::Method, @nospecialize(atypes), sparams::SimpleVector) - isa(atypes, DataType) || return Nothing - mt = ccall(:jl_method_table_for, Any, (Any,), atypes) +function get_compileable_sig(method::Method, @nospecialize(atype), sparams::SimpleVector) + isa(atype, DataType) || return nothing + mt = ccall(:jl_method_table_for, Any, (Any,), atype) mt === nothing && return nothing return ccall(:jl_normalize_to_compilable_sig, Any, (Any, Any, Any, Any), - mt, atypes, sparams, method) + mt, atype, sparams, method) +end + +isa_compileable_sig(@nospecialize(atype), method::Method) = + !iszero(ccall(:jl_isa_compileable_sig, Int32, (Any, Any), atype, method)) + +# eliminate UnionAll vars that might be degenerate due to having identical bounds, +# or a concrete upper bound and appearing covariantly. +function subst_trivial_bounds(@nospecialize(atype)) + if !isa(atype, UnionAll) + return atype + end + v = atype.var + if isconcretetype(v.ub) || v.lb === v.ub + subst = try + atype{v.ub} + catch + # Note in rare cases a var bound might not be valid to substitute. + nothing + end + if subst !== nothing + return subst_trivial_bounds(subst) + end + end + return UnionAll(v, subst_trivial_bounds(atype.body)) +end + +# If removing trivial vars from atype results in an equivalent type, use that +# instead. Otherwise we can get a case like issue #38888, where a signature like +# f(x::S) where S<:Int +# gets cached and matches a concrete dispatch case. +function normalize_typevars(method::Method, @nospecialize(atype), sparams::SimpleVector) + at2 = subst_trivial_bounds(atype) + if at2 !== atype && at2 == atype + atype = at2 + sp_ = ccall(:jl_type_intersection_with_env, Any, (Any, Any), at2, method.sig)::SimpleVector + sparams = sp_[2]::SimpleVector + end + return atype, sparams end # get a handle to the unique specialization object representing a particular instantiation of a call -function specialize_method(method::Method, @nospecialize(atypes), sparams::SimpleVector, preexisting::Bool=false, compilesig::Bool=false) +function specialize_method(method::Method, @nospecialize(atype), sparams::SimpleVector; preexisting::Bool=false, compilesig::Bool=false) + if isa(atype, UnionAll) + atype, sparams = normalize_typevars(method, atype, sparams) + end if compilesig - new_atypes = get_compileable_sig(method, atypes, sparams) - new_atypes === nothing && return nothing - atypes = new_atypes + new_atype = get_compileable_sig(method, atype, sparams) + new_atype === nothing && return nothing + atype = new_atype end if preexisting # check cached specializations # for an existing result stored there - return ccall(:jl_specializations_lookup, Any, (Any, Any), method, atypes) + return ccall(:jl_specializations_lookup, Any, (Any, Any), method, atype)::Union{Nothing,MethodInstance} end - return ccall(:jl_specializations_get_linfo, Ref{MethodInstance}, (Any, Any, Any), method, atypes, sparams) + return ccall(:jl_specializations_get_linfo, Ref{MethodInstance}, (Any, Any, Any), method, atype, sparams) end -function specialize_method(match::MethodMatch, preexisting::Bool=false, compilesig::Bool=false) - return specialize_method(match.method, match.spec_types, match.sparams, preexisting, compilesig) +function specialize_method(match::MethodMatch; kwargs...) + return specialize_method(match.method, match.spec_types, match.sparams; kwargs...) end -# This function is used for computing alternate limit heuristics -function method_for_inference_heuristics(method::Method, @nospecialize(sig), sparams::SimpleVector) - if isdefined(method, :generator) && method.generator.expand_early && may_invoke_generator(method, sig, sparams) - method_instance = specialize_method(method, sig, sparams, false) - if isa(method_instance, MethodInstance) - cinfo = get_staged(method_instance) - if isa(cinfo, CodeInfo) - method2 = cinfo.method_for_inference_limit_heuristics - if method2 isa Method - return method2 - end - end - end +######### +# types # +######### + +function singleton_type(@nospecialize(ft)) + if isa(ft, Const) + return ft.val + elseif isconstType(ft) + return ft.parameters[1] + elseif ft isa DataType && isdefined(ft, :instance) + return ft.instance end return nothing end -argextype(@nospecialize(x), state) = argextype(x, state.src, state.sptypes, state.slottypes) - -const empty_slottypes = Any[] - -function argextype(@nospecialize(x), src, sptypes::Vector{Any}, slottypes::Vector{Any} = empty_slottypes) - if isa(x, Expr) - if x.head === :static_parameter - return sptypes[x.args[1]] - elseif x.head === :boundscheck - return Bool - elseif x.head === :copyast - return argextype(x.args[1], src, sptypes, slottypes) - end - @assert false "argextype only works on argument-position values" - elseif isa(x, SlotNumber) - return slottypes[(x::SlotNumber).id] - elseif isa(x, TypedSlot) - return (x::TypedSlot).typ - elseif isa(x, SSAValue) - return abstract_eval_ssavalue(x::SSAValue, src) - elseif isa(x, Argument) - return isa(src, IncrementalCompact) ? src.ir.argtypes[x.n] : - isa(src, IRCode) ? src.argtypes[x.n] : - slottypes[x.n] - elseif isa(x, QuoteNode) - return Const((x::QuoteNode).value) - elseif isa(x, GlobalRef) - return abstract_eval_global(x.mod, (x::GlobalRef).name) - elseif isa(x, PhiNode) - return Any - elseif isa(x, PiNode) - return x.typ - else - return Const(x) - end -end - ################### # SSAValues/Slots # ################### @@ -219,6 +240,8 @@ function find_ssavalue_uses(body::Vector{Any}, nvals::Int) push!(uses[e.id], line) elseif isa(e, Expr) find_ssavalue_uses(e, uses, line) + elseif isa(e, PhiNode) + find_ssavalue_uses(e, uses, line) end end return uses @@ -239,6 +262,14 @@ function find_ssavalue_uses(e::Expr, uses::Vector{BitSet}, line::Int) end end +function find_ssavalue_uses(e::PhiNode, uses::Vector{BitSet}, line::Int) + for val in e.values + if isa(val, SSAValue) + push!(uses[val.id], line) + end + end +end + function is_throw_call(e::Expr) if e.head === :call f = e.args[1] @@ -252,25 +283,27 @@ function is_throw_call(e::Expr) return false end -function find_throw_blocks(code::Vector{Any}, ir = RefValue{IRCode}()) +function mark_throw_blocks!(src::CodeInfo, handler_at::Vector{Int}) + for stmt in find_throw_blocks(src.code, handler_at) + src.ssaflags[stmt] |= IR_FLAG_THROW_BLOCK + end + return nothing +end + +function find_throw_blocks(code::Vector{Any}, handler_at::Vector{Int}) stmts = BitSet() n = length(code) - try_depth = 0 for i in n:-1:1 s = code[i] if isa(s, Expr) - if s.head === :enter - try_depth -= 1 - elseif s.head === :leave - try_depth += (s.args[1]::Int) - elseif s.head === :gotoifnot - tgt = s.args[2]::Int - if i+1 in stmts && tgt in stmts + if s.head === :gotoifnot + if i+1 in stmts && s.args[2]::Int in stmts push!(stmts, i) end elseif s.head === :return + # see `ReturnNode` handling elseif is_throw_call(s) - if try_depth == 0 + if handler_at[i] == 0 push!(stmts, i) end elseif i+1 in stmts @@ -281,22 +314,12 @@ function find_throw_blocks(code::Vector{Any}, ir = RefValue{IRCode}()) # (where !isdefined(s, :val)) as `throw` points, but that can cause # worse codegen around the call site (issue #37558) elseif isa(s, GotoNode) - tgt = s.label - if isassigned(ir) - tgt = first(ir[].cfg.blocks[tgt].stmts) - end - if tgt in stmts + if s.label in stmts push!(stmts, i) end elseif isa(s, GotoIfNot) - if i+1 in stmts - tgt = s.dest::Int - if isassigned(ir) - tgt = first(ir[].cfg.blocks[tgt].stmts) - end - if tgt in stmts - push!(stmts, i) - end + if i+1 in stmts && s.dest in stmts + push!(stmts, i) end elseif i+1 in stmts push!(stmts, i) @@ -319,12 +342,12 @@ inlining_enabled() = (JLOptions().can_inline == 1) function coverage_enabled(m::Module) ccall(:jl_generating_output, Cint, ()) == 0 || return false # don't alter caches cov = JLOptions().code_coverage - if cov == 1 + if cov == 1 # user m = moduleroot(m) m === Core && return false isdefined(Main, :Base) && m === Main.Base && return false return true - elseif cov == 2 + elseif cov == 2 # all return true end return false diff --git a/base/compiler/validation.jl b/base/compiler/validation.jl index 1a232a290b3a7e..0931686184a2e0 100644 --- a/base/compiler/validation.jl +++ b/base/compiler/validation.jl @@ -1,9 +1,10 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license # Expr head => argument count bounds -const VALID_EXPR_HEADS = IdDict{Any,Any}( +const VALID_EXPR_HEADS = IdDict{Symbol,UnitRange{Int}}( :call => 1:typemax(Int), :invoke => 2:typemax(Int), + :invoke_modify => 3:typemax(Int), :static_parameter => 1:1, :(&) => 1:1, :(=) => 2:2, @@ -16,11 +17,13 @@ const VALID_EXPR_HEADS = IdDict{Any,Any}( :leave => 1:1, :pop_exception => 1:1, :inbounds => 1:1, + :inline => 1:1, + :noinline => 1:1, :boundscheck => 0:0, :copyast => 1:1, :meta => 0:typemax(Int), :global => 1:1, - :foreigncall => 5:typemax(Int), # name, RT, AT, nreq, cconv, args..., roots... + :foreigncall => 5:typemax(Int), # name, RT, AT, nreq, (cconv, effects), args..., roots... :cfunction => 5:5, :isdefined => 1:1, :code_coverage_effect => 0:0, @@ -28,7 +31,10 @@ const VALID_EXPR_HEADS = IdDict{Any,Any}( :gc_preserve_begin => 0:typemax(Int), :gc_preserve_end => 0:typemax(Int), :thunk => 1:1, - :throw_undef_if_not => 2:2 + :throw_undef_if_not => 2:2, + :aliasscope => 0:0, + :popaliasscope => 0:0, + :new_opaque_closure => 4:typemax(Int) ) # @enum isn't defined yet, otherwise I'd use it for this @@ -42,13 +48,15 @@ const EMPTY_SLOTNAMES = "slotnames field is empty" const SLOTFLAGS_MISMATCH = "length(slotnames) < length(slotflags)" const SSAVALUETYPES_MISMATCH = "not all SSAValues in AST have a type in ssavaluetypes" const SSAVALUETYPES_MISMATCH_UNINFERRED = "uninferred CodeInfo ssavaluetypes field does not equal the number of present SSAValues" +const SSAFLAGS_MISMATCH = "not all SSAValues have a corresponding `ssaflags`" const NON_TOP_LEVEL_METHOD = "encountered `Expr` head `:method` in non-top-level code (i.e. `nargs` > 0)" const NON_TOP_LEVEL_GLOBAL = "encountered `Expr` head `:global` in non-top-level code (i.e. `nargs` > 0)" const SIGNATURE_NARGS_MISMATCH = "method signature does not match number of method arguments" const SLOTNAMES_NARGS_MISMATCH = "CodeInfo for method contains fewer slotnames than the number of method arguments" +const INVALID_SIGNATURE_OPAQUE_CLOSURE = "invalid signature of method for opaque closure - `sig` field must always be set to `Tuple`" struct InvalidCodeError <: Exception - kind::AbstractString + kind::String meta::Any end InvalidCodeError(kind::AbstractString) = InvalidCodeError(kind, nothing) @@ -73,7 +81,7 @@ end function _validate_val!(@nospecialize(x), errors, ssavals::BitSet) if isa(x, Expr) - if x.head === :call || x.head === :invoke + if x.head === :call || x.head === :invoke || x.head === :invoke_modify f = x.args[1] if f isa GlobalRef && (f.name === :cglobal) && x.head === :call # TODO: these are not yet linearized @@ -133,12 +141,13 @@ function validate_code!(errors::Vector{>:InvalidCodeError}, c::CodeInfo, is_top_ end validate_val!(lhs) validate_val!(rhs) - elseif head === :call || head === :invoke || head === :gc_preserve_end || head === :meta || + elseif head === :call || head === :invoke || x.head === :invoke_modify || + head === :gc_preserve_end || head === :meta || head === :inbounds || head === :foreigncall || head === :cfunction || head === :const || head === :enter || head === :leave || head === :pop_exception || head === :method || head === :global || head === :static_parameter || head === :new || head === :splatnew || head === :thunk || head === :loopinfo || - head === :throw_undef_if_not || head === :code_coverage_effect + head === :throw_undef_if_not || head === :code_coverage_effect || head === :inline || head === :noinline validate_val!(x) else # TODO: nothing is actually in statement position anymore @@ -176,12 +185,16 @@ function validate_code!(errors::Vector{>:InvalidCodeError}, c::CodeInfo, is_top_ nssavals = length(c.code) !is_top_level && nslotnames == 0 && push!(errors, InvalidCodeError(EMPTY_SLOTNAMES)) nslotnames < nslotflags && push!(errors, InvalidCodeError(SLOTFLAGS_MISMATCH, (nslotnames, nslotflags))) - if c.inferred - nssavaluetypes = length(c.ssavaluetypes) + ssavaluetypes = c.ssavaluetypes + if isa(ssavaluetypes, Vector{Any}) + nssavaluetypes = length(ssavaluetypes) nssavaluetypes < nssavals && push!(errors, InvalidCodeError(SSAVALUETYPES_MISMATCH, (nssavals, nssavaluetypes))) else - c.ssavaluetypes != nssavals && push!(errors, InvalidCodeError(SSAVALUETYPES_MISMATCH_UNINFERRED, (nssavals, c.ssavaluetypes))) + nssavaluetypes = ssavaluetypes::Int + nssavaluetypes ≠ nssavals && push!(errors, InvalidCodeError(SSAVALUETYPES_MISMATCH_UNINFERRED, (nssavals, nssavaluetypes))) end + nssaflags = length(c.ssaflags) + nssavals ≠ nssaflags && push!(errors, InvalidCodeError(SSAFLAGS_MISMATCH, (nssavals, nssaflags))) return errors end @@ -202,8 +215,10 @@ function validate_code!(errors::Vector{>:InvalidCodeError}, mi::Core.MethodInsta else m = mi.def::Method mnargs = m.nargs - n_sig_params = length(Core.Compiler.unwrap_unionall(m.sig).parameters) - if (m.isva ? (n_sig_params < (mnargs - 1)) : (n_sig_params != mnargs)) + n_sig_params = length((unwrap_unionall(m.sig)::DataType).parameters) + if m.is_for_opaque_closure + m.sig === Tuple || push!(errors, InvalidCodeError(INVALID_SIGNATURE_OPAQUE_CLOSURE, (m.sig, m.isva))) + elseif (m.isva ? (n_sig_params < (mnargs - 1)) : (n_sig_params != mnargs)) push!(errors, InvalidCodeError(SIGNATURE_NARGS_MISMATCH, (m.isva, n_sig_params, mnargs))) end end @@ -232,7 +247,7 @@ end function is_valid_rvalue(@nospecialize(x)) is_valid_argument(x) && return true - if isa(x, Expr) && x.head in (:new, :splatnew, :the_exception, :isdefined, :call, :invoke, :foreigncall, :cfunction, :gc_preserve_begin, :copyast) + if isa(x, Expr) && x.head in (:new, :splatnew, :the_exception, :isdefined, :call, :invoke, :invoke_modify, :foreigncall, :cfunction, :gc_preserve_begin, :copyast) return true end return false diff --git a/base/complex.jl b/base/complex.jl index 8aec9cf1301ee1..f68e519386d939 100644 --- a/base/complex.jl +++ b/base/complex.jl @@ -7,6 +7,8 @@ Complex number type with real and imaginary part of type `T`. `ComplexF16`, `ComplexF32` and `ComplexF64` are aliases for `Complex{Float16}`, `Complex{Float32}` and `Complex{Float64}` respectively. + +See also: [`Real`](@ref), [`complex`](@ref), [`real`](@ref). """ struct Complex{T<:Real} <: Number re::T @@ -20,10 +22,15 @@ Complex(x::Real) = Complex(x, zero(x)) The imaginary unit. +See also: [`imag`](@ref), [`angle`](@ref), [`complex`](@ref). + # Examples ```jldoctest julia> im * im -1 + 0im + +julia> (2.0 + 3im)^2 +-5.0 + 12.0im ``` """ const im = Complex(false, true) @@ -54,6 +61,8 @@ float(::Type{Complex{T}}) where {T} = Complex{float(T)} Return the real part of the complex number `z`. +See also: [`imag`](@ref), [`reim`](@ref), [`complex`](@ref), [`isreal`](@ref), [`Real`](@ref). + # Examples ```jldoctest julia> real(1 + 3im) @@ -67,6 +76,8 @@ real(z::Complex) = z.re Return the imaginary part of the complex number `z`. +See also: [`conj`](@ref), [`reim`](@ref), [`adjoint`](@ref), [`angle`](@ref). + # Examples ```jldoctest julia> imag(1 + 3im) @@ -80,7 +91,7 @@ imag(x::Real) = zero(x) """ reim(z) -Return both the real and imaginary parts of the complex number `z`. +Return a tuple of the real and imaginary parts of the complex number `z`. # Examples ```jldoctest @@ -254,6 +265,8 @@ end Compute the complex conjugate of a complex number `z`. +See also: [`angle`](@ref), [`adjoint`](@ref). + # Examples ```jldoctest julia> conj(1 + 3im) @@ -334,30 +347,37 @@ muladd(z::Complex, w::Complex, x::Real) = function /(a::Complex{T}, b::Complex{T}) where T<:Real are = real(a); aim = imag(a); bre = real(b); bim = imag(b) - if abs(bre) <= abs(bim) - if isinf(bre) && isinf(bim) - r = sign(bre)/sign(bim) - else - r = bre / bim + if (isinf(bre) | isinf(bim)) + if isfinite(a) + return complex(zero(T)*sign(are)*sign(bre), -zero(T)*sign(aim)*sign(bim)) end + return T(NaN)+T(NaN)*im + end + if abs(bre) <= abs(bim) + r = bre / bim den = bim + r*bre Complex((are*r + aim)/den, (aim*r - are)/den) else - if isinf(bre) && isinf(bim) - r = sign(bim)/sign(bre) - else - r = bim / bre - end + r = bim / bre den = bre + r*bim Complex((are + aim*r)/den, (aim - are*r)/den) end end -inv(z::Complex{<:Union{Float16,Float32}}) = - oftype(z, inv(widen(z))) - -/(z::Complex{T}, w::Complex{T}) where {T<:Union{Float16,Float32}} = - oftype(z, widen(z)*inv(widen(w))) +function /(z::Complex{T}, w::Complex{T}) where {T<:Union{Float16,Float32}} + c, d = reim(widen(w)) + a, b = reim(widen(z)) + if (isinf(c) | isinf(d)) + if isfinite(z) + return complex(zero(T)*sign(real(z))*sign(real(w)), -zero(T)*sign(imag(z))*sign(imag(w))) + end + return T(NaN)+T(NaN)*im + end + mag = inv(muladd(c, c, d^2)) + re_part = muladd(a, c, b*d) + im_part = muladd(b, c, -a*d) + return oftype(z, Complex(re_part*mag, im_part*mag)) +end # robust complex division for double precision # variables are scaled & unscaled to avoid over/underflow, if necessary @@ -369,7 +389,12 @@ function /(z::ComplexF64, w::ComplexF64) a, b = reim(z); c, d = reim(w) absa = abs(a); absb = abs(b); ab = absa >= absb ? absa : absb # equiv. to max(abs(a),abs(b)) but without NaN-handling (faster) absc = abs(c); absd = abs(d); cd = absc >= absd ? absc : absd - + if (isinf(c) | isinf(d)) + if isfinite(z) + return complex(0.0*sign(a)*sign(c), -0.0*sign(b)*sign(d)) + end + return NaN+NaN*im + end halfov = 0.5*floatmax(Float64) # overflow threshold twounϵ = floatmin(Float64)*2.0/eps(Float64) # underflow threshold @@ -436,32 +461,42 @@ function robust_cdiv2(a::Float64, b::Float64, c::Float64, d::Float64, r::Float64 end end +function inv(z::Complex{T}) where T<:Union{Float16,Float32} + c, d = reim(widen(z)) + (isinf(c) | isinf(d)) && return complex(copysign(zero(T), c), flipsign(-zero(T), d)) + mag = inv(muladd(c, c, d^2)) + return oftype(z, Complex(c*mag, -d*mag)) +end function inv(w::ComplexF64) c, d = reim(w) (isinf(c) | isinf(d)) && return complex(copysign(0.0, c), flipsign(-0.0, d)) - half = 0.5 - two = 2.0 - cd = max(abs(c), abs(d)) - ov = floatmax(c) - un = floatmin(c) - ϵ = eps(Float64) - bs = two/(ϵ*ϵ) + absc, absd = abs(c), abs(d) + cd = ifelse(absc>absd, absc, absd) # cheap `max`: don't need sign- and nan-checks here + + ϵ = eps(Float64) + bs = 2/(ϵ*ϵ) + + # scaling s = 1.0 - cd >= half*ov && (c=half*c; d=half*d; s=s*half) # scale down c,d - cd <= un*two/ϵ && (c=c*bs; d=d*bs; s=s*bs ) # scale up c,d - if abs(d)<=abs(c) - r = d/c - t = 1.0/(c+d*r) - p = t - q = -r * t + if cd >= floatmax(Float64)/2 + c *= 0.5; d *= 0.5; s = 0.5 # scale down c, d + elseif cd <= 2floatmin(Float64)/ϵ + c *= bs; d *= bs; s = bs # scale up c, d + end + + # inversion operations + if absd <= absc + p, q = robust_cinv(c, d) else - c, d = d, c - r = d/c - t = 1.0/(c+d*r) - p = r * t - q = -t + q, p = robust_cinv(-d, -c) end - return ComplexF64(p*s,q*s) # undo scaling + return ComplexF64(p*s, q*s) # undo scaling +end +function robust_cinv(c::Float64, d::Float64) + r = d/c + p = inv(muladd(d, r, c)) + q = -r*p + return p, q end function ssqs(x::T, y::T) where T<:Real @@ -518,16 +553,12 @@ end # return Complex(abs(iz)/r/2, copysign(r,iz)) # end -# compute exp(im*theta) -function cis(theta::Real) - s, c = sincos(theta) - Complex(c, s) -end - """ - cis(z) + cis(x) + +More efficient method for `exp(im*x)` by using Euler's formula: ``cos(x) + i sin(x) = \\exp(i x)``. -Return ``\\exp(iz)``. +See also [`cispi`](@ref), [`sincos`](@ref), [`exp`](@ref), [`angle`](@ref). # Examples ```jldoctest @@ -535,17 +566,52 @@ julia> cis(π) ≈ -1 true ``` """ +function cis end +function cis(theta::Real) + s, c = sincos(theta) + Complex(c, s) +end + function cis(z::Complex) v = exp(-imag(z)) s, c = sincos(real(z)) Complex(v * c, v * s) end +""" + cispi(x) + +More accurate method for `cis(pi*x)` (especially for large `x`). + +See also [`cis`](@ref), [`sincospi`](@ref), [`exp`](@ref), [`angle`](@ref). + +# Examples +```jldoctest +julia> cispi(10000) +1.0 + 0.0im + +julia> cispi(0.25 + 1im) +0.030556854645952924 + 0.030556854645952924im +``` + +!!! compat "Julia 1.6" + This function requires Julia 1.6 or later. +""" +function cispi end +cispi(theta::Real) = Complex(reverse(sincospi(theta))...) + +function cispi(z::Complex) + sipi, copi = sincospi(z) + return complex(real(copi) - imag(sipi), imag(copi) + real(sipi)) +end + """ angle(z) Compute the phase angle in radians of a complex number `z`. +See also: [`atan`](@ref), [`cis`](@ref). + # Examples ```jldoctest julia> rad2deg(angle(1 + im)) diff --git a/base/condition.jl b/base/condition.jl index 0efbd2c897da9a..4965b43a7019b4 100644 --- a/base/condition.jl +++ b/base/condition.jl @@ -5,7 +5,7 @@ @noinline function concurrency_violation() # can be useful for debugging #try; error(); catch; ccall(:jlbacktrace, Cvoid, ()); end - error("concurrency violation detected") + throw(ConcurrencyViolationError("lock must be held")) end """ @@ -34,7 +34,7 @@ assert_havelock(l::AbstractLock, tid::Nothing) = concurrency_violation() This struct does not implement a real lock, but instead pretends to be always locked on the original thread it was allocated on, and simply ignores all other interactions. -It also does not synchronize tasks; for that use a real lock such as [`RecursiveLock`](@ref). +It also does not synchronize tasks; for that use a real lock such as [`ReentrantLock`](@ref). This can be used in the place of a real lock to, instead, simply and cheaply assert that the operation is only occurring on a single cooperatively-scheduled thread. It is thus functionally equivalent to allocating a real, recursive, task-unaware lock @@ -61,12 +61,12 @@ Abstract implementation of a condition object for synchronizing tasks objects with a given lock. """ struct GenericCondition{L<:AbstractLock} - waitq::InvasiveLinkedList{Task} + waitq::IntrusiveLinkedList{Task} lock::L - GenericCondition{L}() where {L<:AbstractLock} = new{L}(InvasiveLinkedList{Task}(), L()) - GenericCondition{L}(l::L) where {L<:AbstractLock} = new{L}(InvasiveLinkedList{Task}(), l) - GenericCondition(l::AbstractLock) = new{typeof(l)}(InvasiveLinkedList{Task}(), l) + GenericCondition{L}() where {L<:AbstractLock} = new{L}(IntrusiveLinkedList{Task}(), L()) + GenericCondition{L}(l::L) where {L<:AbstractLock} = new{L}(IntrusiveLinkedList{Task}(), l) + GenericCondition(l::AbstractLock) = new{typeof(l)}(IntrusiveLinkedList{Task}(), l) end assert_havelock(c::GenericCondition) = assert_havelock(c.lock) @@ -76,7 +76,25 @@ trylock(c::GenericCondition) = trylock(c.lock) islocked(c::GenericCondition) = islocked(c.lock) lock(f, c::GenericCondition) = lock(f, c.lock) -unlock(f, c::GenericCondition) = unlock(f, c.lock) + +# have waiter wait for c +function _wait2(c::GenericCondition, waiter::Task) + ct = current_task() + assert_havelock(c) + push!(c.waitq, waiter) + # since _wait2 is similar to schedule, we should observe the sticky bit now + if waiter.sticky && Threads.threadid(waiter) == 0 + # Issue #41324 + # t.sticky && tid == 0 is a task that needs to be co-scheduled with + # the parent task. If the parent (current_task) is not sticky we must + # set it to be sticky. + # XXX: Ideally we would be able to unset this + ct.sticky = true + tid = Threads.threadid() + ccall(:jl_set_task_tid, Cint, (Any, Cint), waiter, tid-1) + end + return +end """ wait([x]) @@ -84,7 +102,8 @@ unlock(f, c::GenericCondition) = unlock(f, c.lock) Block the current task until some event occurs, depending on the type of the argument: * [`Channel`](@ref): Wait for a value to be appended to the channel. -* [`Condition`](@ref): Wait for [`notify`](@ref) on a condition. +* [`Condition`](@ref): Wait for [`notify`](@ref) on a condition and return the `val` + parameter passed to `notify`. * `Process`: Wait for a process or process chain to exit. The `exitcode` field of a process can be used to determine success or failure. * [`Task`](@ref): Wait for a `Task` to finish. If the task fails with an exception, a @@ -99,8 +118,7 @@ proceeding. """ function wait(c::GenericCondition) ct = current_task() - assert_havelock(c) - push!(c.waitq, ct) + _wait2(c, ct) token = unlockall(c.lock) try return wait() @@ -121,7 +139,7 @@ is raised as an exception in the woken tasks. Return the count of tasks woken up. Return 0 if no tasks are waiting on `condition`. """ -notify(c::GenericCondition, @nospecialize(arg = nothing); all=true, error=false) = notify(c, arg, all, error) +@constprop :none notify(c::GenericCondition, @nospecialize(arg = nothing); all=true, error=false) = notify(c, arg, all, error) function notify(c::GenericCondition, @nospecialize(arg), all, error) assert_havelock(c) cnt = 0 diff --git a/base/coreio.jl b/base/coreio.jl index 2796c53e759f54..3e508c64a0a64d 100644 --- a/base/coreio.jl +++ b/base/coreio.jl @@ -1,7 +1,7 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -print(xs...) = print(stdout::IO, xs...) -println(xs...) = println(stdout::IO, xs...) +print(xs...) = print(stdout, xs...) +println(xs...) = println(stdout, xs...) println(io::IO) = print(io, '\n') function show end @@ -9,24 +9,26 @@ function repr end struct DevNull <: IO end const devnull = DevNull() -isreadable(::DevNull) = false -iswritable(::DevNull) = true -isopen(::DevNull) = true -read(::DevNull, ::Type{UInt8}) = throw(EOFError()) write(::DevNull, ::UInt8) = 1 unsafe_write(::DevNull, ::Ptr{UInt8}, n::UInt)::Int = n close(::DevNull) = nothing -flush(::DevNull) = nothing -wait_readnb(::DevNull) = wait() wait_close(::DevNull) = wait() -eof(::DevNull) = true +bytesavailable(io::DevNull) = 0 let CoreIO = Union{Core.CoreSTDOUT, Core.CoreSTDERR} - global write, unsafe_write - write(io::CoreIO, x::UInt8) = Core.write(io, x) - unsafe_write(io::CoreIO, x::Ptr{UInt8}, nb::UInt) = Core.unsafe_write(io, x, nb) + global write(io::CoreIO, x::UInt8) = Core.write(io, x) + global unsafe_write(io::CoreIO, x::Ptr{UInt8}, nb::UInt) = Core.unsafe_write(io, x, nb) + + CoreIO = Union{CoreIO, DevNull} + global read(::CoreIO, ::Type{UInt8}) = throw(EOFError()) + global isopen(::CoreIO) = true + global isreadable(::CoreIO) = false + global iswritable(::CoreIO) = true + global flush(::CoreIO) = nothing + global eof(::CoreIO) = true + global wait_readnb(::CoreIO, nb::Int) = nothing end -stdin = devnull -stdout = Core.stdout -stderr = Core.stderr +stdin::IO = devnull +stdout::IO = Core.stdout +stderr::IO = Core.stderr diff --git a/base/cpuid.jl b/base/cpuid.jl new file mode 100644 index 00000000000000..48930d8064ba99 --- /dev/null +++ b/base/cpuid.jl @@ -0,0 +1,115 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +module CPUID + +export cpu_isa + +""" + ISA(features::Set{UInt32}) + +A structure which represents the Instruction Set Architecture (ISA) of a +computer. It holds the `Set` of features of the CPU. + +The numerical values of the features are automatically generated from the C +source code of Julia and stored in the `features_h.jl` Julia file. +""" +struct ISA + features::Set{UInt32} +end + +Base.:<=(a::ISA, b::ISA) = a.features <= b.features +Base.:<(a::ISA, b::ISA) = a.features < b.features +Base.isless(a::ISA, b::ISA) = a < b + +include(string(length(Core.ARGS) >= 2 ? Core.ARGS[2] : "", "features_h.jl")) # include($BUILDROOT/base/features_h.jl) + +# Keep in sync with `arch_march_isa_mapping`. +const ISAs_by_family = Dict( + "i686" => [ + # Source: https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html. + # Implicit in all sets, because always required by Julia: mmx, sse, sse2 + "pentium4" => ISA(Set{UInt32}()), + "prescott" => ISA(Set((JL_X86_sse3,))), + ], + "x86_64" => [ + # Source: https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html. + # Implicit in all sets, because always required by x86-64 architecture: mmx, sse, sse2 + "x86_64" => ISA(Set{UInt32}()), + "core2" => ISA(Set((JL_X86_sse3, JL_X86_ssse3))), + "nehalem" => ISA(Set((JL_X86_sse3, JL_X86_ssse3, JL_X86_sse41, JL_X86_sse42, JL_X86_popcnt))), + "sandybridge" => ISA(Set((JL_X86_sse3, JL_X86_ssse3, JL_X86_sse41, JL_X86_sse42, JL_X86_popcnt, JL_X86_avx, JL_X86_aes, JL_X86_pclmul))), + "haswell" => ISA(Set((JL_X86_movbe, JL_X86_sse3, JL_X86_ssse3, JL_X86_sse41, JL_X86_sse42, JL_X86_popcnt, JL_X86_avx, JL_X86_avx2, JL_X86_aes, JL_X86_pclmul, JL_X86_fsgsbase, JL_X86_rdrnd, JL_X86_fma, JL_X86_bmi, JL_X86_bmi2, JL_X86_f16c))), + "skylake" => ISA(Set((JL_X86_movbe, JL_X86_sse3, JL_X86_ssse3, JL_X86_sse41, JL_X86_sse42, JL_X86_popcnt, JL_X86_avx, JL_X86_avx2, JL_X86_aes, JL_X86_pclmul, JL_X86_fsgsbase, JL_X86_rdrnd, JL_X86_fma, JL_X86_bmi, JL_X86_bmi2, JL_X86_f16c, JL_X86_rdseed, JL_X86_adx, JL_X86_prfchw, JL_X86_clflushopt, JL_X86_xsavec, JL_X86_xsaves))), + "skylake_avx512" => ISA(Set((JL_X86_movbe, JL_X86_sse3, JL_X86_ssse3, JL_X86_sse41, JL_X86_sse42, JL_X86_popcnt, JL_X86_pku, JL_X86_avx, JL_X86_avx2, JL_X86_aes, JL_X86_pclmul, JL_X86_fsgsbase, JL_X86_rdrnd, JL_X86_fma, JL_X86_bmi, JL_X86_bmi2, JL_X86_f16c, JL_X86_rdseed, JL_X86_adx, JL_X86_prfchw, JL_X86_clflushopt, JL_X86_xsavec, JL_X86_xsaves, JL_X86_avx512f, JL_X86_clwb, JL_X86_avx512vl, JL_X86_avx512bw, JL_X86_avx512dq, JL_X86_avx512cd))), + ], + "armv6l" => [ + # The only armv6l processor we know of that runs Julia on armv6l + # We don't have a good way to tell the different armv6l variants apart through features, + # and honestly we don't care much since it's basically this one chip that people want to use with Julia. + "arm1176jzfs" => ISA(Set{UInt32}()), + ], + "armv7l" => [ + "armv7l" => ISA(Set{UInt32}()), + "armv7l+neon" => ISA(Set((JL_AArch32_neon,))), + "armv7l+neon+vfpv4" => ISA(Set((JL_AArch32_neon, JL_AArch32_vfp4))), + ], + "aarch64" => [ + # Implicit in all sets, because always required: fp, asimd + "armv8.0-a" => ISA(Set{UInt32}()), + "armv8.1-a" => ISA(Set((JL_AArch64_v8_1a, JL_AArch64_lse, JL_AArch64_crc, JL_AArch64_rdm))), + "armv8.2-a+crypto" => ISA(Set((JL_AArch64_v8_2a, JL_AArch64_lse, JL_AArch64_crc, JL_AArch64_rdm, JL_AArch64_aes, JL_AArch64_sha2))), + "a64fx" => ISA(Set((JL_AArch64_v8_2a, JL_AArch64_lse, JL_AArch64_crc, JL_AArch64_rdm, JL_AArch64_sha2, JL_AArch64_ccpp, JL_AArch64_complxnum, JL_AArch64_fullfp16, JL_AArch64_sve))), + "apple_m1" => ISA(Set((JL_AArch64_v8_5a, JL_AArch64_lse, JL_AArch64_crc, JL_AArch64_rdm, JL_AArch64_aes, JL_AArch64_sha2, JL_AArch64_sha3, JL_AArch64_ccpp, JL_AArch64_complxnum, JL_AArch64_fp16fml, JL_AArch64_fullfp16, JL_AArch64_dotprod, JL_AArch64_rcpc, JL_AArch64_altnzcv))), + ], + "powerpc64le" => [ + # We have no way to test powerpc64le features yet, so we're only going to declare the lowest ISA: + "power8" => ISA(Set{UInt32}()), + ] +) + +# Test a CPU feature exists on the currently-running host +test_cpu_feature(feature::UInt32) = ccall(:jl_test_cpu_feature, Bool, (UInt32,), feature) + +# Normalize some variation in ARCH values (which typically come from `uname -m`) +function normalize_arch(arch::String) + arch = lowercase(arch) + if arch ∈ ("amd64",) + arch = "x86_64" + elseif arch ∈ ("i386", "i486", "i586") + arch = "i686" + elseif arch ∈ ("armv6",) + arch = "armv6l" + elseif arch ∈ ("arm", "armv7", "armv8", "armv8l") + arch = "armv7l" + elseif arch ∈ ("arm64",) + arch = "aarch64" + elseif arch ∈ ("ppc64le",) + arch = "powerpc64le" + end + return arch +end + +let + # Collect all relevant features for the current architecture, if any. + FEATURES = UInt32[] + arch = normalize_arch(String(Sys.ARCH)) + if arch in keys(ISAs_by_family) + for isa in ISAs_by_family[arch] + unique!(append!(FEATURES, last(isa).features)) + end + end + + # Use `@eval` to inline the list of features. + @eval function cpu_isa() + return ISA(Set{UInt32}(feat for feat in $(FEATURES) if test_cpu_feature(feat))) + end +end + +""" + cpu_isa() + +Return the [`ISA`](@ref) (instruction set architecture) of the current CPU. +""" +cpu_isa + +end # module CPUID diff --git a/base/deepcopy.jl b/base/deepcopy.jl index 36c9c399def541..317d999004c42f 100644 --- a/base/deepcopy.jl +++ b/base/deepcopy.jl @@ -53,7 +53,7 @@ end function deepcopy_internal(@nospecialize(x), stackdict::IdDict) T = typeof(x)::DataType nf = nfields(x) - if T.mutable + if ismutable(x) if haskey(stackdict, x) return stackdict[x] end @@ -87,7 +87,7 @@ end function deepcopy_internal(x::Array, stackdict::IdDict) if haskey(stackdict, x) - return stackdict[x] + return stackdict[x]::typeof(x) end _deepcopy_array_t(x, eltype(x), stackdict) end @@ -126,3 +126,21 @@ function deepcopy_internal(x::Union{Dict,IdDict}, stackdict::IdDict) end dest end + +function deepcopy_internal(x::AbstractLock, stackdict::IdDict) + if haskey(stackdict, x) + return stackdict[x] + end + y = typeof(x)() + stackdict[x] = y + return y +end + +function deepcopy_internal(x::GenericCondition, stackdict::IdDict) + if haskey(stackdict, x) + return stackdict[x] + end + y = typeof(x)(deepcopy_internal(x.lock)) + stackdict[x] = y + return y +end diff --git a/base/deprecated.jl b/base/deprecated.jl index 45adac55a355cf..28a35e23635f4c 100644 --- a/base/deprecated.jl +++ b/base/deprecated.jl @@ -15,11 +15,12 @@ # is only printed the first time for each call place. """ - @deprecate old new [ex=true] + @deprecate old new [export_old=true] -Deprecate method `old` and specify the replacement call `new`. Prevent `@deprecate` from -exporting `old` by setting `ex` to `false`. `@deprecate` defines a new method with the same -signature as `old`. +Deprecate method `old` and specify the replacement call `new`, defining a new method `old` +with the specified signature in the process. + +To prevent `old` from being exported, set `export_old` to `false`. !!! compat "Julia 1.5" As of Julia 1.5, functions defined by `@deprecate` do not print warning when `julia` @@ -34,14 +35,31 @@ old (generic function with 1 method) julia> @deprecate old(x) new(x) false old (generic function with 1 method) ``` + +Calls to `@deprecate` without explicit type-annotations will define deprecated methods +accepting arguments of type `Any`. To restrict deprecation to a specific signature, annotate +the arguments of `old`. For example, +```jldoctest; filter = r"in Main at.*" +julia> new(x::Int) = x; + +julia> new(x::Float64) = 2x; + +julia> @deprecate old(x::Int) new(x); + +julia> methods(old) +# 1 method for generic function "old": +[1] old(x::Int64) in Main at deprecated.jl:70 +``` +will define and deprecate a method `old(x::Int)` that mirrors `new(x::Int)` but will not +define nor deprecate the method `old(x::Float64)`. """ -macro deprecate(old, new, ex=true) +macro deprecate(old, new, export_old=true) meta = Expr(:meta, :noinline) if isa(old, Symbol) oldname = Expr(:quote, old) newname = Expr(:quote, new) Expr(:toplevel, - ex ? Expr(:export, esc(old)) : nothing, + export_old ? Expr(:export, esc(old)) : nothing, :(function $(esc(old))(args...) $meta depwarn($"`$old` is deprecated, use `$new` instead.", Core.Typeof($(esc(old))).name.mt.name) @@ -65,7 +83,7 @@ macro deprecate(old, new, ex=true) error("invalid usage of @deprecate") end Expr(:toplevel, - ex ? Expr(:export, esc(oldsym)) : nothing, + export_old ? Expr(:export, esc(oldsym)) : nothing, :($(esc(old)) = begin $meta depwarn($"`$oldcall` is deprecated, use `$newcall` instead.", Core.Typeof($(esc(oldsym))).name.mt.name) @@ -88,8 +106,13 @@ function depwarn(msg, funcsym; force::Bool=false) _module=begin bt = backtrace() frame, caller = firstcaller(bt, funcsym) - # TODO: Is it reasonable to attribute callers without linfo to Core? - caller.linfo isa Core.MethodInstance ? caller.linfo.def.module : Core + linfo = caller.linfo + if linfo isa Core.MethodInstance + def = linfo.def + def isa Module ? def : def.module + else + Core # TODO: Is it reasonable to attribute callers without linfo to Core? + end end, _file=String(caller.file), _line=caller.line, @@ -117,12 +140,14 @@ function firstcaller(bt::Vector, funcsyms) end found = lkup.func in funcsyms # look for constructor type name - if !found && lkup.linfo isa Core.MethodInstance + if !found li = lkup.linfo - ft = ccall(:jl_first_argument_datatype, Any, (Any,), li.def.sig) - if isa(ft, DataType) && ft.name === Type.body.name - ft = unwrap_unionall(ft.parameters[1]) - found = (isa(ft, DataType) && ft.name.name in funcsyms) + if li isa Core.MethodInstance + ft = ccall(:jl_first_argument_datatype, Any, (Any,), (li.def::Method).sig) + if isa(ft, DataType) && ft.name === Type.body.name + ft = unwrap_unionall(ft.parameters[1]) + found = (isa(ft, DataType) && ft.name.name in funcsyms) + end end end end @@ -226,16 +251,46 @@ getindex(match::Core.MethodMatch, field::Int) = tuple_type_head(T::Type) = fieldtype(T, 1) tuple_type_cons(::Type, ::Type{Union{}}) = Union{} function tuple_type_cons(::Type{S}, ::Type{T}) where T<:Tuple where S - @_pure_meta + @_total_may_throw_meta Tuple{S, T.parameters...} end function parameter_upper_bound(t::UnionAll, idx) - @_pure_meta + @_total_may_throw_meta return rewrap_unionall((unwrap_unionall(t)::DataType).parameters[idx], t) end # these were internal functions, but some packages seem to be relying on them -@deprecate cat_shape(dims, shape::Tuple{}, shapes::Tuple...) cat_shape(dims, shapes) +@deprecate cat_shape(dims, shape::Tuple{}, shapes::Tuple...) cat_shape(dims, shapes) false cat_shape(dims, shape::Tuple{}) = () # make sure `cat_shape(dims, ())` do not recursively calls itself +@deprecate unsafe_indices(A) axes(A) false +@deprecate unsafe_length(r) length(r) false + +# these were internal type aliases, but some pacakges seem to be relying on them +const Any16{N} = Tuple{Any,Any,Any,Any,Any,Any,Any,Any, + Any,Any,Any,Any,Any,Any,Any,Any,Vararg{Any,N}} +const All16{T,N} = Tuple{T,T,T,T,T,T,T,T, + T,T,T,T,T,T,T,T,Vararg{T,N}} + # END 1.6 deprecations + +# BEGIN 1.7 deprecations + +# the plan is to eventually overload getproperty to access entries of the dict +@noinline function getproperty(x::Pairs, s::Symbol) + depwarn("use values(kwargs) and keys(kwargs) instead of kwargs.data and kwargs.itr", :getproperty, force=true) + return getfield(x, s) +end + +# This function was marked as experimental and not exported. +@deprecate catch_stack(task=current_task(); include_bt=true) current_exceptions(task; backtrace=include_bt) false + +# END 1.7 deprecations + +# BEGIN 1.8 deprecations + +const var"@_inline_meta" = var"@inline" +const var"@_noinline_meta" = var"@noinline" +@deprecate getindex(t::Tuple, i::Real) t[convert(Int, i)] + +# END 1.8 deprecations diff --git a/base/dict.jl b/base/dict.jl index 8f885e26c45aaa..199edba251cdbf 100644 --- a/base/dict.jl +++ b/base/dict.jl @@ -55,7 +55,8 @@ Dict{String, Int64} with 2 entries: ``` """ mutable struct Dict{K,V} <: AbstractDict{K,V} - slots::Array{UInt8,1} + # Metadata: empty => 0x00, removed => 0x7f, full => 0b1[7 most significant hash bits] + slots::Vector{UInt8} keys::Array{K,1} vals::Array{V,1} ndel::Int @@ -145,13 +146,24 @@ end empty(a::AbstractDict, ::Type{K}, ::Type{V}) where {K, V} = Dict{K, V}() -hashindex(key, sz) = (((hash(key)::UInt % Int) & (sz-1)) + 1)::Int +# Gets 7 most significant bits from the hash (hsh), first bit is 1 +_shorthash7(hsh::UInt32) = (hsh >> UInt(25))%UInt8 | 0x80 +_shorthash7(hsh::UInt64) = (hsh >> UInt(57))%UInt8 | 0x80 -@propagate_inbounds isslotempty(h::Dict, i::Int) = h.slots[i] == 0x0 -@propagate_inbounds isslotfilled(h::Dict, i::Int) = h.slots[i] == 0x1 -@propagate_inbounds isslotmissing(h::Dict, i::Int) = h.slots[i] == 0x2 +# hashindex (key, sz) - computes optimal position and shorthash7 +# idx - optimal position in the hash table +# sh::UInt8 - short hash (7 highest hash bits) +function hashindex(key, sz) + hsh = hash(key)::UInt + idx = (((hsh % Int) & (sz-1)) + 1)::Int + return idx, _shorthash7(hsh) +end + +@propagate_inbounds isslotempty(h::Dict, i::Int) = h.slots[i] == 0x00 +@propagate_inbounds isslotfilled(h::Dict, i::Int) = (h.slots[i] & 0x80) != 0 +@propagate_inbounds isslotmissing(h::Dict, i::Int) = h.slots[i] == 0x7f -function rehash!(h::Dict{K,V}, newsz = length(h.keys)) where V where K +@constprop :none function rehash!(h::Dict{K,V}, newsz = length(h.keys)) where V where K olds = h.slots oldk = h.keys oldv = h.vals @@ -161,7 +173,7 @@ function rehash!(h::Dict{K,V}, newsz = length(h.keys)) where V where K h.idxfloor = 1 if h.count == 0 resize!(h.slots, newsz) - fill!(h.slots, 0) + fill!(h.slots, 0x0) resize!(h.keys, newsz) resize!(h.vals, newsz) h.ndel = 0 @@ -176,51 +188,41 @@ function rehash!(h::Dict{K,V}, newsz = length(h.keys)) where V where K maxprobe = 0 for i = 1:sz - @inbounds if olds[i] == 0x1 + @inbounds if (olds[i] & 0x80) != 0 k = oldk[i] v = oldv[i] - index0 = index = hashindex(k, newsz) + index, sh = hashindex(k, newsz) + index0 = index while slots[index] != 0 index = (index & (newsz-1)) + 1 end probe = (index - index0) & (newsz-1) probe > maxprobe && (maxprobe = probe) - slots[index] = 0x1 + slots[index] = olds[i] keys[index] = k vals[index] = v count += 1 - - if h.age != age0 - # if `h` is changed by a finalizer, retry - return rehash!(h, newsz) - end end end + @assert h.age == age0 "Muliple concurent writes to Dict detected!" + h.age += 1 h.slots = slots h.keys = keys h.vals = vals h.count = count h.ndel = 0 h.maxprobe = maxprobe - @assert h.age == age0 - return h end function sizehint!(d::Dict{T}, newsz) where T oldsz = length(d.slots) # limit new element count to max_values of the key type - newsz = min(newsz, max_values(T)::Int) + newsz = min(max(newsz, length(d)), max_values(T)::Int) # need at least 1.5n space to hold n elements - newsz = cld(3 * newsz, 2) - if newsz <= oldsz - # todo: shrink - # be careful: rehash!() assumes everything fits. it was only designed - # for growing. - return d - end - rehash!(d, newsz) + newsz = _tablesz(cld(3 * newsz, 2)) + return newsz == oldsz ? d : rehash!(d, newsz) end """ @@ -257,45 +259,44 @@ end # get the index where a key is stored, or -1 if not present function ht_keyindex(h::Dict{K,V}, key) where V where K + isempty(h) && return -1 sz = length(h.keys) iter = 0 maxprobe = h.maxprobe - index = hashindex(key, sz) + index, sh = hashindex(key, sz) keys = h.keys @inbounds while true - if isslotempty(h,index) - break - end - if !isslotmissing(h,index) && (key === keys[index] || isequal(key,keys[index])) - return index + isslotempty(h,index) && return -1 + if h.slots[index] == sh + k = keys[index] + if (key === k || isequal(key, k)) + return index + end end index = (index & (sz-1)) + 1 - iter += 1 - iter > maxprobe && break + (iter += 1) > maxprobe && return -1 end - return -1 + # This line is unreachable end -# get the index where a key is stored, or -pos if not present -# and the key would be inserted at pos +# get (index, sh) for the key +# index - where a key is stored, or -pos if not present +# and the key would be inserted at pos +# sh::UInt8 - short hash (7 highest hash bits) # This version is for use by setindex! and get! -function ht_keyindex2!(h::Dict{K,V}, key) where V where K - age0 = h.age +function ht_keyindex2_shorthash!(h::Dict{K,V}, key) where V where K sz = length(h.keys) iter = 0 maxprobe = h.maxprobe - index = hashindex(key, sz) + index, sh = hashindex(key, sz) avail = 0 keys = h.keys @inbounds while true if isslotempty(h,index) - if avail < 0 - return avail - end - return -index + return (avail < 0 ? avail : -index), sh end if isslotmissing(h,index) @@ -304,8 +305,11 @@ function ht_keyindex2!(h::Dict{K,V}, key) where V where K # in case "key" already exists in a later collided slot. avail = -index end - elseif key === keys[index] || isequal(key, keys[index]) - return index + elseif h.slots[index] == sh + k = keys[index] + if key === k || isequal(key, k) + return index, sh + end end index = (index & (sz-1)) + 1 @@ -313,14 +317,14 @@ function ht_keyindex2!(h::Dict{K,V}, key) where V where K iter > maxprobe && break end - avail < 0 && return avail + avail < 0 && return avail, sh maxallowed = max(maxallowedprobe, sz>>maxprobeshift) # Check if key is not present, may need to keep searching to find slot @inbounds while iter < maxallowed if !isslotfilled(h,index) h.maxprobe = iter - return -index + return -index, sh end index = (index & (sz-1)) + 1 iter += 1 @@ -328,11 +332,14 @@ function ht_keyindex2!(h::Dict{K,V}, key) where V where K rehash!(h, h.count > 64000 ? sz*2 : sz*4) - return ht_keyindex2!(h, key) + return ht_keyindex2_shorthash!(h, key) end -@propagate_inbounds function _setindex!(h::Dict, v, key, index) - h.slots[index] = 0x1 +# Only for better backward compatibility. It can be removed in the future. +ht_keyindex2!(h::Dict, key) = ht_keyindex2_shorthash!(h, key)[1] + +@propagate_inbounds function _setindex!(h::Dict, v, key, index, sh = _shorthash7(hash(key))) + h.slots[index] = sh h.keys[index] = key h.vals[index] = v h.count += 1 @@ -347,6 +354,7 @@ end # > 3/4 deleted or > 2/3 full rehash!(h, h.count > 64000 ? h.count*2 : h.count*4) end + nothing end function setindex!(h::Dict{K,V}, v0, key0) where V where K @@ -359,19 +367,35 @@ end function setindex!(h::Dict{K,V}, v0, key::K) where V where K v = convert(V, v0) - index = ht_keyindex2!(h, key) + index, sh = ht_keyindex2_shorthash!(h, key) + + if index > 0 + h.age += 1 + @inbounds h.keys[index] = key + @inbounds h.vals[index] = v + else + @inbounds _setindex!(h, v, key, -index, sh) + end + + return h +end + +function setindex!(h::Dict{K,Any}, v, key::K) where K + @nospecialize v + index, sh = ht_keyindex2_shorthash!(h, key) if index > 0 h.age += 1 @inbounds h.keys[index] = key @inbounds h.vals[index] = v else - @inbounds _setindex!(h, v, key, -index) + @inbounds _setindex!(h, v, key, -index, sh) end return h end + """ get!(collection, key, default) @@ -436,26 +460,25 @@ function get!(default::Callable, h::Dict{K,V}, key0) where V where K end function get!(default::Callable, h::Dict{K,V}, key::K) where V where K - index = ht_keyindex2!(h, key) + index, sh = ht_keyindex2_shorthash!(h, key) index > 0 && return h.vals[index] age0 = h.age v = convert(V, default()) if h.age != age0 - index = ht_keyindex2!(h, key) + index, sh = ht_keyindex2_shorthash!(h, key) end if index > 0 h.age += 1 @inbounds h.keys[index] = key @inbounds h.vals[index] = v else - @inbounds _setindex!(h, v, key, -index) + @inbounds _setindex!(h, v, key, -index, sh) end return v end - function getindex(h::Dict{K,V}, key) where V where K index = ht_keyindex(h, key) @inbounds return (index < 0) ? throw(KeyError(key)) : h.vals[index]::V @@ -467,6 +490,9 @@ end Return the value stored for the given key, or the given default value if no mapping for the key is present. +!!! compat "Julia 1.7" + For tuples and numbers, this function requires at least Julia 1.7. + # Examples ```jldoctest julia> d = Dict("a"=>1, "b"=>2); @@ -603,7 +629,7 @@ function pop!(h::Dict) end function _delete!(h::Dict{K,V}, index) where {K,V} - @inbounds h.slots[index] = 0x2 + @inbounds h.slots[index] = 0x7f @inbounds _unsetindex!(h.keys, index) @inbounds _unsetindex!(h.vals, index) h.ndel += 1 @@ -662,7 +688,7 @@ end @propagate_inbounds _iterate(t::Dict{K,V}, i) where {K,V} = i == 0 ? nothing : (Pair{K,V}(t.keys[i],t.vals[i]), i == typemax(Int) ? 0 : i+1) @propagate_inbounds function iterate(t::Dict) - _iterate(t, skip_deleted_floor!(t)) + _iterate(t, skip_deleted(t, t.idxfloor)) end @propagate_inbounds iterate(t::Dict, i) = _iterate(t, skip_deleted(t, i)) @@ -680,7 +706,7 @@ end function filter!(pred, h::Dict{K,V}) where {K,V} h.count == 0 && return h @inbounds for i=1:length(h.slots) - if h.slots[i] == 0x01 && !pred(Pair{K,V}(h.keys[i], h.vals[i])) + if ((h.slots[i] & 0x80) != 0) && !pred(Pair{K,V}(h.keys[i], h.vals[i])) _delete!(h, i) end end @@ -696,7 +722,7 @@ end function map!(f, iter::ValueIterator{<:Dict}) dict = iter.dict vals = dict.vals - # @inbounds is here so the it gets propagated to isslotfiled + # @inbounds is here so that it gets propagated to isslotfilled @inbounds for i = dict.idxfloor:lastindex(vals) if isslotfilled(dict, i) vals[i] = f(vals[i]) @@ -705,6 +731,22 @@ function map!(f, iter::ValueIterator{<:Dict}) return iter end +function mergewith!(combine, d1::Dict{K, V}, d2::AbstractDict) where {K, V} + haslength(d2) && sizehint!(d1, length(d1) + length(d2)) + for (k, v) in d2 + i, sh = ht_keyindex2_shorthash!(d1, k) + if i > 0 + d1.vals[i] = combine(d1.vals[i], v) + else + if !isequal(k, convert(K, k)) + throw(ArgumentError("$(limitrepr(k)) is not a valid key for type $K")) + end + @inbounds _setindex!(d1, convert(V, v), k, -i, sh) + end + end + return d1 +end + struct ImmutableDict{K,V} <: AbstractDict{K,V} parent::ImmutableDict{K,V} key::K @@ -771,15 +813,23 @@ function get(dict::ImmutableDict, key, default) return default end +function get(default::Callable, dict::ImmutableDict, key) + while isdefined(dict, :parent) + isequal(dict.key, key) && return dict.value + dict = dict.parent + end + return default() +end + # this actually defines reverse iteration (e.g. it should not be used for merge/copy/filter type operations) function iterate(d::ImmutableDict{K,V}, t=d) where {K, V} !isdefined(t, :parent) && return nothing (Pair{K,V}(t.key, t.value), t.parent) end -length(t::ImmutableDict) = count(x->true, t) +length(t::ImmutableDict) = count(Returns(true), t) isempty(t::ImmutableDict) = !isdefined(t, :parent) empty(::ImmutableDict, ::Type{K}, ::Type{V}) where {K, V} = ImmutableDict{K,V}() -_similar_for(c::Dict, ::Type{Pair{K,V}}, itr, isz) where {K, V} = empty(c, K, V) -_similar_for(c::AbstractDict, ::Type{T}, itr, isz) where {T} = +_similar_for(c::AbstractDict, ::Type{Pair{K,V}}, itr, isz, len) where {K, V} = empty(c, K, V) +_similar_for(c::AbstractDict, ::Type{T}, itr, isz, len) where {T} = throw(ArgumentError("for AbstractDicts, similar requires an element type of Pair;\n if calling map, consider a comprehension instead")) diff --git a/base/div.jl b/base/div.jl index 1923da121cdf91..7b172ecc95a631 100644 --- a/base/div.jl +++ b/base/div.jl @@ -5,14 +5,20 @@ """ div(x, y, r::RoundingMode=RoundToZero) -The quotient from Euclidean division. Computes x/y, rounded to an integer according -to the rounding mode `r`. In other words, the quantity +The quotient from Euclidean (integer) division. Computes x/y, rounded to +an integer according to the rounding mode `r`. In other words, the quantity round(x/y,r) without any intermediate rounding. -See also: [`fld`](@ref), [`cld`](@ref) which are special cases of this function +!!! compat "Julia 1.4" + The three-argument method taking a `RoundingMode` requires Julia 1.4 or later. + +See also [`fld`](@ref) and [`cld`](@ref), which are special cases of this function. + +!!! compat "Julia 1.9" + `RoundFromZero` requires at least Julia 1.9. # Examples: ```jldoctest @@ -30,6 +36,10 @@ julia> div(-5, 2, RoundNearestTiesAway) -3 julia> div(-5, 2, RoundNearestTiesUp) -2 +julia> div(4, 3, RoundFromZero) +2 +julia> div(-4, 3, RoundFromZero) +-2 ``` """ div(x, y, r::RoundingMode) @@ -60,6 +70,26 @@ without any intermediate rounding. `[0,-y)` otherwise. The result may not be exact if `x` and `y` have the same sign, and `abs(x) < abs(y)`. See also [`RoundUp`](@ref). +- if `r == RoundFromZero`, then the result is in the interval `(-y, 0]` if `y` is positive, or + `[0, -y)` otherwise. The result may not be exact if `x` and `y` have the same sign, and + `abs(x) < abs(y)`. See also [`RoundFromZero`](@ref). + +!!! compat "Julia 1.9" + `RoundFromZero` requires at least Julia 1.9. + +# Examples: +```jldoctest +julia> x = 9; y = 4; + +julia> x % y # same as rem(x, y) +1 + +julia> x ÷ y # same as div(x, y) +2 + +julia> x == div(x, y) * y + rem(x, y) +true +``` """ rem(x, y, r::RoundingMode) @@ -70,18 +100,41 @@ rem(x, y, ::RoundingMode{:Up}) = mod(x, -y) rem(x, y, r::RoundingMode{:Nearest}) = x - y*div(x, y, r) rem(x::Integer, y::Integer, r::RoundingMode{:Nearest}) = divrem(x, y, r)[2] +function rem(x, y, ::typeof(RoundFromZero)) + signbit(x) == signbit(y) ? rem(x, y, RoundUp) : rem(x, y, RoundDown) +end + """ fld(x, y) Largest integer less than or equal to `x/y`. Equivalent to `div(x, y, RoundDown)`. -See also: [`div`](@ref) +See also [`div`](@ref), [`cld`](@ref), [`fld1`](@ref). # Examples ```jldoctest julia> fld(7.3,5.5) 1.0 + +julia> fld.(-5:5, 3)' +1×11 adjoint(::Vector{Int64}) with eltype Int64: + -2 -2 -1 -1 -1 0 0 0 1 1 1 ``` +Because `fld(x, y)` implements strictly correct floored rounding based on the true +value of floating-point numbers, unintuitive situations can arise. For example: +```jldoctest +julia> fld(6.0,0.1) +59.0 +julia> 6.0/0.1 +60.0 +julia> 6.0/big(0.1) +59.99999999999999666933092612453056361837965690217069245739573412231113406246995 +``` +What is happening here is that the true value of the floating-point number written +as `0.1` is slightly larger than the numerical value 1/10 while `6.0` represents +the number 6 precisely. Therefore the true value of `6.0 / 0.1` is slightly less +than 60. When doing division, this is rounded to precisely `60.0`, but +`fld(6.0, 0.1)` always takes the floor of the true value, so the result is `59.0`. """ fld(a, b) = div(a, b, RoundDown) @@ -90,12 +143,16 @@ fld(a, b) = div(a, b, RoundDown) Smallest integer larger than or equal to `x/y`. Equivalent to `div(x, y, RoundUp)`. -See also: [`div`](@ref) +See also [`div`](@ref), [`fld`](@ref). # Examples ```jldoctest julia> cld(5.5,2.2) 3.0 + +julia> cld.(-5:5, 3)' +1×11 adjoint(::Vector{Int64}) with eltype Int64: + -1 -1 -1 0 0 0 1 1 1 2 2 ``` """ cld(a, b) = div(a, b, RoundUp) @@ -108,6 +165,8 @@ The quotient and remainder from Euclidean division. Equivalent to `(div(x,y,r), rem(x,y,r))`. Equivalently, with the default value of `r`, this call is equivalent to `(x÷y, x%y)`. +See also: [`fldmod`](@ref), [`cld`](@ref). + # Examples ```jldoctest julia> divrem(3,7) @@ -118,6 +177,8 @@ julia> divrem(7,3) ``` """ divrem(x, y) = divrem(x, y, RoundToZero) + + function divrem(a, b, r::RoundingMode) if r === RoundToZero # For compat. Remove in 2.0. @@ -129,6 +190,25 @@ function divrem(a, b, r::RoundingMode) (div(a, b, r), rem(a, b, r)) end end +#avoids calling rem for Integers-Integers (all modes), +#a-d*b not precise for Floats - AbstractFloat, AbstractIrrational. Rationals are still slower +function divrem(a::Integer, b::Integer, r::Union{typeof(RoundUp), + typeof(RoundDown), + typeof(RoundToZero)}) + if r === RoundToZero + # For compat. Remove in 2.0. + d = div(a, b) + (d, a - d*b) + elseif r === RoundDown + # For compat. Remove in 2.0. + d = fld(a, b) + (d, a - d*b) + elseif r === RoundUp + # For compat. Remove in 2.0. + d = div(a, b, r) + (d, a - d*b) + end +end function divrem(x::Integer, y::Integer, rnd::typeof(RoundNearest)) (q, r) = divrem(x, y) if x >= 0 @@ -178,11 +258,17 @@ function divrem(x::Integer, y::Integer, rnd::typeof(RoundNearestTiesUp)) end end +function divrem(x, y, ::typeof(RoundFromZero)) + signbit(x) == signbit(y) ? divrem(x, y, RoundUp) : divrem(x, y, RoundDown) +end + """ fldmod(x, y) The floored quotient and modulus after division. A convenience wrapper for `divrem(x, y, RoundDown)`. Equivalent to `(fld(x,y), mod(x,y))`. + +See also: [`fld`](@ref), [`cld`](@ref), [`fldmod1`](@ref). """ fldmod(x,y) = divrem(x, y, RoundDown) @@ -212,12 +298,16 @@ function div(x::Integer, y::Integer, rnd::Union{typeof(RoundNearest), divrem(x, y, rnd)[1] end +function div(x::Integer, y::Integer, ::typeof(RoundFromZero)) + signbit(x) == signbit(y) ? div(x, y, RoundUp) : div(x, y, RoundDown) +end + # For bootstrapping purposes, we define div for integers directly. Provide the # generic signature also div(a::T, b::T, ::typeof(RoundToZero)) where {T<:Union{BitSigned, BitUnsigned64}} = div(a, b) div(a::Bool, b::Bool, r::RoundingMode) = div(a, b) # Prevent ambiguities -for rm in (RoundUp, RoundDown, RoundToZero) +for rm in (RoundUp, RoundDown, RoundToZero, RoundFromZero) @eval div(a::Bool, b::Bool, r::$(typeof(rm))) = div(a, b) end function div(x::Bool, y::Bool, rnd::Union{typeof(RoundNearest), diff --git a/base/docs/Docs.jl b/base/docs/Docs.jl index da315cadf81b18..b84b3ee8d55f48 100644 --- a/base/docs/Docs.jl +++ b/base/docs/Docs.jl @@ -73,11 +73,16 @@ const modules = Module[] const META = gensym(:meta) const METAType = IdDict{Any,Any} -meta(m::Module) = isdefined(m, META) ? getfield(m, META)::METAType : METAType() +function meta(m::Module) + if !isdefined(m, META) || getfield(m, META) === nothing + initmeta(m) + end + return getfield(m, META)::METAType +end function initmeta(m::Module) - if !isdefined(m, META) - Core.eval(m, :(const $META = $(METAType()))) + if !isdefined(m, META) || getfield(m, META) === nothing + Core.eval(m, :($META = $(METAType()))) push!(modules, m) end nothing @@ -286,14 +291,15 @@ end uncurly(@nospecialize ex) = isexpr(ex, :curly) ? ex.args[1] : ex -namify(@nospecialize x) = astname(x, isexpr(x, :macro)) +namify(@nospecialize x) = astname(x, isexpr(x, :macro))::Union{Symbol,Expr,GlobalRef} function astname(x::Expr, ismacro::Bool) - if isexpr(x, :.) + head = x.head + if head === :. ismacro ? macroname(x) : x # Call overloading, e.g. `(a::A)(b) = b` or `function (a::A)(b) b end` should document `A(b)` - elseif (isexpr(x, :function) || isexpr(x, :(=))) && isexpr(x.args[1], :call) && isexpr(x.args[1].args[1], :(::)) - return astname(x.args[1].args[1].args[end], ismacro) + elseif (head === :function || head === :(=)) && isexpr(x.args[1], :call) && isexpr((x.args[1]::Expr).args[1], :(::)) + return astname(((x.args[1]::Expr).args[1]::Expr).args[end], ismacro) else n = isexpr(x, (:module, :struct)) ? 2 : 1 astname(x.args[n], ismacro) @@ -342,11 +348,11 @@ function metadata(__source__, __module__, expr, ismodule) P = Pair{Symbol,Any} fields = P[] last_docstr = nothing - for each in expr.args[3].args + for each in (expr.args[3]::Expr).args if isa(each, Symbol) || isexpr(each, :(::)) # a field declaration if last_docstr !== nothing - push!(fields, P(namify(each), last_docstr)) + push!(fields, P(namify(each::Union{Symbol,Expr}), last_docstr)) last_docstr = nothing end elseif isexpr(each, :function) || isexpr(each, :(=)) @@ -354,7 +360,7 @@ function metadata(__source__, __module__, expr, ismodule) elseif isa(each, String) || isexpr(each, :string) || isexpr(each, :call) || (isexpr(each, :macrocall) && each.args[1] === Symbol("@doc_str")) # forms that might be doc strings - last_docstr = each + last_docstr = each::Union{String,Expr} end end dict = :($(Dict{Symbol,Any})($([(:($(P)($(quot(f)), $d)))::Expr for (f, d) in fields]...))) @@ -401,8 +407,7 @@ function moduledoc(__source__, __module__, meta, def, def′::Expr) def = unblock(def) block = def.args[3].args if !def.args[1] - isempty(block) && error("empty baremodules are not documentable.") - insert!(block, 2, :(import Base: @doc)) + pushfirst!(block, :(import Base: @doc)) end push!(block, docex) esc(Expr(:toplevel, def)) diff --git a/base/docs/basedocs.jl b/base/docs/basedocs.jl index 9dcc50bc23eb0e..c547709ba2f07b 100644 --- a/base/docs/basedocs.jl +++ b/base/docs/basedocs.jl @@ -23,6 +23,9 @@ as well as many great tutorials and learning resources: For help on a specific function or macro, type `?` followed by its name, e.g. `?cos`, or `?@time`, and press enter. Type `;` to enter shell mode, `]` to enter package mode. + +To exit the interactive session, type `CTRL-D` (press the +control key together with the `d` key), or type `exit()`. """ kw"help", kw"Julia", kw"julia", kw"" @@ -70,7 +73,7 @@ abstract type Real <: Number end ``` [`Number`](@ref) has no supertype, whereas [`Real`](@ref) is an abstract subtype of `Number`. """ -kw"abstract type" +kw"abstract type", kw"abstract" """ module @@ -106,7 +109,7 @@ kw"module" `__init__()` function in your module would executes immediately *after* the module is loaded at runtime for the first time (i.e., it is only called once and only after all statements in the module have been executed). Because it is called *after* fully importing the module, `__init__` -functions of submodules will be executed *first*. Two typical uses of __init__ are calling +functions of submodules will be executed *first*. Two typical uses of `__init__` are calling runtime initialization functions of external C libraries and initializing global constants that involve pointers returned by external libraries. See the [manual section about modules](@ref modules) for more details. @@ -127,7 +130,7 @@ kw"__init__" baremodule `baremodule` declares a module that does not contain `using Base` or local definitions of -[`eval`](@ref Base.eval) and [`include`](@ref Base.include). It does still import `Core`. In other words, +[`eval`](@ref Base.MainInclude.eval) and [`include`](@ref Base.include). It does still import `Core`. In other words, ```julia module Mod @@ -150,6 +153,7 @@ include(p) = Base.include(Mod, p) ... end +``` """ kw"baremodule" @@ -178,8 +182,8 @@ kw"primitive type" A macro maps a sequence of argument expressions to a returned expression, and the resulting expression is substituted directly into the program at the point where the macro is invoked. -Macros are a way to run generated code without calling [`eval`](@ref Base.eval), since the generated -code instead simply becomes part of the surrounding program. +Macros are a way to run generated code without calling [`eval`](@ref Base.MainInclude.eval), +since the generated code instead simply becomes part of the surrounding program. Macro arguments may include expressions, literal values, and symbols. Macros can be defined for variable number of arguments (varargs), but do not accept keyword arguments. Every macro also implicitly gets passed the arguments `__source__`, which contains the line number @@ -207,6 +211,24 @@ Say: hey there friend """ kw"macro" +""" + __module__ + +The argument `__module__` is only visible inside the macro, and it provides information +(in the form of a `Module` object) about the expansion context of the macro invocation. +See the manual section on [Macro invocation](@ref) for more information. +""" +kw"__module__" + +""" + __source__ + +The argument `__source__` is only visible inside the macro, and it provides information +(in the form of a `LineNumberNode` object) about the parser location of the `@` sign from +the macro invocation. See the manual section on [Macro invocation](@ref) for more information. +""" +kw"__source__" + """ local @@ -257,6 +279,19 @@ julia> z """ kw"global" +""" + ' ' + +A pair of single-quote characters delimit a [`Char`](@ref) (that is, character) literal. + +# Examples +```jldoctest +julia> 'j' +'j': ASCII/Unicode U+006A (category Ll: Letter, lowercase) +``` +""" +kw"''" + """ = @@ -330,7 +365,7 @@ julia> push!(a, 2, 3) Assigning `[]` does not eliminate elements from a collection; instead use [`filter!`](@ref). ```jldoctest julia> a = collect(1:3); a[a .<= 1] = [] -ERROR: DimensionMismatch("tried to assign 0 elements to 1 destinations") +ERROR: DimensionMismatch: tried to assign 0 elements to 1 destinations [...] julia> filter!(x -> x > 1, a) # in-place & thus more efficient than a = a[a .> 1] @@ -395,21 +430,98 @@ kw"." """ let -`let` statements allocate new variable bindings each time they run. Whereas an -assignment modifies an existing value location, `let` creates new locations. This -difference is only detectable in the case of variables that outlive their scope via -closures. The `let` syntax accepts a comma-separated series of assignments and variable -names: +`let` blocks create a new hard scope and optionally introduce new local bindings. + +Just like the [other scope constructs](@ref man-scope-table), `let` blocks define +the block of code where newly introduced local variables are accessible. +Additionally, the syntax has a special meaning for comma-separated assignments +and variable names that may optionally appear on the same line as the `let`: ```julia let var1 = value1, var2, var3 = value3 code end ``` -The assignments are evaluated in order, with each right-hand side evaluated in the scope -before the new variable on the left-hand side has been introduced. Therefore it makes -sense to write something like `let x = x`, since the two `x` variables are distinct and -have separate storage. + +The variables introduced on this line are local to the `let` block and the assignments are +evaluated in order, with each right-hand side evaluated in the scope +without considering the name on the left-hand side. Therefore it makes +sense to write something like `let x = x`, since the two `x` variables are distinct with +the left-hand side locally shadowing the `x` from the outer scope. This can even +be a useful idiom as new local variables are freshly created each time local scopes +are entered, but this is only observable in the case of variables that outlive their +scope via closures. + +By contrast, [`begin`](@ref) blocks also group multiple expressions together but do +not introduce scope or have the special assignment syntax. + +### Examples + +In the function below, there is a single `x` that is iteratively updated three times by the `map`. +The closures returned all reference that one `x` at its final value: + +```jldoctest +julia> function test_outer_x() + x = 0 + map(1:3) do _ + x += 1 + return ()->x + end + end +test_outer_x (generic function with 1 method) + +julia> [f() for f in test_outer_x()] +3-element Vector{Int64}: + 3 + 3 + 3 +``` + +If, however, we add a `let` block that introduces a _new_ local variable we will end up +with three distinct variables being captured (one at each iteration) even though we +chose to use (shadow) the same name. + +```jldoctest +julia> function test_let_x() + x = 0 + map(1:3) do _ + x += 1 + let x = x + return ()->x + end + end + end +test_let_x (generic function with 1 method) + +julia> [f() for f in test_let_x()] +3-element Vector{Int64}: + 1 + 2 + 3 +``` + +All scope constructs that introduce new local variables behave this way +when repeatedly run; the distinctive feature of `let` is its ability +to succinctly declare new `local`s that may shadow outer variables of the same +name. For example, directly using the argument of the `do` function similarly +captures three distinct variables: + +```jldoctest +julia> function test_do_x() + map(1:3) do x + return ()->x + end + end +test_do_x (generic function with 1 method) + +julia> [f() for f in test_do_x()] +3-element Vector{Int64}: + 1 + 2 + 3 +``` + + """ kw"let" @@ -432,6 +544,18 @@ For other purposes, `:( ... )` and `quote .. end` blocks are treated identically """ kw"quote" +""" + @ + +The at sign followed by a macro name marks a macro call. Macros provide the +ability to include generated code in the final body of a program. A macro maps +a tuple of arguments, expressed as space-separated expressions or a +function-call-like argument list, to a returned *expression*. The resulting +expression is compiled directly into the surrounding code. See +[Metaprogramming](@ref man-macros) for more details and examples. +""" +kw"@" + """ {} @@ -582,6 +706,32 @@ the last expression in the function body. """ kw"function" +""" + x -> y + +Create an anonymous function mapping argument(s) `x` to the function body `y`. + +```jldoctest +julia> f = x -> x^2 + 2x - 1 +#1 (generic function with 1 method) + +julia> f(2) +7 +``` + +Anonymous functions can also be defined for multiple argumets. +```jldoctest +julia> g = (x,y) -> x^2 + y^2 +#2 (generic function with 1 method) + +julia> g(2,3) +13 +``` + +See the manual section on [anonymous functions](@ref man-anonymous-functions) for more details. +""" +kw"->" + """ return @@ -646,6 +796,13 @@ otherwise the condition expression `x > y` is evaluated, and if it is true, the corresponding block is evaluated; if neither expression is true, the `else` block is evaluated. The `elseif` and `else` blocks are optional, and as many `elseif` blocks as desired can be used. + +In contrast to some other languages conditions must be of type `Bool`. It does not +suffice for conditions to be convertible to `Bool`. +```jldoctest +julia> if 1 end +ERROR: TypeError: non-boolean (Int64) used in boolean context +``` """ kw"if", kw"elseif", kw"else" @@ -665,7 +822,7 @@ See the manual section on [control flow](@ref man-conditional-evaluation) for mo ``` julia> x = 1; y = 2; -julia> println(x > y ? "x is larger" : "y is larger") +julia> x > y ? println("x is larger") : println("y is larger") y is larger ``` """ @@ -719,8 +876,9 @@ kw"while" `end` marks the conclusion of a block of expressions, for example [`module`](@ref), [`struct`](@ref), [`mutable struct`](@ref), [`begin`](@ref), [`let`](@ref), [`for`](@ref) etc. -`end` may also be used when indexing into an array to represent -the last index of a dimension. + +`end` may also be used when indexing to represent the last index of a +collection or the last index of a dimension of an array. # Examples ```jldoctest @@ -895,12 +1053,22 @@ kw"..." ; `;` has a similar role in Julia as in many C-like languages, and is used to delimit the -end of the previous statement. `;` is not necessary after new lines, but can be used to +end of the previous statement. + +`;` is not necessary at the end of a line, but can be used to separate statements on a single line or to join statements into a single expression. -`;` is also used to suppress output printing in the REPL and similar interfaces. + +Adding `;` at the end of a line in the REPL will suppress printing the result of that expression. + +In function declarations, and optionally in calls, `;` separates regular arguments from keywords. + +While constructing arrays, if the arguments inside the square brackets are separated by `;` +then their contents are vertically concatenated together. + +In the standard REPL, typing `;` on an empty line will switch to shell mode. # Examples -```julia +```jldoctest julia> function foo() x = "Hello, "; x *= "World!" return x @@ -914,6 +1082,19 @@ julia> foo(); julia> bar() "Hello, Mars!" + +julia> function plot(x, y; style="solid", width=1, color="black") + ### + end + +julia> [1 2; 3 4] +2×2 Matrix{Int64}: + 1 2 + 3 4 + +julia> ; # upon typing ;, the prompt changes (in place) to: shell> +shell> echo hello +hello ``` """ kw";" @@ -922,6 +1103,19 @@ kw";" x && y Short-circuiting boolean AND. + +See also [`&`](@ref), the ternary operator `? :`, and the manual section on [control flow](@ref man-conditional-evaluation). + +# Examples +```jldoctest +julia> x = 3; + +julia> x > 1 && x < 10 && x isa Int +true + +julia> x < 0 && error("expected positive x") +false +``` """ kw"&&" @@ -929,6 +1123,17 @@ kw"&&" x || y Short-circuiting boolean OR. + +See also: [`|`](@ref), [`xor`](@ref), [`&&`](@ref). + +# Examples +```jldoctest +julia> pi < 3 || ℯ < 3 +true + +julia> false || true || println("neither is true!") +true +``` """ kw"||" @@ -965,7 +1170,7 @@ first argument: with arguments are available as consecutive unnamed SSA variables (%0, %1, etc.); - as a 2-element tuple, containing a string of module IR and a string representing the name of the entry-point function to call; -- as a 2-element tuple, but with the module provided as an `Vector{UINt8}` with bitcode. +- as a 2-element tuple, but with the module provided as an `Vector{UInt8}` with bitcode. Note that contrary to `ccall`, the argument types must be specified as a tuple type, and not a tuple of types. All types, as well as the LLVM code, should be specified as literals, and @@ -990,6 +1195,22 @@ end Usually `begin` will not be necessary, since keywords such as [`function`](@ref) and [`let`](@ref) implicitly begin blocks of code. See also [`;`](@ref). + +`begin` may also be used when indexing to represent the first index of a +collection or the first index of a dimension of an array. + +# Examples +```jldoctest +julia> A = [1 2; 3 4] +2×2 Array{Int64,2}: + 1 2 + 3 4 + +julia> A[begin, :] +2-element Array{Int64,1}: + 1 + 2 +``` """ kw"begin" @@ -1043,10 +1264,10 @@ fields of the type to be set after construction. See the manual section on kw"mutable struct" """ - new + new, or new{A,B,...} -Special function available to inner constructors which created a new object -of the type. +Special function available to inner constructors which creates a new object +of the type. The form new{A,B,...} explicitly specifies values of parameters for parametric types. See the manual section on [Inner Constructor Methods](@ref man-inner-constructor-methods) for more information. """ @@ -1136,6 +1357,8 @@ devnull Nothing A type with no fields that is the type of [`nothing`](@ref). + +See also: [`isnothing`](@ref), [`Some`](@ref), [`Missing`](@ref). """ Nothing @@ -1144,6 +1367,8 @@ Nothing The singleton instance of type [`Nothing`](@ref), used by convention when there is no value to return (as in a C `void` function) or when a variable or field holds no value. + +See also: [`isnothing`](@ref), [`something`](@ref), [`missing`](@ref). """ nothing @@ -1215,7 +1440,7 @@ julia> isa(+, Function) true julia> typeof(sin) -typeof(sin) +typeof(sin) (singleton type of function sin, subtype of Function) julia> ans <: Function true @@ -1384,8 +1609,9 @@ DomainError """ Task(func) -Create a `Task` (i.e. coroutine) to execute the given function `func` (which must be -callable with no arguments). The task exits when this function returns. +Create a `Task` (i.e. coroutine) to execute the given function `func` (which +must be callable with no arguments). The task exits when this function returns. +The task will run in the "world age" from the parent at construction when [`schedule`](@ref)d. # Examples ```jldoctest @@ -1686,6 +1912,8 @@ NaN julia> false * NaN 0.0 ``` + +See also: [`digits`](@ref), [`iszero`](@ref), [`NaN`](@ref). """ Bool @@ -1772,19 +2000,31 @@ Symbol(x...) Construct a tuple of the given objects. +See also [`Tuple`](@ref), [`NamedTuple`](@ref). + # Examples ```jldoctest -julia> tuple(1, 'a', pi) -(1, 'a', π) +julia> tuple(1, 'b', pi) +(1, 'b', π) + +julia> ans === (1, 'b', π) +true + +julia> Tuple(Real[1, 2, pi]) # takes a collection +(1, 2, π) ``` """ tuple """ - getfield(value, name::Symbol) - getfield(value, i::Int) + getfield(value, name::Symbol, [order::Symbol]) + getfield(value, i::Int, [order::Symbol]) -Extract a field from a composite `value` by name or position. +Extract a field from a composite `value` by name or position. Optionally, an +ordering can be defined for the operation. If the field was declared `@atomic`, +the specification is strongly recommended to be compatible with the stores to +that location. Otherwise, if not declared as `@atomic`, this parameter must be +`:not_atomic` if specified. See also [`getproperty`](@ref Base.getproperty) and [`fieldnames`](@ref). # Examples @@ -1805,10 +2045,14 @@ julia> getfield(a, 1) getfield """ - setfield!(value, name::Symbol, x) + setfield!(value, name::Symbol, x, [order::Symbol]) + setfield!(value, i::Int, x, [order::Symbol]) -Assign `x` to a named field in `value` of composite type. -The `value` must be mutable and `x` must be a subtype of `fieldtype(typeof(value), name)`. +Assign `x` to a named field in `value` of composite type. The `value` must be +mutable and `x` must be a subtype of `fieldtype(typeof(value), name)`. +Additionally, an ordering can be specified for this operation. If the field was +declared `@atomic`, this specification is mandatory. Otherwise, if not declared +as `@atomic`, it must be `:not_atomic` if specified. See also [`setproperty!`](@ref Base.setproperty!). # Examples @@ -1828,16 +2072,68 @@ julia> a = 1//2 1//2 julia> setfield!(a, :num, 3); -ERROR: setfield! immutable struct of type Rational cannot be changed +ERROR: setfield!: immutable struct of type Rational cannot be changed ``` """ setfield! +""" + swapfield!(value, name::Symbol, x, [order::Symbol]) + swapfield!(value, i::Int, x, [order::Symbol]) + +These atomically perform the operations to simultaneously get and set a field: + + y = getfield(value, name) + setfield!(value, name, x) + return y +""" +swapfield! + +""" + modifyfield!(value, name::Symbol, op, x, [order::Symbol]) -> Pair + modifyfield!(value, i::Int, op, x, [order::Symbol]) -> Pair + +These atomically perform the operations to get and set a field after applying +the function `op`. + + y = getfield(value, name) + z = op(y, x) + setfield!(value, name, z) + return y => z + +If supported by the hardware (for example, atomic increment), this may be +optimized to the appropriate hardware instruction, otherwise it'll use a loop. +""" +modifyfield! + +""" + replacefield!(value, name::Symbol, expected, desired, + [success_order::Symbol, [fail_order::Symbol=success_order]) -> (; old, success::Bool) + replacefield!(value, i::Int, expected, desired, + [success_order::Symbol, [fail_order::Symbol=success_order]) -> (; old, success::Bool) + +These atomically perform the operations to get and conditionally set a field to +a given value. + + y = getfield(value, name, fail_order) + ok = y === expected + if ok + setfield!(value, name, desired, success_order) + end + return (; old = y, success = ok) + +If supported by the hardware, this may be optimized to the appropriate hardware +instruction, otherwise it'll use a loop. +""" +replacefield! + """ typeof(x) Get the concrete type of `x`. +See also [`eltype`](@ref). + # Examples ```jldoctest julia> a = 1//2; @@ -1854,12 +2150,16 @@ Matrix{Float64} (alias for Array{Float64, 2}) typeof """ - isdefined(m::Module, s::Symbol) - isdefined(object, s::Symbol) - isdefined(object, index::Int) + isdefined(m::Module, s::Symbol, [order::Symbol]) + isdefined(object, s::Symbol, [order::Symbol]) + isdefined(object, index::Int, [order::Symbol]) -Tests whether a global variable or object field is defined. The arguments can be a module and a symbol -or a composite object and field name (as a symbol) or index. +Tests whether a global variable or object field is defined. The arguments can +be a module and a symbol or a composite object and field name (as a symbol) or +index. Optionally, an ordering can be defined for the operation. If the field +was declared `@atomic`, the specification is strongly recommended to be +compatible with the stores to that location. Otherwise, if not declared as +`@atomic`, this parameter must be `:not_atomic` if specified. To test whether an array element is defined, use [`isassigned`](@ref) instead. @@ -1894,7 +2194,7 @@ isdefined """ Vector{T}(undef, n) -Construct an uninitialized [`Vector{T}`](@ref) of length `n`. See [`undef`](@ref). +Construct an uninitialized [`Vector{T}`](@ref) of length `n`. # Examples ```julia-repl @@ -1944,14 +2244,19 @@ Vector{T}(::Missing, n) """ Matrix{T}(undef, m, n) -Construct an uninitialized [`Matrix{T}`](@ref) of size `m`×`n`. See [`undef`](@ref). +Construct an uninitialized [`Matrix{T}`](@ref) of size `m`×`n`. # Examples ```julia-repl julia> Matrix{Float64}(undef, 2, 3) 2×3 Array{Float64, 2}: - 6.93517e-310 6.93517e-310 6.93517e-310 - 6.93517e-310 6.93517e-310 1.29396e-320 + 2.36365e-314 2.28473e-314 5.0e-324 + 2.26704e-314 2.26711e-314 NaN + +julia> similar(ans, Int32, 2, 2) +2×2 Matrix{Int32}: + 490537216 1277177453 + 1 1936748399 ``` """ Matrix{T}(::UndefInitializer, m, n) @@ -1999,19 +2304,28 @@ containing elements of type `T`. `N` can either be supplied explicitly, as in `Array{T,N}(undef, dims)`, or be determined by the length or number of `dims`. `dims` may be a tuple or a series of integer arguments corresponding to the lengths in each dimension. If the rank `N` is supplied explicitly, then it must -match the length or number of `dims`. See [`undef`](@ref). +match the length or number of `dims`. Here [`undef`](@ref) is +the [`UndefInitializer`](@ref). # Examples ```julia-repl julia> A = Array{Float64, 2}(undef, 2, 3) # N given explicitly -2×3 Array{Float64, 2}: +2×3 Matrix{Float64}: 6.90198e-310 6.90198e-310 6.90198e-310 6.90198e-310 6.90198e-310 0.0 -julia> B = Array{Float64}(undef, 2) # N determined by the input -2-element Array{Float64, 1}: - 1.87103e-320 - 0.0 +julia> B = Array{Float64}(undef, 4) # N determined by the input +4-element Vector{Float64}: + 2.360075077e-314 + NaN + 2.2671131793e-314 + 2.299821756e-314 + +julia> similar(B, 2, 4, 1) # use typeof(B), and the given size +2×4×1 Array{Float64, 3}: +[:, :, 1] = + 2.26703e-314 2.26708e-314 0.0 2.80997e-314 + 0.0 2.26703e-314 2.26708e-314 0.0 ``` """ Array{T,N}(::UndefInitializer, dims) @@ -2088,10 +2402,12 @@ Alias for `UndefInitializer()`, which constructs an instance of the singleton ty [`UndefInitializer`](@ref), used in array initialization to indicate the array-constructor-caller would like an uninitialized array. +See also: [`missing`](@ref), [`similar`](@ref). + # Examples ```julia-repl julia> Array{Float64, 1}(undef, 3) -3-element Array{Float64, 1}: +3-element Vector{Float64}: 2.2752528595e-314 2.202942107e-314 2.275252907e-314 @@ -2127,6 +2443,8 @@ julia> +(1, 20, 4) Unary minus operator. +See also: [`abs`](@ref), [`flipsign`](@ref). + # Examples ```jldoctest julia> -1 @@ -2198,8 +2516,8 @@ julia> 4.5/2 """ ArgumentError(msg) -The parameters to a function call do not match a valid signature. Argument `msg` is a -descriptive error string. +The arguments passed to a function are invalid. +`msg` is a descriptive error message. """ ArgumentError @@ -2232,6 +2550,9 @@ AssertionError An error occurred while [`include`](@ref Base.include)ing, [`require`](@ref Base.require)ing, or [`using`](@ref) a file. The error specifics should be available in the `.error` field. + +!!! compat "Julia 1.7" + LoadErrors are no longer emitted by `@macroexpand`, `@macroexpand1`, and `macroexpand` as of Julia 1.7. """ LoadError @@ -2278,14 +2599,14 @@ union [`Union{}`](@ref) is the bottom type of Julia. julia> IntOrString = Union{Int,AbstractString} Union{Int64, AbstractString} -julia> 1 :: IntOrString -1 +julia> 1 isa IntOrString +true -julia> "Hello!" :: IntOrString -"Hello!" +julia> "Hello!" isa IntOrString +true -julia> 1.0 :: IntOrString -ERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got a value of type Float64 +julia> 1.0 isa IntOrString +false ``` """ Union @@ -2311,10 +2632,20 @@ UnionAll """ :: -With the `::`-operator type annotations are attached to expressions and variables in programs. -See the manual section on [Type Declarations](@ref). +The `::` operator either asserts that a value has the given type, or declares that +a local variable or function return always has the given type. + +Given `expression::T`, `expression` is first evaluated. If the result is of type +`T`, the value is simply returned. Otherwise, a [`TypeError`](@ref) is thrown. + +In local scope, the syntax `local x::T` or `x::T = expression` declares that local variable +`x` always has type `T`. When a value is assigned to the variable, it will be +converted to type `T` by calling [`convert`](@ref). -Outside of declarations `::` is used to assert that expressions and variables in programs have a given type. +In a method declaration, the syntax `function f(x)::T` causes any value returned by +the method to be converted to type `T`. + +See the manual section on [Type Declarations](@ref). # Examples ```jldoctest @@ -2323,6 +2654,13 @@ ERROR: TypeError: typeassert: expected AbstractFloat, got a value of type Int64 julia> (1+2)::Int 3 + +julia> let + local x::Int + x = 2.0 + x + end +2 ``` """ kw"::" @@ -2330,15 +2668,17 @@ kw"::" """ Vararg{T,N} -The last parameter of a tuple type [`Tuple`](@ref) can be the special type `Vararg`, which denotes any -number of trailing elements. The type `Vararg{T,N}` corresponds to exactly `N` elements of type `T`. +The last parameter of a tuple type [`Tuple`](@ref) can be the special value `Vararg`, which denotes any +number of trailing elements. `Vararg{T,N}` corresponds to exactly `N` elements of type `T`. Finally `Vararg{T}` corresponds to zero or more elements of type `T`. `Vararg` tuple types are used to represent the arguments accepted by varargs methods (see the section on [Varargs Functions](@ref) in the manual.) +See also [`NTuple`](@ref). + # Examples ```jldoctest julia> mytupletype = Tuple{AbstractString, Vararg{Int}} -Tuple{AbstractString, Vararg{Int64, N} where N} +Tuple{AbstractString, Vararg{Int64}} julia> isa(("1",), mytupletype) true @@ -2367,6 +2707,8 @@ is considered an abstract type, and tuple types are only concrete if their param field names; fields are only accessed by index. See the manual section on [Tuple Types](@ref). + +See also [`Vararg`](@ref), [`NTuple`](@ref), [`tuple`](@ref), [`NamedTuple`](@ref). """ Tuple @@ -2422,8 +2764,11 @@ typeassert """ getproperty(value, name::Symbol) + getproperty(value, name::Symbol, order::Symbol) The syntax `a.b` calls `getproperty(a, :b)`. +The syntax `@atomic order a.b` calls `getproperty(a, :b, :order)` and +the syntax `@atomic a.b` calls `getproperty(a, :b, :sequentially_consistent)`. # Examples ```jldoctest @@ -2448,21 +2793,65 @@ julia> obj.x 1 ``` -See also [`propertynames`](@ref Base.propertynames) and +See also [`getfield`](@ref Core.getfield), +[`propertynames`](@ref Base.propertynames) and [`setproperty!`](@ref Base.setproperty!). """ Base.getproperty """ setproperty!(value, name::Symbol, x) + setproperty!(value, name::Symbol, x, order::Symbol) The syntax `a.b = c` calls `setproperty!(a, :b, c)`. +The syntax `@atomic order a.b = c` calls `setproperty!(a, :b, c, :order)` +and the syntax `@atomic a.b = c` calls `getproperty(a, :b, :sequentially_consistent)`. + +!!! compat "Julia 1.8" + `setproperty!` on modules requires at least Julia 1.8. -See also [`propertynames`](@ref Base.propertynames) and +See also [`setfield!`](@ref Core.setfield!), +[`propertynames`](@ref Base.propertynames) and [`getproperty`](@ref Base.getproperty). """ Base.setproperty! +""" + swapproperty!(x, f::Symbol, v, order::Symbol=:not_atomic) + +The syntax `@atomic a.b, _ = c, a.b` returns `(c, swapproperty!(a, :b, c, :sequentially_consistent))`, +where there must be one getfield expression common to both sides. + +See also [`swapfield!`](@ref Core.swapfield!) +and [`setproperty!`](@ref Base.setproperty!). +""" +Base.swapproperty! + +""" + modifyproperty!(x, f::Symbol, op, v, order::Symbol=:not_atomic) + +The syntax `@atomic max(a().b, c)` returns `modifyproperty!(a(), :b, +max, c, :sequentially_consistent))`, where the first argument must be a +`getfield` expression and is modified atomically. + +See also [`modifyfield!`](@ref Core.modifyfield!) +and [`setproperty!`](@ref Base.setproperty!). +""" +Base.modifyproperty! + +""" + replaceproperty!(x, f::Symbol, expected, desired, success_order::Symbol=:not_atomic, fail_order::Symbol=success_order) + +Perform a compare-and-swap operation on `x.f` from `expected` to `desired`, per +egal. The syntax `@atomic_replace! x.f expected => desired` can be used instead +of the function call form. + +See also [`replacefield!`](@ref Core.replacefield!) +and [`setproperty!`](@ref Base.setproperty!). +""" +Base.replaceproperty! + + """ StridedArray{T, N} @@ -2503,6 +2892,13 @@ StridedVecOrMat Module A `Module` is a separate global variable workspace. See [`module`](@ref) and the [manual section about modules](@ref modules) for details. + + Module(name::Symbol=:anonymous, std_imports=true, default_names=true) + +Return a module with the specified name. A `baremodule` corresponds to `Module(:ModuleName, false)` + +An empty module containing no names at all can be created with `Module(:ModuleName, false, false)`. +This module will not import `Base` or `Core` and does not contain a reference to itself. """ Module @@ -2538,4 +2934,81 @@ A quoted piece of code, that does not support interpolation. See the [manual sec """ QuoteNode + +""" + " +`"` Is used to delimit string literals. + +# Examples + +```jldoctest +julia> "Hello World!" +"Hello World!" + +julia> "Hello World!\\n" +"Hello World!\\n" +``` + +See also [`\"""`](@ref \"\"\"). +""" +kw"\"" + +""" + \""" +`\"""` is used to delimit string literals. Strings created by triple quotation marks can contain `"` characters without escaping and are dedented to the level of the least-indented line. This is useful for defining strings within code that is indented. + +# Examples + +```jldoctest +julia> \"""Hello World!\""" +"Hello World!" + +julia> \"""Contains "quote" characters\""" +"Contains \\"quote\\" characters" + +julia> \""" + Hello, + world.\""" +"Hello,\\nworld." +``` + +See also [`"`](@ref \") +""" +kw"\"\"\"" + +""" + donotdelete(args...) + +This function prevents dead-code elimination (DCE) of itself and any arguments +passed to it, but is otherwise the lightest barrier possible. In particular, +it is not a GC safepoint, does model an observable heap effect, does not expand +to any code itself and may be re-ordered with respect to other side effects +(though the total number of executions may not change). + +A useful model for this function is that it hashes all memory `reachable` from +args and escapes this information through some observable side-channel that does +not otherwise impact program behavior. Of course that's just a model. The +function does nothing and returns `nothing`. + +This is intended for use in benchmarks that want to guarantee that `args` are +actually computed. (Otherwise DCE may see that the result of the benchmark is +unused and delete the entire benchmark code). + +**Note**: `donotdelete` does not affect constant folding. For example, in + `donotdelete(1+1)`, no add instruction needs to be executed at runtime and + the code is semantically equivalent to `donotdelete(2).` + +# Examples + +function loop() + for i = 1:1000 + # The complier must guarantee that there are 1000 program points (in the correct + # order) at which the value of `i` is in a register, but has otherwise + # total control over the program. + donotdelete(i) + end +end +""" +Base.donotdelete + end diff --git a/base/docs/utils.jl b/base/docs/utils.jl index b1f3327086808c..928dfde01ccf00 100644 --- a/base/docs/utils.jl +++ b/base/docs/utils.jl @@ -18,6 +18,13 @@ You can also use a stream for large amounts of data: HTML() do io println(io, "
foo
") end + +!!! warning + `HTML` is currently exported to maintain + backwards compatibility, but this export + is deprecated. It is recommended to use + this type as `Docs.HTML` or to explicitly + import it from `Docs`. """ mutable struct HTML{T} content::T @@ -38,6 +45,12 @@ show(io::IO, ::MIME"text/html", h::HTML{<:Function}) = h.content(io) @html_str -> Docs.HTML Create an `HTML` object from a literal string. + +# Examples +```jldoctest +julia> html"Julia" +HTML{String}("Julia") +``` """ macro html_str(s) :(HTML($s)) @@ -63,6 +76,13 @@ You can also use a stream for large amounts of data: Text() do io println(io, "foo") end + +!!! warning + `Text` is currently exported to maintain + backwards compatibility, but this export + is deprecated. It is recommended to use + this type as `Docs.Text` or to explicitly + import it from `Docs`. """ mutable struct Text{T} content::T @@ -79,6 +99,12 @@ hash(t::T, h::UInt) where {T<:Union{HTML,Text}} = hash(T, hash(t.content, h)) @text_str -> Docs.Text Create a `Text` object from a literal string. + +# Examples +```jldoctest +julia> text"Julia" +Julia +``` """ macro text_str(s) :(Text($s)) diff --git a/base/download.jl b/base/download.jl index 60f4823c7a4e8d..59cbadcb2f6afb 100644 --- a/base/download.jl +++ b/base/download.jl @@ -1,14 +1,5 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -const DOWNLOAD_HOOKS = Callable[] - -function download_url(url::AbstractString) - for hook in DOWNLOAD_HOOKS - url = String(hook(url)::AbstractString) - end - return url -end - Downloads() = require(PkgId( UUID((0xf43a241f_c20a_4ad4, 0x852c_f6b1247861c6)), "Downloads", @@ -25,9 +16,10 @@ specified, a temporary path. Returns the path of the downloaded file. around `Downloads.download`. In new code, you should use that function directly instead of calling this. """ -function download(url::AbstractString, path::AbstractString) - invokelatest(Downloads().download, download_url(url), path) -end -function download(url::AbstractString) - invokelatest(Downloads().download, download_url(url)) +download(url::AbstractString, path::AbstractString) = do_download(url, path) +download(url::AbstractString) = do_download(url, nothing) + +function do_download(url::AbstractString, path::Union{AbstractString, Nothing}) + depwarn("Base.download is deprecated; use Downloads.download instead", :download) + invokelatest(Downloads().download, url, path) end diff --git a/base/env.jl b/base/env.jl index 8f5256f25915ee..4fdc02e582a4c6 100644 --- a/base/env.jl +++ b/base/env.jl @@ -32,7 +32,7 @@ if Sys.iswindows() function _unsetenv(svar::AbstractString) var = cwstring(svar) ret = ccall(:SetEnvironmentVariableW,stdcall,Int32,(Ptr{UInt16},Ptr{UInt16}),var,C_NULL) - windowserror(:setenv, ret == 0) + windowserror(:setenv, ret == 0 && Libc.GetLastError() != ERROR_ENVVAR_NOT_FOUND) end else # !windows _getenv(var::AbstractString) = ccall(:getenv, Cstring, (Cstring,), var) @@ -77,7 +77,7 @@ variable may result in an uppercase `ENV` key.) const ENV = EnvDict() getindex(::EnvDict, k::AbstractString) = access_env(k->throw(KeyError(k)), k) -get(::EnvDict, k::AbstractString, def) = access_env(k->def, k) +get(::EnvDict, k::AbstractString, def) = access_env(Returns(def), k) get(f::Callable, ::EnvDict, k::AbstractString) = access_env(k->f(), k) in(k::AbstractString, ::KeySet{String, EnvDict}) = _hasenv(k) pop!(::EnvDict, k::AbstractString) = (v = ENV[k]; _unsetenv(k); v) @@ -87,7 +87,7 @@ setindex!(::EnvDict, v, k::AbstractString) = _setenv(k,string(v)) push!(::EnvDict, kv::Pair{<:AbstractString}) = setindex!(ENV, kv.second, kv.first) if Sys.iswindows() - GESW() = (pos = ccall(:GetEnvironmentStringsW,stdcall,Ptr{UInt16},()); (pos,pos)) + GESW() = (pos = ccall(:GetEnvironmentStringsW, stdcall, Ptr{UInt16}, ()); (pos, pos)) function winuppercase(s::AbstractString) isempty(s) && return s LOCALE_INVARIANT = 0x0000007f @@ -99,32 +99,43 @@ if Sys.iswindows() return transcode(String, ws) end function iterate(hash::EnvDict, block::Tuple{Ptr{UInt16},Ptr{UInt16}} = GESW()) - if unsafe_load(block[1]) == 0 - ccall(:FreeEnvironmentStringsW, stdcall, Int32, (Ptr{UInt16},), block[2]) - return nothing + while true + if unsafe_load(block[1]) == 0 + ccall(:FreeEnvironmentStringsW, stdcall, Int32, (Ptr{UInt16},), block[2]) + return nothing + end + pos = block[1] + blk = block[2] + len = ccall(:wcslen, UInt, (Ptr{UInt16},), pos) + buf = Vector{UInt16}(undef, len) + GC.@preserve buf unsafe_copyto!(pointer(buf), pos, len) + env = transcode(String, buf) + pos += (len + 1) * 2 + if !isempty(env) + m = findnext('=', env, nextind(env, firstindex(env))) + else + m = nothing + end + if m === nothing + @warn "malformed environment entry: $env" + continue + end + return (Pair{String,String}(winuppercase(env[1:prevind(env, m)]), env[nextind(env, m):end]), (pos, blk)) end - pos = block[1] - blk = block[2] - len = ccall(:wcslen, UInt, (Ptr{UInt16},), pos) - buf = Vector{UInt16}(undef, len) - GC.@preserve buf unsafe_copyto!(pointer(buf), pos, len) - env = transcode(String, buf) - m = match(r"^(=?[^=]+)=(.*)$"s, env) - if m === nothing - error("malformed environment entry: $env") - end - return (Pair{String,String}(winuppercase(m.captures[1]), m.captures[2]), (pos+(len+1)*2, blk)) end else # !windows function iterate(::EnvDict, i=0) - env = ccall(:jl_environ, Any, (Int32,), i) - env === nothing && return nothing - env = env::String - m = match(r"^(.*?)=(.*)$"s, env) - if m === nothing - error("malformed environment entry: $env") + while true + env = ccall(:jl_environ, Any, (Int32,), i) + env === nothing && return nothing + env = env::String + m = findfirst('=', env) + if m === nothing + @warn "malformed environment entry: $env" + nothing + end + return (Pair{String,String}(env[1:prevind(env, m)], env[nextind(env, m):end]), i+1) end - return (Pair{String,String}(m.captures[1], m.captures[2]), i+1) end end # os-test @@ -144,7 +155,7 @@ function show(io::IO, ::EnvDict) end """ - withenv(f::Function, kv::Pair...) + withenv(f, kv::Pair...) Execute `f` in an environment that is temporarily modified (not replaced as in `setenv`) by zero or more `"var"=>val` arguments `kv`. `withenv` is generally used via the @@ -152,7 +163,7 @@ by zero or more `"var"=>val` arguments `kv`. `withenv` is generally used via the environment variable (if it is set). When `withenv` returns, the original environment has been restored. """ -function withenv(f::Function, keyvals::Pair{T}...) where T<:AbstractString +function withenv(f, keyvals::Pair{T}...) where T<:AbstractString old = Dict{T,Any}() for (key,val) in keyvals old[key] = get(ENV,key,nothing) @@ -165,4 +176,4 @@ function withenv(f::Function, keyvals::Pair{T}...) where T<:AbstractString end end end -withenv(f::Function) = f() # handle empty keyvals case; see #10853 +withenv(f) = f() # handle empty keyvals case; see #10853 diff --git a/base/error.jl b/base/error.jl index 16b66af68be12d..4459e54def19b5 100644 --- a/base/error.jl +++ b/base/error.jl @@ -20,6 +20,8 @@ throw(e) Throw an object as an exception. + +See also: [`rethrow`](@ref), [`error`](@ref). """ throw @@ -38,7 +40,7 @@ error(s::AbstractString) = throw(ErrorException(s)) Raise an `ErrorException` with the given message. """ function error(s::Vararg{Any,N}) where {N} - @_noinline_meta + @noinline throw(ErrorException(Main.Base.string(s...))) end @@ -54,10 +56,10 @@ exception will continue propagation as if it had not been caught. the program state at the time of the error so you're encouraged to instead throw a new exception using `throw(e)`. In Julia 1.1 and above, using `throw(e)` will preserve the root cause exception on the stack, as - described in [`catch_stack`](@ref). + described in [`current_exceptions`](@ref). """ rethrow() = ccall(:jl_rethrow, Bottom, ()) -rethrow(e) = ccall(:jl_rethrow_other, Bottom, (Any,), e) +rethrow(@nospecialize(e)) = ccall(:jl_rethrow_other, Bottom, (Any,), e) struct InterpreterIP code::Union{CodeInfo,Core.MethodInstance,Nothing} @@ -105,7 +107,7 @@ end Get a backtrace object for the current program point. """ function backtrace() - @_noinline_meta + @noinline # skip frame for backtrace(). Note that for this to work properly, # backtrace() itself must not be interpreted nor inlined. skip = 1 @@ -123,37 +125,43 @@ function catch_backtrace() return _reformat_bt(bt::Vector{Ptr{Cvoid}}, bt2::Vector{Any}) end +struct ExceptionStack <: AbstractArray{Any,1} + stack::Array{Any,1} +end + """ - catch_stack(task=current_task(); [inclue_bt=true]) + current_exceptions(task::Task=current_task(); [backtrace::Bool=true]) Get the stack of exceptions currently being handled. For nested catch blocks there may be more than one current exception in which case the most recently -thrown exception is last in the stack. The stack is returned as a Vector of -`(exception,backtrace)` pairs, or a Vector of exceptions if `include_bt` is -false. +thrown exception is last in the stack. The stack is returned as an +`ExceptionStack` which is an AbstractVector of named tuples +`(exception,backtrace)`. If `backtrace` is false, the backtrace in each pair +will be set to `nothing`. Explicitly passing `task` will return the current exception stack on an arbitrary task. This is useful for inspecting tasks which have failed due to uncaught exceptions. -!!! compat "Julia 1.1" - This function is experimental in Julia 1.1 and will likely be renamed in a - future release (see https://github.com/JuliaLang/julia/pull/29901). +!!! compat "Julia 1.7" + This function went by the experimental name `catch_stack()` in Julia + 1.1–1.6, and had a plain Vector-of-tuples as a return type. """ -function catch_stack(task=current_task(); include_bt=true) - raw = ccall(:jl_get_excstack, Any, (Any,Cint,Cint), task, include_bt, typemax(Cint))::Vector{Any} +function current_exceptions(task::Task=current_task(); backtrace::Bool=true) + raw = ccall(:jl_get_excstack, Any, (Any,Cint,Cint), task, backtrace, typemax(Cint))::Vector{Any} formatted = Any[] - stride = include_bt ? 3 : 1 + stride = backtrace ? 3 : 1 for i = reverse(1:stride:length(raw)) - e = raw[i] - push!(formatted, include_bt ? (e,Base._reformat_bt(raw[i+1],raw[i+2])) : e) + exc = raw[i] + bt = backtrace ? Base._reformat_bt(raw[i+1],raw[i+2]) : nothing + push!(formatted, (exception=exc,backtrace=bt)) end - formatted + ExceptionStack(formatted) end ## keyword arg lowering generates calls to this ## function kwerr(kw, args::Vararg{Any,N}) where {N} - @_noinline_meta + @noinline throw(MethodError(typeof(args[1]).name.mt.kwsorter, (kw,args...))) end @@ -253,7 +261,7 @@ function iterate(ebo::ExponentialBackOff, state= (ebo.n, min(ebo.first_delay, eb state[1] < 1 && return nothing next_n = state[1]-1 curr_delay = state[2] - next_delay = min(ebo.max_delay, state[2] * ebo.factor * (1.0 - ebo.jitter + (rand(Float64) * 2.0 * ebo.jitter))) + next_delay = min(ebo.max_delay, state[2] * ebo.factor * (1.0 - ebo.jitter + (Libc.rand(Float64) * 2.0 * ebo.jitter))) (curr_delay, (next_n, next_delay)) end length(ebo::ExponentialBackOff) = ebo.n diff --git a/base/errorshow.jl b/base/errorshow.jl index 9042d40b5c949b..2f6fa6604b775b 100644 --- a/base/errorshow.jl +++ b/base/errorshow.jl @@ -9,7 +9,7 @@ This method is used to display the exception after a call to [`throw`](@ref). # Examples ```jldoctest julia> struct MyException <: Exception - msg::AbstractString + msg::String end julia> function Base.showerror(io::IO, err::MyException) @@ -31,7 +31,7 @@ showerror(io::IO, ex) = show(io, ex) show_index(io::IO, x::Any) = show(io, x) show_index(io::IO, x::Slice) = show_index(io, x.indices) -show_index(io::IO, x::LogicalIndex) = show_index(io, x.mask) +show_index(io::IO, x::LogicalIndex) = summary(io, x.mask) show_index(io::IO, x::OneTo) = print(io, "1:", x.stop) show_index(io::IO, x::Colon) = print(io, ':') @@ -92,7 +92,7 @@ function showerror(io::IO, ex, bt; backtrace=true) end function showerror(io::IO, ex::LoadError, bt; backtrace=true) - print(io, "LoadError: ") + !isa(ex.error, LoadError) && print(io, "LoadError: ") showerror(io, ex.error, bt, backtrace=backtrace) print(io, "\nin expression starting at $(ex.file):$(ex.line)") end @@ -152,6 +152,7 @@ showerror(io::IO, ex::KeyError) = (print(io, "KeyError: key "); print(io, " not found")) showerror(io::IO, ex::InterruptException) = print(io, "InterruptException:") showerror(io::IO, ex::ArgumentError) = print(io, "ArgumentError: ", ex.msg) +showerror(io::IO, ex::DimensionMismatch) = print(io, "DimensionMismatch: ", ex.msg) showerror(io::IO, ex::AssertionError) = print(io, "AssertionError: ", ex.msg) showerror(io::IO, ex::OverflowError) = print(io, "OverflowError: ", ex.msg) @@ -159,14 +160,8 @@ showerror(io::IO, ex::UndefKeywordError) = print(io, "UndefKeywordError: keyword argument $(ex.var) not assigned") function showerror(io::IO, ex::UndefVarError) - if ex.var in [:UTF16String, :UTF32String, :WString, :utf16, :utf32, :wstring, :RepString] - return showerror(io, ErrorException(""" - `$(ex.var)` has been moved to the package LegacyStrings.jl: - Run Pkg.add("LegacyStrings") to install LegacyStrings on Julia v0.5-; - Then do `using LegacyStrings` to get `$(ex.var)`. - """)) - end print(io, "UndefVarError: $(ex.var) not defined") + Experimental.show_error_hints(io, ex) end function showerror(io::IO, ex::InexactError) @@ -176,6 +171,10 @@ function showerror(io::IO, ex::InexactError) Experimental.show_error_hints(io, ex) end +function showerror(io::IO, ex::CanonicalIndexError) + print(io, "CanonicalIndexError: ", ex.func, " not defined for ", ex.type) +end + typesof(@nospecialize args...) = Tuple{Any[ Core.Typeof(args[i]) for i in 1:length(args) ]...} function print_with_compare(io::IO, @nospecialize(a::DataType), @nospecialize(b::DataType), color::Symbol) @@ -205,19 +204,20 @@ function print_with_compare(io::IO, @nospecialize(a), @nospecialize(b), color::S end end -function show_convert_error(io::IO, ex::MethodError, @nospecialize(arg_types_param)) +function show_convert_error(io::IO, ex::MethodError, arg_types_param) # See #13033 T = striptype(ex.args[1]) if T === nothing print(io, "First argument to `convert` must be a Type, got ", ex.args[1]) else - print_one_line = isa(T, DataType) && isa(arg_types_param[2], DataType) && T.name != arg_types_param[2].name + p2 = arg_types_param[2] + print_one_line = isa(T, DataType) && isa(p2, DataType) && T.name != p2.name printstyled(io, "Cannot `convert` an object of type ") print_one_line || printstyled(io, "\n ") - print_with_compare(io, arg_types_param[2], T, :light_green) + print_with_compare(io, p2, T, :light_green) printstyled(io, " to an object of type ") print_one_line || printstyled(io, "\n ") - print_with_compare(io, T, arg_types_param[2], :light_red) + print_with_compare(io, T, p2, :light_red) end end @@ -228,10 +228,11 @@ function showerror(io::IO, ex::MethodError) arg_types = (is_arg_types ? ex.args : typesof(ex.args...))::DataType f = ex.f meth = methods_including_ambiguous(f, arg_types) - if length(meth) > 1 + if isa(meth, MethodList) && length(meth) > 1 return showerror_ambiguous(io, meth, f, arg_types) end arg_types_param::SimpleVector = arg_types.parameters + show_candidates = true print(io, "MethodError: ") ft = typeof(f) name = ft.name.mt.name @@ -248,7 +249,10 @@ function showerror(io::IO, ex::MethodError) if f === Base.convert && length(arg_types_param) == 2 && !is_arg_types f_is_function = true show_convert_error(io, ex, arg_types_param) - elseif isempty(methods(f)) && isa(f, DataType) && f.abstract + elseif f === mapreduce_empty || f === reduce_empty + print(io, "reducing over an empty collection is not allowed; consider supplying `init` to the reducer") + show_candidates = false + elseif isempty(methods(f)) && isa(f, DataType) && isabstracttype(f) print(io, "no constructors have been defined for ", f) elseif isempty(methods(f)) && !isa(f, Function) && !isa(f, Type) print(io, "objects of type ", ft, " are not callable") @@ -281,14 +285,11 @@ function showerror(io::IO, ex::MethodError) if any(x -> x <: AbstractArray{<:Number}, arg_types_param) && any(x -> x <: Number, arg_types_param) - nouns = Dict{Any,String}( - Base.:+ => "addition", - Base.:- => "subtraction", - ) + nounf = f === Base.:+ ? "addition" : "subtraction" varnames = ("scalar", "array") first, second = arg_types_param[1] <: Number ? varnames : reverse(varnames) fstring = f === Base.:+ ? "+" : "-" # avoid depending on show_default for functions (invalidation) - print(io, "\nFor element-wise $(nouns[f]), use broadcasting with dot syntax: $first .$fstring $second") + print(io, "\nFor element-wise $nounf, use broadcasting with dot syntax: $first .$fstring $second") end end if ft <: AbstractArray @@ -316,14 +317,14 @@ function showerror(io::IO, ex::MethodError) hasrows |= isrow push!(vec_args, isrow ? vec(arg) : arg) end - if hasrows && applicable(f, vec_args...) + if hasrows && applicable(f, vec_args...) && isempty(kwargs) print(io, "\n\nYou might have used a 2d row vector where a 1d column vector was required.", "\nNote the difference between 1d column vector [1,2,3] and 2d row vector [1 2 3].", "\nYou can convert to a column vector with the vec() function.") end end Experimental.show_error_hints(io, ex, arg_types_param, kwargs) - try + show_candidates && try show_method_candidates(io, ex, kwargs) catch ex @error "Error showing method candidates, aborted" exception=ex,catch_backtrace() @@ -349,13 +350,15 @@ function showerror_ambiguous(io::IO, meth, f, args) sigfix = typeintersect(m.sig, sigfix) end if isa(unwrap_unionall(sigfix), DataType) && sigfix <: Tuple - if all(m->morespecific(sigfix, m.sig), meth) - print(io, "\nPossible fix, define\n ") - Base.show_tuple_as_call(io, :function, sigfix) - else - println(io) - print(io, "To resolve the ambiguity, try making one of the methods more specific, or ") - print(io, "adding a new method more specific than any of the existing applicable methods.") + let sigfix=sigfix + if all(m->morespecific(sigfix, m.sig), meth) + print(io, "\nPossible fix, define\n ") + Base.show_tuple_as_call(io, :function, sigfix) + else + println(io) + print(io, "To resolve the ambiguity, try making one of the methods more specific, or ") + print(io, "adding a new method more specific than any of the existing applicable methods.") + end end end nothing @@ -371,6 +374,13 @@ function showerror_nostdio(err, msg::AbstractString) ccall(:jl_printf, Cint, (Ptr{Cvoid},Cstring), stderr_stream, "\n") end +stacktrace_expand_basepaths()::Bool = + tryparse(Bool, get(ENV, "JULIA_STACKTRACE_EXPAND_BASEPATHS", "false")) === true +stacktrace_contract_userdir()::Bool = + tryparse(Bool, get(ENV, "JULIA_STACKTRACE_CONTRACT_HOMEDIR", "true")) === true +stacktrace_linebreaks()::Bool = + tryparse(Bool, get(ENV, "JULIA_STACKTRACE_LINEBREAKS", "false")) === true + function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=()) is_arg_types = isa(ex.args, DataType) arg_types = is_arg_types ? ex.args : typesof(ex.args...) @@ -399,7 +409,11 @@ function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=() buf = IOBuffer() iob0 = iob = IOContext(buf, io) tv = Any[] - sig0 = method.sig + if func isa Core.OpaqueClosure + sig0 = signature_type(func, typeof(func).parameters[1]) + else + sig0 = method.sig + end while isa(sig0, UnionAll) push!(tv, sig0.var) iob = IOContext(iob, :unionall_env => sig0.var) @@ -423,7 +437,7 @@ function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=() # If isvarargtype then it checks whether the rest of the input arguments matches # the varargtype if Base.isvarargtype(sig[i]) - sigstr = (unwrap_unionall(sig[i]).parameters[1], "...") + sigstr = (unwrapva(unwrap_unionall(sig[i])), "...") j = length(t_i) else sigstr = (sig[i],) @@ -460,7 +474,7 @@ function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=() # It ensures that methods like f(a::AbstractString...) gets the correct # number of right_matches for t in arg_types_param[length(sig):end] - if t <: rewrap_unionall(unwrap_unionall(sig[end]).parameters[1], method.sig) + if t <: rewrap_unionall(unwrapva(unwrap_unionall(sig[end])), method.sig) right_matches += 1 end end @@ -473,7 +487,7 @@ function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=() for (k, sigtype) in enumerate(sig[length(t_i)+1:end]) sigtype = isvarargtype(sigtype) ? unwrap_unionall(sigtype) : sigtype if Base.isvarargtype(sigtype) - sigstr = ((sigtype::DataType).parameters[1], "...") + sigstr = (unwrapva(sigtype::Core.TypeofVararg), "...") else sigstr = (sigtype,) end @@ -498,7 +512,12 @@ function show_method_candidates(io::IO, ex::MethodError, @nospecialize kwargs=() end print(iob, ")") show_method_params(iob0, tv) - print(iob, " at ", method.file, ":", method.line) + file, line = updated_methodloc(method) + if file === nothing + file = string(method.file) + end + stacktrace_contract_userdir() && (file = contractuser(file)) + print(iob, " at ", file, ":", line) if !isempty(kwargs)::Bool unexpected = Symbol[] if isempty(kwords) || !(any(endswith(string(kword), "...") for kword in kwords)) @@ -555,35 +574,21 @@ end # replace `sf` as needed. const update_stackframes_callback = Ref{Function}(identity) -function replaceuserpath(str) - str = replace(str, homedir() => "~") - # seems to be necessary for some paths with small letter drive c:// etc - str = replace(str, lowercasefirst(homedir()) => "~") - return str -end - -const STACKTRACE_MODULECOLORS = [:light_blue, :light_yellow, - :light_magenta, :light_green, :light_cyan, :light_red, - :blue, :yellow, :magenta, :green, :cyan, :red] -stacktrace_expand_basepaths()::Bool = - tryparse(Bool, get(ENV, "JULIA_STACKTRACE_EXPAND_BASEPATHS", "false")) === true -stacktrace_contract_userdir()::Bool = - tryparse(Bool, get(ENV, "JULIA_STACKTRACE_CONTRACT_HOMEDIR", "true")) === true -stacktrace_linebreaks()::Bool = - tryparse(Bool, get(ENV, "JULIA_STACKTRACE_LINEBREAKS", "false")) === true +const STACKTRACE_MODULECOLORS = [:magenta, :cyan, :green, :yellow] +const STACKTRACE_FIXEDCOLORS = IdDict(Base => :light_black, Core => :light_black) function show_full_backtrace(io::IO, trace::Vector; print_linebreaks::Bool) - n = length(trace) - ndigits_max = ndigits(n) + num_frames = length(trace) + ndigits_max = ndigits(num_frames) - modulecolordict = Dict{Module, Symbol}() + modulecolordict = copy(STACKTRACE_FIXEDCOLORS) modulecolorcycler = Iterators.Stateful(Iterators.cycle(STACKTRACE_MODULECOLORS)) println(io, "\nStacktrace:") - for (i, frame) in enumerate(trace) - print_stackframe(io, i, frame, 1, ndigits_max, modulecolordict, modulecolorcycler) - if i < n + for (i, (frame, n)) in enumerate(trace) + print_stackframe(io, i, frame, n, ndigits_max, modulecolordict, modulecolorcycler) + if i < num_frames println(io) print_linebreaks && println(io) end @@ -678,7 +683,7 @@ end # Print a stack frame where the module color is determined by looking up the parent module in # `modulecolordict`. If the module does not have a color, yet, a new one can be drawn # from `modulecolorcycler`. -function print_stackframe(io, i, frame, n, digit_align_width, modulecolordict, modulecolorcycler) +function print_stackframe(io, i, frame::StackFrame, n::Int, digit_align_width, modulecolordict, modulecolorcycler) m = Base.parentmodule(frame) if m !== nothing while parentmodule(m) !== m @@ -698,10 +703,11 @@ end # Print a stack frame where the module color is set manually with `modulecolor`. -function print_stackframe(io, i, frame, n, digit_align_width, modulecolor) +function print_stackframe(io, i, frame::StackFrame, n::Int, digit_align_width, modulecolor) file, line = string(frame.file), frame.line + file = fixup_stdlib_path(file) stacktrace_expand_basepaths() && (file = something(find_source_file(file), file)) - stacktrace_contract_userdir() && (file = replaceuserpath(file)) + stacktrace_contract_userdir() && (file = contractuser(file)) # Used by the REPL to make it possible to open # the location of a stackframe/method in the editor. @@ -741,13 +747,7 @@ function print_stackframe(io, i, frame, n, digit_align_width, modulecolor) # filename, separator, line # use escape codes for formatting, printstyled can't do underlined and color # codes are bright black (90) and underlined (4) - function print_underlined(io::IO, s...) - colored = get(io, :color, false)::Bool - start_s = colored ? "\033[90;4m" : "" - end_s = colored ? "\033[0m" : "" - print(io, start_s, s..., end_s) - end - print_underlined(io, pathparts[end], ":", line) + printstyled(io, pathparts[end], ":", line; color = :light_black, underline = true) # inlined printstyled(io, inlined ? " [inlined]" : "", color = :light_black) @@ -782,8 +782,7 @@ function show_backtrace(io::IO, t::Vector) try invokelatest(update_stackframes_callback[], filtered) catch end # process_backtrace returns a Vector{Tuple{Frame, Int}} - frames = map(x->first(x)::StackFrame, filtered) - show_full_backtrace(io, frames; print_linebreaks = stacktrace_linebreaks()) + show_full_backtrace(io, filtered; print_linebreaks = stacktrace_linebreaks()) return end @@ -796,10 +795,9 @@ end # For improved user experience, filter out frames for include() implementation # - see #33065. See also #35371 for extended discussion of internal frames. function _simplify_include_frames(trace) - i = length(trace) - kept_frames = trues(i) + kept_frames = trues(length(trace)) first_ignored = nothing - while i >= 1 + for i in length(trace):-1:1 frame::StackFrame, _ = trace[i] mod = parentmodule(frame) if first_ignored === nothing @@ -821,10 +819,9 @@ function _simplify_include_frames(trace) first_ignored = nothing end end - i -= 1 end if first_ignored !== nothing - kept_frames[i:first_ignored] .= false + kept_frames[1:first_ignored] .= false end return trace[kept_frames] end @@ -872,7 +869,7 @@ function process_backtrace(t::Vector, limit::Int=typemax(Int); skipC = true) return _simplify_include_frames(ret) end -function show_exception_stack(io::IO, stack::Vector) +function show_exception_stack(io::IO, stack) # Display exception stack with the top of the stack first. This ordering # means that the user doesn't have to scroll up in the REPL to discover the # root cause. @@ -896,3 +893,28 @@ function show(io::IO, ip::InterpreterIP) print(io, " in $(ip.code) at statement $(Int(ip.stmt))") end end + +# handler for displaying a hint in case the user tries to call +# the instance of a number (probably missing the operator) +# eg: (1 + 2)(3 + 4) +function noncallable_number_hint_handler(io, ex, arg_types, kwargs) + @nospecialize + if ex.f isa Number + print(io, "\nMaybe you forgot to use an operator such as ") + printstyled(io, "*, ^, %, / etc. ", color=:cyan) + print(io, "?") + end +end + +Experimental.register_error_hint(noncallable_number_hint_handler, MethodError) + +# ExceptionStack implementation +size(s::ExceptionStack) = size(s.stack) +getindex(s::ExceptionStack, i::Int) = s.stack[i] + +function show(io::IO, ::MIME"text/plain", stack::ExceptionStack) + nexc = length(stack) + printstyled(io, nexc, "-element ExceptionStack", nexc == 0 ? "" : ":\n") + show_exception_stack(io, stack) +end +show(io::IO, stack::ExceptionStack) = show(io, MIME("text/plain"), stack) diff --git a/base/essentials.jl b/base/essentials.jl index fb360ea6482dbe..498c6f8f4f1967 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -1,11 +1,18 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -using Core: CodeInfo, SimpleVector +using Core: CodeInfo, SimpleVector, donotdelete, arrayref const Callable = Union{Function,Type} const Bottom = Union{} +# Define minimal array interface here to help code used in macros: +length(a::Array) = arraylen(a) + +# This is more complicated than it needs to be in order to get Win64 through bootstrap +eval(:(getindex(A::Array, i1::Int) = arrayref($(Expr(:boundscheck)), A, i1))) +eval(:(getindex(A::Array, i1::Int, i2::Int, I::Int...) = (@inline; arrayref($(Expr(:boundscheck)), A, i1, i2, I...)))) + """ AbstractSet{T} @@ -23,14 +30,24 @@ An `AbstractDict{K, V}` should be an iterator of `Pair{K, V}`. """ abstract type AbstractDict{K,V} end -# The real @inline macro is not available until after array.jl, so this -# internal macro splices the meta Expr directly into the function body. -macro _inline_meta() - Expr(:meta, :inline) -end -macro _noinline_meta() - Expr(:meta, :noinline) +""" + Iterators.Pairs(values, keys) <: AbstractDict{eltype(keys), eltype(values)} + +Transforms an indexable container into a Dictionary-view of the same data. +Modifying the key-space of the underlying data may invalidate this object. +""" +struct Pairs{K, V, I, A} <: AbstractDict{K, V} + data::A + itr::I end +Pairs{K, V}(data::A, itr::I) where {K, V, I, A} = Pairs{K, V, I, A}(data, itr) +Pairs{K}(data::A, itr::I) where {K, I, A} = Pairs{K, eltype(A), I, A}(data, itr) +Pairs(data::A, itr::I) where {I, A} = Pairs{eltype(I), eltype(A), I, A}(data, itr) +pairs(::Type{NamedTuple}) = Pairs{Symbol, V, NTuple{N, Symbol}, NamedTuple{names, T}} where {V, N, names, T<:NTuple{N, Any}} + +## optional pretty printer: +#const NamedTuplePair{N, V, names, T<:NTuple{N, Any}} = Pairs{Symbol, V, NTuple{N, Symbol}, NamedTuple{names, T}} +#export NamedTuplePair macro _gc_preserve_begin(arg1) Expr(:gc_preserve_begin, esc(arg1)) @@ -44,12 +61,12 @@ end @nospecialize Applied to a function argument name, hints to the compiler that the method -should not be specialized for different types of that argument, -but instead to use precisely the declared type for each argument. -This is only a hint for avoiding excess code generation. -Can be applied to an argument within a formal argument list, +implementation should not be specialized for different types of that argument, +but instead use the declared type for that argument. +It can be applied to an argument within a formal argument list, or in the function body. -When applied to an argument, the macro must wrap the entire argument expression. +When applied to an argument, the macro must wrap the entire argument expression, e.g., +`@nospecialize(x::Real)` or `@nospecialize(i::Integer...)` rather than wrapping just the argument name. When used in a function body, the macro must occur in statement position and before any code. @@ -77,6 +94,38 @@ end f(y) = [x for x in y] @specialize ``` + +!!! note + `@nospecialize` affects code generation but not inference: it limits the diversity + of the resulting native code, but it does not impose any limitations (beyond the + standard ones) on type-inference. + +# Example + +```julia +julia> f(A::AbstractArray) = g(A) +f (generic function with 1 method) + +julia> @noinline g(@nospecialize(A::AbstractArray)) = A[1] +g (generic function with 1 method) + +julia> @code_typed f([1.0]) +CodeInfo( +1 ─ %1 = invoke Main.g(_2::AbstractArray)::Float64 +└── return %1 +) => Float64 +``` + +Here, the `@nospecialize` annotation results in the equivalent of + +```julia +f(A::AbstractArray) = invoke(g, Tuple{AbstractArray}, A) +``` + +ensuring that only one version of native code will be generated for `g`, +one that is generic for any `AbstractArray`. +However, the specific return type is still inferred for both `g` and `f`, +and this is still used in optimizing the callers of `f` and `g`. """ macro nospecialize(vars...) if nfields(vars) === 1 @@ -111,7 +160,8 @@ end Tests whether variable `s` is defined in the current scope. -See also [`isdefined`](@ref). +See also [`isdefined`](@ref) for field properties and [`isassigned`](@ref) for +array indexes or [`haskey`](@ref) for other mappings. # Examples ```jldoctest @@ -140,9 +190,37 @@ macro isdefined(s::Symbol) return Expr(:escape, Expr(:isdefined, s)) end +function _is_internal(__module__) + if ccall(:jl_base_relative_to, Any, (Any,), __module__)::Module === Core.Compiler || + nameof(__module__) === :Base + return true + end + return false +end + +# can be used in place of `@pure` (supposed to be used for bootstrapping) macro _pure_meta() - return Expr(:meta, :pure) + return _is_internal(__module__) && Expr(:meta, :pure) +end +# can be used in place of `@assume_effects :total` (supposed to be used for bootstrapping) +macro _total_meta() + return _is_internal(__module__) && Expr(:meta, Expr(:purity, + #=:consistent=#true, + #=:effect_free=#true, + #=:nothrow=#true, + #=:terminates_globally=#true, + #=:terminates_locally=#false)) +end +# can be used in place of `@assume_effects :total_may_throw` (supposed to be used for bootstrapping) +macro _total_may_throw_meta() + return _is_internal(__module__) && Expr(:meta, Expr(:purity, + #=:consistent=#true, + #=:effect_free=#true, + #=:nothrow=#false, + #=:terminates_globally=#true, + #=:terminates_locally=#false)) end + # another version of inlining that propagates an inbounds context macro _propagate_inbounds_meta() return Expr(:meta, :inline, :propagate_inbounds) @@ -170,7 +248,7 @@ Stacktrace: [...] ``` -If `T` is a [`AbstractFloat`](@ref) or [`Rational`](@ref) type, +If `T` is a [`AbstractFloat`](@ref) type, then it will return the closest value to `x` representable by `T`. ```jldoctest @@ -180,11 +258,8 @@ julia> x = 1/3 julia> convert(Float32, x) 0.33333334f0 -julia> convert(Rational{Int32}, x) -1//3 - -julia> convert(Rational{Int64}, x) -6004799503160661//18014398509481984 +julia> convert(BigFloat, x) +0.333333333333333314829616256247390992939472198486328125 ``` If `T` is a collection type and `x` a collection, the result of @@ -197,12 +272,12 @@ julia> y = convert(Vector{Int}, x); julia> y === x true ``` + +See also: [`round`](@ref), [`trunc`](@ref), [`oftype`](@ref), [`reinterpret`](@ref). """ function convert end -convert(::Type{Union{}}, x) = throw(MethodError(convert, (Union{}, x))) -convert(::Type{Any}, x) = x -convert(::Type{T}, x::T) where {T} = x +convert(::Type{Union{}}, @nospecialize x) = throw(MethodError(convert, (Union{}, x))) convert(::Type{Type}, x::Type) = x # the ssair optimizer is strongly dependent on this method existing to avoid over-specialization # in the absence of inlining-enabled # (due to fields typed as `Type`, which is generally a bad idea) @@ -216,10 +291,10 @@ Evaluate an expression with values interpolated into it using `eval`. If two arguments are provided, the first is the module to evaluate in. """ macro eval(ex) - :(Core.eval($__module__, $(Expr(:quote,ex)))) + return Expr(:escape, Expr(:call, GlobalRef(Core, :eval), __module__, Expr(:quote, ex))) end macro eval(mod, ex) - :(Core.eval($(esc(mod)), $(Expr(:quote,ex)))) + return Expr(:escape, Expr(:call, GlobalRef(Core, :eval), mod, Expr(:quote, ex))) end argtail(x, rest...) = rest @@ -229,6 +304,8 @@ argtail(x, rest...) = rest Return a `Tuple` consisting of all but the first component of `x`. +See also: [`front`](@ref Base.front), [`rest`](@ref Base.rest), [`first`](@ref), [`Iterators.peel`](@ref). + # Examples ```jldoctest julia> Base.tail((1,2,3)) @@ -255,6 +332,18 @@ function rewrap_unionall(@nospecialize(t), @nospecialize(u)) return UnionAll(u.var, rewrap_unionall(t, u.body)) end +function rewrap_unionall(t::Core.TypeofVararg, @nospecialize(u)) + isdefined(t, :T) || return t + if !isa(u, UnionAll) + return t + end + T = rewrap_unionall(t.T, u) + if !isdefined(t, :N) || t.N === u.var + return Vararg{T} + end + return Vararg{T, t.N} +end + # replace TypeVars in all enclosing UnionAlls with fresh TypeVars function rename_unionall(@nospecialize(u)) if !isa(u, UnionAll) @@ -271,10 +360,8 @@ function rename_unionall(@nospecialize(u)) return UnionAll(nv, body{nv}) end -const _va_typename = Vararg.body.body.name function isvarargtype(@nospecialize(t)) - t = unwrap_unionall(t) - return isa(t, DataType) && (t::DataType).name === _va_typename + return isa(t, Core.TypeofVararg) end function isvatuple(@nospecialize(t)) @@ -286,18 +373,14 @@ function isvatuple(@nospecialize(t)) return false end -function unwrapva(@nospecialize(t)) - # NOTE: this returns a related type, but it's NOT a subtype of the original tuple - t2 = unwrap_unionall(t) - return isvarargtype(t2) ? rewrap_unionall(t2.parameters[1], t) : t -end +unwrapva(t::Core.TypeofVararg) = isdefined(t, :T) ? t.T : Any +unwrapva(@nospecialize(t)) = t -function unconstrain_vararg_length(@nospecialize(va)) +function unconstrain_vararg_length(va::Core.TypeofVararg) # construct a new Vararg type where its length is unconstrained, # but its element type still captures any dependencies the input # element type may have had on the input length - T = unwrap_unionall(va).parameters[1] - return rewrap_unionall(Vararg{T}, va) + return Vararg{unwrapva(va)} end typename(a) = error("typename does not apply to this type") @@ -310,7 +393,7 @@ function typename(a::Union) end typename(union::UnionAll) = typename(union.body) -_tuple_error(T::Type, x) = (@_noinline_meta; throw(MethodError(convert, (T, x)))) +_tuple_error(T::Type, x) = (@noinline; throw(MethodError(convert, (T, x)))) convert(::Type{T}, x::T) where {T<:Tuple} = x function convert(::Type{T}, x::NTuple{N,Any}) where {N, T<:Tuple} @@ -319,7 +402,7 @@ function convert(::Type{T}, x::NTuple{N,Any}) where {N, T<:Tuple} if typeintersect(NTuple{N,Any}, T) === Union{} _tuple_error(T, x) end - cvt1(n) = (@_inline_meta; convert(fieldtype(T, n), getfield(x, n, #=boundscheck=#false))) + cvt1(n) = (@inline; convert(fieldtype(T, n), getfield(x, n, #=boundscheck=#false))) return ntuple(cvt1, Val(N))::NTuple{N,Any} end @@ -420,15 +503,15 @@ julia> reinterpret(Float32, UInt32[1 2 3 4 5]) ``` """ reinterpret(::Type{T}, x) where {T} = bitcast(T, x) -reinterpret(::Type{Unsigned}, x::Float16) = reinterpret(UInt16,x) -reinterpret(::Type{Signed}, x::Float16) = reinterpret(Int16,x) """ sizeof(T::DataType) sizeof(obj) Size, in bytes, of the canonical binary representation of the given `DataType` `T`, if any. -Size, in bytes, of object `obj` if it is not `DataType`. +Or the size, in bytes, of object `obj` if it is not a `DataType`. + +See also [`summarysize`](@ref). # Examples ```jldoctest @@ -441,7 +524,7 @@ julia> sizeof(ComplexF64) julia> sizeof(1.0) 8 -julia> sizeof([1.0:10.0;]) +julia> sizeof(collect(1.0:10.0)) 80 ``` @@ -456,21 +539,24 @@ Stacktrace: """ sizeof(x) = Core.sizeof(x) -# simple Array{Any} operations needed for bootstrap -@eval setindex!(A::Array{Any}, @nospecialize(x), i::Int) = arrayset($(Expr(:boundscheck)), A, x, i) - """ - precompile(f, args::Tuple{Vararg{Any}}) + ifelse(condition::Bool, x, y) + +Return `x` if `condition` is `true`, otherwise return `y`. This differs from `?` or `if` in +that it is an ordinary function, so all the arguments are evaluated first. In some cases, +using `ifelse` instead of an `if` statement can eliminate the branch in generated code and +provide higher performance in tight loops. -Compile the given function `f` for the argument tuple (of types) `args`, but do not execute it. +# Examples +```jldoctest +julia> ifelse(1 > 2, 1, 2) +2 +``` """ -function precompile(@nospecialize(f), args::Tuple) - ccall(:jl_compile_hint, Int32, (Any,), Tuple{Core.Typeof(f), args...}) != 0 -end +ifelse(condition::Bool, x, y) = Core.ifelse(condition, x, y) -function precompile(argt::Type) - ccall(:jl_compile_hint, Int32, (Any,), argt) != 0 -end +# simple Array{Any} operations needed for bootstrap +@eval setindex!(A::Array{Any}, @nospecialize(x), i::Int) = arrayset($(Expr(:boundscheck)), A, x, i) """ esc(e) @@ -521,7 +607,7 @@ julia> f2() As noted there, the caller must verify—using information they can access—that their accesses are valid before using `@inbounds`. For indexing into your [`AbstractArray`](@ref) subclasses, for example, this involves checking the - indices against its [`size`](@ref). Therefore, `@boundscheck` annotations + indices against its [`axes`](@ref). Therefore, `@boundscheck` annotations should only be added to a [`getindex`](@ref) or [`setindex!`](@ref) implementation after you are certain its behavior is correct. """ @@ -540,7 +626,7 @@ element `i` of array `A` is skipped to improve performance. ```julia function sum(A::AbstractArray) r = zero(eltype(A)) - for i = 1:length(A) + for i in eachindex(A) @inbounds r += A[i] end return r @@ -705,12 +791,11 @@ call obsolete versions of a function `f`. `f` directly, and the type of the result cannot be inferred by the compiler.) """ function invokelatest(@nospecialize(f), @nospecialize args...; kwargs...) + kwargs = merge(NamedTuple(), kwargs) if isempty(kwargs) - return Core._apply_latest(f, args) + return Core._call_latest(f, args...) end - # We use a closure (`inner`) to handle kwargs. - inner() = f(args...; kwargs...) - Core._apply_latest(inner) + return Core._call_latest(Core.kwfunc(f), kwargs, f, args...) end """ @@ -740,21 +825,28 @@ of [`invokelatest`](@ref). world age refers to system state unrelated to the main Julia session. """ function invoke_in_world(world::UInt, @nospecialize(f), @nospecialize args...; kwargs...) + kwargs = Base.merge(NamedTuple(), kwargs) if isempty(kwargs) - return Core._apply_in_world(world, f, args) + return Core._call_in_world(world, f, args...) end - inner() = f(args...; kwargs...) - Core._apply_in_world(world, inner) + return Core._call_in_world(world, Core.kwfunc(f), kwargs, f, args...) end # TODO: possibly make this an intrinsic -inferencebarrier(@nospecialize(x)) = Ref{Any}(x)[] +inferencebarrier(@nospecialize(x)) = RefValue{Any}(x).x """ isempty(collection) -> Bool Determine whether a collection is empty (has no elements). +!!! warning + + `isempty(itr)` may consume the next element of a stateful iterator `itr` + unless an appropriate `Base.isdone(itr)` or `isempty` method is defined. + Use of `isempty` should therefore be avoided when writing generic + code which should support any iterator type. + # Examples ```jldoctest julia> isempty([]) @@ -799,6 +891,8 @@ values(itr) = itr A type with no fields whose singleton instance [`missing`](@ref) is used to represent missing values. + +See also: [`skipmissing`](@ref), [`nonmissingtype`](@ref), [`Nothing`](@ref). """ struct Missing end @@ -806,6 +900,8 @@ struct Missing end missing The singleton instance of type [`Missing`](@ref) representing a missing value. + +See also: [`NaN`](@ref), [`skipmissing`](@ref), [`nonmissingtype`](@ref). """ const missing = Missing() @@ -813,9 +909,10 @@ const missing = Missing() ismissing(x) Indicate whether `x` is [`missing`](@ref). + +See also: [`skipmissing`](@ref), [`isnothing`](@ref), [`isnan`](@ref). """ -ismissing(::Any) = false -ismissing(::Missing) = true +ismissing(x) = x === missing function popfirst! end diff --git a/base/experimental.jl b/base/experimental.jl index 3e5038fb997391..9edd197c198e9c 100644 --- a/base/experimental.jl +++ b/base/experimental.jl @@ -10,6 +10,7 @@ module Experimental using Base: Threads, sync_varname +using Base.Meta """ Const(A::Array) @@ -28,9 +29,9 @@ Base.IndexStyle(::Type{<:Const}) = IndexLinear() Base.size(C::Const) = size(C.a) Base.axes(C::Const) = axes(C.a) @eval Base.getindex(A::Const, i1::Int) = - (Base.@_inline_meta; Core.const_arrayref($(Expr(:boundscheck)), A.a, i1)) + (Base.@inline; Core.const_arrayref($(Expr(:boundscheck)), A.a, i1)) @eval Base.getindex(A::Const, i1::Int, i2::Int, I::Int...) = - (Base.@_inline_meta; Core.const_arrayref($(Expr(:boundscheck)), A.a, i1, i2, I...)) + (Base.@inline; Core.const_arrayref($(Expr(:boundscheck)), A.a, i1, i2, I...)) """ @aliasscope expr @@ -114,14 +115,43 @@ parent module. Supported values are 0, 1, 2, and 3. The effective optimization level is the minimum of that specified on the -command line and in per-module settings. +command line and in per-module settings. If a `--min-optlevel` value is +set on the command line, that is enforced as a lower bound. """ macro optlevel(n::Int) return Expr(:meta, :optlevel, n) end """ - Experimental.@compiler_options optimize={0,1,2,3} compile={yes,no,all,min} infer={yes,no} + Experimental.@max_methods n::Int + +Set the maximum number of potentially-matching methods considered when running inference +for methods defined in the current module. This setting affects inference of calls with +incomplete knowledge of the argument types. + +Supported values are `1`, `2`, `3`, `4`, and `default` (currently equivalent to `3`). +""" +macro max_methods(n::Int) + 0 < n < 5 || error("We must have that `1 <= max_methods <= 4`, but `max_methods = $n`.") + return Expr(:meta, :max_methods, n) +end + +""" + Experimental.@max_methods n::Int function fname end + +Set the maximum number of potentially-matching methods considered when running inference +for the generic function `fname`. Overrides any module-level or global inference settings +for max_methods. This setting is global for the entire generic function (or more precisely +the MethodTable). +""" +macro max_methods(n::Int, fdef::Expr) + 0 < n <= 255 || error("We must have that `1 <= max_methods <= 255`, but `max_methods = $n`.") + (fdef.head == :function && length(fdef.args) == 1) || error("Second argument must be a function forward declaration") + return :(typeof($(esc(fdef))).name.max_methods = $(UInt8(n))) +end + +""" + Experimental.@compiler_options optimize={0,1,2,3} compile={yes,no,all,min} infer={yes,no} max_methods={default,1,2,3,...} Set compiler options for code in the enclosing module. Options correspond directly to command-line options with the same name, where applicable. The following options @@ -131,6 +161,7 @@ are currently supported: * `compile`: Toggle native code compilation. Currently only `min` is supported, which requests the minimum possible amount of compilation. * `infer`: Enable or disable type inference. If disabled, implies [`@nospecialize`](@ref). + * `max_methods`: Maximum number of matching methods considered when running type inference. """ macro compiler_options(args...) opts = Expr(:block) @@ -150,6 +181,12 @@ macro compiler_options(args...) a = a === false || a === :no ? 0 : a === true || a === :yes ? 1 : error("invalid argument to \"infer\" option") push!(opts.args, Expr(:meta, :infer, a)) + elseif ex.args[1] === :max_methods + a = ex.args[2] + a = a === :default ? 3 : + a isa Int ? ((0 < a < 5) ? a : error("We must have that `1 <= max_methods <= 4`, but `max_methods = $a`.")) : + error("invalid argument to \"max_methods\" option") + push!(opts.args, Expr(:meta, :max_methods, a)) else error("unknown option \"$(ex.args[1])\"") end @@ -160,6 +197,30 @@ macro compiler_options(args...) return opts end +""" + Experimental.@force_compile + +Force compilation of the block or function (Julia's built-in interpreter is blocked from executing it). + +# Examples + +``` +julia> occursin("interpreter", string(stacktrace(begin + # with forced compilation + Base.Experimental.@force_compile + backtrace() + end, true))) +false + +julia> occursin("interpreter", string(stacktrace(begin + # without forced compilation + backtrace() + end, true))) +true +``` +""" +macro force_compile() Expr(:meta, :force_compile) end + # UI features for errors """ @@ -241,7 +302,8 @@ the handler for that type. This interface is experimental and subject to change or removal without notice. """ function show_error_hints(io, ex, args...) - hinters = get!(()->[], _hint_handlers, typeof(ex)) + hinters = get(_hint_handlers, typeof(ex), nothing) + isnothing(hinters) && return for handler in hinters try Base.invokelatest(handler, io, ex, args...) @@ -252,4 +314,49 @@ function show_error_hints(io, ex, args...) end end +# OpaqueClosure +include("opaque_closure.jl") + +""" + Experimental.@overlay mt [function def] + +Define a method and add it to the method table `mt` instead of to the global method table. +This can be used to implement a method override mechanism. Regular compilation will not +consider these methods, and you should customize the compilation flow to look in these +method tables (e.g., using [`Core.Compiler.OverlayMethodTable`](@ref)). + +""" +macro overlay(mt, def) + def = macroexpand(__module__, def) # to expand @inline, @generated, etc + if !isexpr(def, [:function, :(=)]) + error("@overlay requires a function Expr") + end + if isexpr(def.args[1], :call) + def.args[1].args[1] = Expr(:overlay, mt, def.args[1].args[1]) + elseif isexpr(def.args[1], :where) + def.args[1].args[1].args[1] = Expr(:overlay, mt, def.args[1].args[1].args[1]) + else + error("@overlay requires a function Expr") + end + esc(def) +end + +let new_mt(name::Symbol, mod::Module) = begin + ccall(:jl_check_top_level_effect, Cvoid, (Any, Cstring), mod, "@MethodTable") + ccall(:jl_new_method_table, Any, (Any, Any), name, mod) + end + @eval macro MethodTable(name::Symbol) + esc(:(const $name = $$new_mt($(quot(name)), $(__module__)))) + end +end + +""" + Experimental.@MethodTable(name) + +Create a new MethodTable in the current module, bound to `name`. This method table can be +used with the [`Experimental.@overlay`](@ref) macro to define methods for a function without +adding them to the global method table. +""" +:@MethodTable + end diff --git a/base/exports.jl b/base/exports.jl index 121f42db09af97..dff6b0c9bc2081 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -22,6 +22,8 @@ export AbstractVector, AbstractVecOrMat, Array, + AbstractMatch, + AbstractPattern, AbstractDict, BigFloat, BigInt, @@ -38,6 +40,7 @@ export ComplexF64, ComplexF32, ComplexF16, + ComposedFunction, DenseMatrix, DenseVecOrMat, DenseVector, @@ -54,6 +57,7 @@ export IOStream, LinRange, Irrational, + LazyString, Matrix, MergeSort, Missing, @@ -67,6 +71,7 @@ export Rational, Regex, RegexMatch, + Returns, RoundFromZero, RoundDown, RoundingMode, @@ -117,6 +122,7 @@ export Cwstring, # Exceptions + CanonicalIndexError, CapturedException, CompositeException, DimensionMismatch, @@ -165,6 +171,10 @@ export ≢, xor, ⊻, + nand, + nor, + ⊼, + ⊽, %, ÷, &, @@ -226,6 +236,7 @@ export cbrt, ceil, cis, + cispi, clamp, cld, cmp, @@ -376,12 +387,14 @@ export eachindex, eachrow, eachslice, + extrema!, extrema, fill!, fill, first, hcat, hvcat, + hvncat, indexin, argmax, argmin, @@ -457,6 +470,7 @@ export searchsorted, searchsortedfirst, searchsortedlast, + insorted, startswith, # linear algebra @@ -484,6 +498,7 @@ export # collections all!, all, + allequal, allunique, any!, any, @@ -493,6 +508,7 @@ export count, delete!, deleteat!, + keepat!, eltype, empty!, empty, @@ -561,11 +577,14 @@ export bytes2hex, chomp, chop, + chopprefix, + chopsuffix, codepoint, codeunit, codeunits, digits, digits!, + eachsplit, escape_string, hex2bytes, hex2bytes!, @@ -650,7 +669,9 @@ export isbits, isequal, ismutable, + ismutabletype, isless, + isunordered, ifelse, objectid, sizeof, @@ -663,6 +684,7 @@ export istaskstarted, istaskfailed, lock, + @lock, notify, ReentrantLock, schedule, @@ -675,6 +697,7 @@ export timedwait, asyncmap, asyncmap!, + errormonitor, # channels take!, @@ -685,9 +708,11 @@ export # missing values coalesce, + @coalesce, ismissing, missing, skipmissing, + @something, something, isnothing, nonmissingtype, @@ -700,6 +725,7 @@ export # errors backtrace, catch_backtrace, + current_exceptions, error, rethrow, retry, @@ -712,6 +738,9 @@ export convert, getproperty, setproperty!, + swapproperty!, + modifyproperty!, + replaceproperty!, fieldoffset, fieldname, fieldnames, @@ -781,6 +810,7 @@ export # I/O and events close, + closewrite, countlines, eachline, readeach, @@ -814,6 +844,7 @@ export readline, readlines, readuntil, + redirect_stdio, redirect_stderr, redirect_stdin, redirect_stdout, @@ -868,10 +899,12 @@ export chown, cp, ctime, + diskstat, download, filemode, filesize, gperm, + hardlink, isblockdev, ischardev, isdir, @@ -905,7 +938,7 @@ export uperm, walkdir, -# external processes ## TODO: whittle down these exports. +# external processes detach, getpid, ignorestatus, @@ -915,6 +948,7 @@ export run, setenv, addenv, + setcpuaffinity, success, withenv, @@ -955,6 +989,7 @@ export @v_str, # version number @raw_str, # raw string with no interpolation/unescaping @NamedTuple, + @lazy_str, # lazy string # documentation @text_str, @@ -966,6 +1001,7 @@ export # profiling @time, + @showtime, @timed, @timev, @elapsed, @@ -995,6 +1031,9 @@ export @polly, @assert, + @atomic, + @atomicswap, + @atomicreplace, @__dot__, @enum, @label, diff --git a/base/expr.jl b/base/expr.jl index ff5e92005b8dd0..e0cd8a9b0a32c6 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -1,11 +1,15 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license +isexpr(@nospecialize(ex), heads) = isa(ex, Expr) && in(ex.head, heads) +isexpr(@nospecialize(ex), heads, n::Int) = isa(ex, Expr) && in(ex.head, heads) && length(ex.args) == n +const is_expr = isexpr + ## symbols ## """ gensym([tag]) -Generates a symbol which will not conflict with other variable names. +Generates a symbol which will not conflict with other variable names (in the same module). """ gensym() = ccall(:jl_gensym, Ref{Symbol}, ()) @@ -31,6 +35,9 @@ end ## expressions ## +isexpr(@nospecialize(ex), head::Symbol) = isa(ex, Expr) && ex.head === head +isexpr(@nospecialize(ex), head::Symbol, n::Int) = isa(ex, Expr) && ex.head === head && length(ex.args) == n + copy(e::Expr) = exprarray(e.head, copy_exprargs(e.args)) # copy parts of an AST that the compiler mutates @@ -185,18 +192,74 @@ Give a hint to the compiler that this function is worth inlining. Small functions typically do not need the `@inline` annotation, as the compiler does it automatically. By using `@inline` on bigger functions, an extra nudge can be given to the compiler to inline it. -This is shown in the following example: + +`@inline` can be applied immediately before the definition or in its function body. ```julia -@inline function bigfunction(x) - #= - Function Definition - =# +# annotate long-form definition +@inline function longdef(x) + ... +end + +# annotate short-form definition +@inline shortdef(x) = ... + +# annotate anonymous function that a `do` block creates +f() do + @inline + ... end ``` + +!!! compat "Julia 1.8" + The usage within a function body requires at least Julia 1.8. + +--- + @inline block + +Give a hint to the compiler that calls within `block` are worth inlining. + +```julia +# The compiler will try to inline `f` +@inline f(...) + +# The compiler will try to inline `f`, `g` and `+` +@inline f(...) + g(...) +``` + +!!! note + A callsite annotation always has the precedence over the annotation applied to the + definition of the called function: + ```julia + @noinline function explicit_noinline(args...) + # body + end + + let + @inline explicit_noinline(args...) # will be inlined + end + ``` + +!!! note + When there are nested callsite annotations, the innermost annotation has the precedence: + ```julia + @noinline let a0, b0 = ... + a = @inline f(a0) # the compiler will try to inline this call + b = f(b0) # the compiler will NOT try to inline this call + return a, b + end + ``` + +!!! warning + Although a callsite annotation will try to force inlining in regardless of the cost model, + there are still chances it can't succeed in it. Especially, recursive calls can not be + inlined even if they are annotated as `@inline`d. + +!!! compat "Julia 1.8" + The callsite annotation requires at least Julia 1.8. """ -macro inline(ex) - esc(isa(ex, Expr) ? pushmeta!(ex, :inline) : ex) +macro inline(x) + return annotate_meta_def_or_block(x, :inline) end """ @@ -206,42 +269,339 @@ Give a hint to the compiler that it should not inline a function. Small functions are typically inlined automatically. By using `@noinline` on small functions, auto-inlining can be -prevented. This is shown in the following example: +prevented. + +`@noinline` can be applied immediately before the definition or in its function body. ```julia -@noinline function smallfunction(x) - #= - Function Definition - =# +# annotate long-form definition +@noinline function longdef(x) + ... +end + +# annotate short-form definition +@noinline shortdef(x) = ... + +# annotate anonymous function that a `do` block creates +f() do + @noinline + ... end ``` +!!! compat "Julia 1.8" + The usage within a function body requires at least Julia 1.8. + +--- + @noinline block + +Give a hint to the compiler that it should not inline the calls within `block`. + +```julia +# The compiler will try to not inline `f` +@noinline f(...) + +# The compiler will try to not inline `f`, `g` and `+` +@noinline f(...) + g(...) +``` + +!!! note + A callsite annotation always has the precedence over the annotation applied to the + definition of the called function: + ```julia + @inline function explicit_inline(args...) + # body + end + + let + @noinline explicit_inline(args...) # will not be inlined + end + ``` + +!!! note + When there are nested callsite annotations, the innermost annotation has the precedence: + ```julia + @inline let a0, b0 = ... + a = @noinline f(a0) # the compiler will NOT try to inline this call + b = f(b0) # the compiler will try to inline this call + return a, b + end + ``` + +!!! compat "Julia 1.8" + The callsite annotation requires at least Julia 1.8. + +--- !!! note If the function is trivial (for example returning a constant) it might get inlined anyway. """ -macro noinline(ex) - esc(isa(ex, Expr) ? pushmeta!(ex, :noinline) : ex) +macro noinline(x) + return annotate_meta_def_or_block(x, :noinline) end """ @pure ex - @pure(ex) `@pure` gives the compiler a hint for the definition of a pure function, helping for type inference. -A pure function can only depend on immutable information. -This also means a `@pure` function cannot use any global mutable state, including -generic functions. Calls to generic functions depend on method tables which are -mutable global state. -Use with caution, incorrect `@pure` annotation of a function may introduce -hard to identify bugs. Double check for calls to generic functions. -This macro is intended for internal compiler use and may be subject to changes. +!!! warning + This macro is intended for internal compiler use and may be subject to changes. + +!!! warning + In Julia 1.8 and higher, it is favorable to use [`@assume_effects`](@ref) instead of `@pure`. + This is because `@assume_effects` allows a finer grained control over Julia's purity + modeling and the effect system enables a wider range of optimizations. """ macro pure(ex) esc(isa(ex, Expr) ? pushmeta!(ex, :pure) : ex) end +""" + @constprop setting ex + +`@constprop` controls the mode of interprocedural constant propagation for the +annotated function. Two `setting`s are supported: + +- `@constprop :aggressive ex`: apply constant propagation aggressively. + For a method where the return type depends on the value of the arguments, + this can yield improved inference results at the cost of additional compile time. +- `@constprop :none ex`: disable constant propagation. This can reduce compile + times for functions that Julia might otherwise deem worthy of constant-propagation. + Common cases are for functions with `Bool`- or `Symbol`-valued arguments or keyword arguments. +""" +macro constprop(setting, ex) + if isa(setting, QuoteNode) + setting = setting.value + end + setting === :aggressive && return esc(isa(ex, Expr) ? pushmeta!(ex, :aggressive_constprop) : ex) + setting === :none && return esc(isa(ex, Expr) ? pushmeta!(ex, :no_constprop) : ex) + throw(ArgumentError("@constprop $setting not supported")) +end + +""" + @assume_effects setting... ex + +`@assume_effects` overrides the compiler's effect modeling for the given method. +`ex` must be a method definition or `@ccall` expression. + +```jldoctest +julia> Base.@assume_effects :terminates_locally function pow(x) + # this :terminates_locally allows `pow` to be constant-folded + res = 1 + 1 < x < 20 || error("bad pow") + while x > 1 + res *= x + x -= 1 + end + return res + end +pow (generic function with 1 method) + +julia> code_typed() do + pow(12) + end +1-element Vector{Any}: + CodeInfo( +1 ─ return 479001600 +) => Int64 + +julia> Base.@assume_effects :total_may_throw @ccall jl_type_intersection(Vector{Int}::Any, Vector{<:Integer}::Any)::Any +Vector{Int64} (alias for Array{Int64, 1}) +``` + +!!! warning + Improper use of this macro causes undefined behavior (including crashes, + incorrect answers, or other hard to track bugs). Use with care and only if + absolutely required. + +In general, each `setting` value makes an assertion about the behavior of the +function, without requiring the compiler to prove that this behavior is indeed +true. These assertions are made for all world ages. It is thus advisable to limit +the use of generic functions that may later be extended to invalidate the +assumption (which would cause undefined behavior). + +The following `setting`s are supported. +- `:consistent` +- `:effect_free` +- `:nothrow` +- `:terminates_globally` +- `:terminates_locally` +- `:total` + +--- +# `:consistent` + +The `:consistent` setting asserts that for egal (`===`) inputs: +- The manner of termination (return value, exception, non-termination) will always be the same. +- If the method returns, the results will always be egal. + +!!! note + This in particular implies that the return value of the method must be + immutable. Multiple allocations of mutable objects (even with identical + contents) are not egal. + +!!! note + The `:consistent`-cy assertion is made world-age wise. More formally, write + ``fᵢ`` for the evaluation of ``f`` in world-age ``i``, then we require: + ```math + ∀ i, x, y: x ≡ y → fᵢ(x) ≡ fᵢ(y) + ``` + However, for two world ages ``i``, ``j`` s.t. ``i ≠ j``, we may have ``fᵢ(x) ≢ fⱼ(y)``. + + A further implication is that `:consistent` functions may not make their + return value dependent on the state of the heap or any other global state + that is not constant for a given world age. + +!!! note + The `:consistent`-cy includes all legal rewrites performed by the optimizer. + For example, floating-point fastmath operations are not considered `:consistent`, + because the optimizer may rewrite them causing the output to not be `:consistent`, + even for the same world age (e.g. because one ran in the interpreter, while + the other was optimized). + +!!! note + If `:consistent` functions terminate by throwing an exception, that exception + itself is not required to meet the egality requirement specified above. + +--- +# `:effect_free` + +The `:effect_free` setting asserts that the method is free of externally semantically +visible side effects. The following is an incomplete list of externally semantically +visible side effects: +- Changing the value of a global variable. +- Mutating the heap (e.g. an array or mutable value), except as noted below +- Changing the method table (e.g. through calls to eval) +- File/Network/etc. I/O +- Task switching + +However, the following are explicitly not semantically visible, even if they +may be observable: +- Memory allocations (both mutable and immutable) +- Elapsed time +- Garbage collection +- Heap mutations of objects whose lifetime does not exceed the method (i.e. + were allocated in the method and do not escape). +- The returned value (which is externally visible, but not a side effect) + +The rule of thumb here is that an externally visible side effect is anything +that would affect the execution of the remainder of the program if the function +were not executed. + +!!! note + The `:effect_free` assertion is made both for the method itself and any code + that is executed by the method. Keep in mind that the assertion must be + valid for all world ages and limit use of this assertion accordingly. + +--- +# `:nothrow` + +The `:nothrow` settings asserts that this method does not terminate abnormally +(i.e. will either always return a value or never return). + +!!! note + It is permissible for `:nothrow` annotated methods to make use of exception + handling internally as long as the exception is not rethrown out of the + method itself. + +!!! note + `MethodErrors` and similar exceptions count as abnormal termination. + +--- +# `:terminates_globally` + +The `:terminates_globally` settings asserts that this method will eventually terminate +(either normally or abnormally), i.e. does not loop indefinitely. + +!!! note + This `:terminates_globally` assertion covers any other methods called by the annotated method. + +!!! note + The compiler will consider this a strong indication that the method will + terminate relatively *quickly* and may (if otherwise legal), call this + method at compile time. I.e. it is a bad idea to annotate this setting + on a method that *technically*, but not *practically*, terminates. + +--- +# `:terminates_locally` + +The `:terminates_locally` setting is like `:terminates_globally`, except that it only +applies to syntactic control flow *within* the annotated method. It is thus +a much weaker (and thus safer) assertion that allows for the possibility of +non-termination if the method calls some other method that does not terminate. + +!!! note + `:terminates_globally` implies `:terminates_locally`. + +--- +# `:total` + +This `setting` combines the following other assertions: +- `:consistent` +- `:effect_free` +- `:nothrow` +- `:terminates_globally` +and is a convenient shortcut. + +--- +# `:total_may_throw` + +This `setting` combines the following other assertions: +- `:consistent` +- `:effect_free` +- `:terminates_globally` +and is a convenient shortcut. + +!!! note + This setting is particularly useful since it allows the compiler to evaluate a call of + the applied method when all the call arguments are fully known to be constant, no matter + if the call results in an error or not. + + `@assume_effects :total_may_throw` is similar to [`@pure`](@ref) with the primary + distinction that the `:consistent`-cy requirement applies world-age wise rather + than globally as described above. However, in particular, a method annotated + `@pure` should always be `:total` or `:total_may_throw`. + Another advantage is that effects introduced by `@assume_effects` are propagated to + callers interprocedurally while a purity defined by `@pure` is not. +""" +macro assume_effects(args...) + (consistent, effect_free, nothrow, terminates_globally, terminates_locally) = + (false, false, false, false, false, false) + for setting in args[1:end-1] + if isa(setting, QuoteNode) + setting = setting.value + end + if setting === :consistent + consistent = true + elseif setting === :effect_free + effect_free = true + elseif setting === :nothrow + nothrow = true + elseif setting === :terminates_globally + terminates_globally = true + elseif setting === :terminates_locally + terminates_locally = true + elseif setting === :total + consistent = effect_free = nothrow = terminates_globally = true + elseif setting === :total_may_throw + consistent = effect_free = terminates_globally = true + else + throw(ArgumentError("@assume_effects $setting not supported")) + end + end + ex = args[end] + isa(ex, Expr) || throw(ArgumentError("Bad expression `$ex` in `@assume_effects [settings] ex`")) + if ex.head === :macrocall && ex.args[1] == Symbol("@ccall") + ex.args[1] = GlobalRef(Base, Symbol("@ccall_effects")) + insert!(ex.args, 3, Core.Compiler.encode_effects_override(Core.Compiler.EffectsOverride( + consistent, effect_free, nothrow, terminates_globally, terminates_locally + ))) + return esc(ex) + end + return esc(pushmeta!(ex, :purity, consistent, effect_free, nothrow, terminates_globally, terminates_locally)) +end + """ @propagate_inbounds @@ -266,6 +626,15 @@ end ## some macro utilities ## +unwrap_macrocalls(@nospecialize(x)) = x +function unwrap_macrocalls(ex::Expr) + inner = ex + while inner.head === :macrocall + inner = inner.args[end]::Expr + end + return inner +end + function pushmeta!(ex::Expr, sym::Symbol, args::Any...) if isempty(args) tag = sym @@ -273,10 +642,7 @@ function pushmeta!(ex::Expr, sym::Symbol, args::Any...) tag = Expr(sym, args...)::Expr end - inner = ex - while inner.head === :macrocall - inner = inner.args[end]::Expr - end + inner = unwrap_macrocalls(ex) idx, exargs = findmeta(inner) if idx != 0 @@ -326,8 +692,23 @@ function findmetaarg(metaargs, sym) return 0 end -function is_short_function_def(ex) - ex.head === :(=) || return false +function annotate_meta_def_or_block(@nospecialize(ex), meta::Symbol) + inner = unwrap_macrocalls(ex) + if is_function_def(inner) + # annotation on a definition + return esc(pushmeta!(ex, meta)) + else + # annotation on a block + return Expr(:block, + Expr(meta, true), + Expr(:local, Expr(:(=), :val, esc(ex))), + Expr(meta, false), + :val) + end +end + +function is_short_function_def(@nospecialize(ex)) + isexpr(ex, :(=)) || return false while length(ex.args) >= 1 && isa(ex.args[1], Expr) (ex.args[1].head === :call) && return true (ex.args[1].head === :where || ex.args[1].head === :(::)) || return false @@ -335,9 +716,11 @@ function is_short_function_def(ex) end return false end +is_function_def(@nospecialize(ex)) = + return isexpr(ex, :function) || is_short_function_def(ex) || isexpr(ex, :->) function findmeta(ex::Expr) - if ex.head === :function || is_short_function_def(ex) || ex.head === :-> + if is_function_def(ex) body = ex.args[2]::Expr body.head === :block || error(body, " is not a block expression") return findmeta_block(ex.args) @@ -391,7 +774,7 @@ end """ @generated f - @generated(f) + `@generated` is used to annotate a function which will be generated. In the body of the generated function, only types of arguments can be read (not the values). The function returns a quoted expression evaluated when the @@ -401,7 +784,7 @@ the global scope or depending on mutable elements. See [Metaprogramming](@ref) for further details. ## Example: -```julia +```jldoctest julia> @generated function bar(x) if x <: Integer return :(x ^ 2) @@ -422,12 +805,16 @@ macro generated(f) if isa(f, Expr) && (f.head === :function || is_short_function_def(f)) body = f.args[2] lno = body.args[1] + tmp = gensym("tmp") return Expr(:escape, Expr(f.head, f.args[1], Expr(:block, lno, Expr(:if, Expr(:generated), - body, + # https://github.com/JuliaLang/julia/issues/25678 + Expr(:block, + :(local $tmp = $body), + :(if $tmp isa $(GlobalRef(Core, :CodeInfo)); return $tmp; else $tmp; end)), Expr(:block, Expr(:meta, :generated_only), Expr(:return, nothing)))))) @@ -435,3 +822,234 @@ macro generated(f) error("invalid syntax; @generated must be used with a function definition") end end + + +""" + @atomic var + @atomic order ex + +Mark `var` or `ex` as being performed atomically, if `ex` is a supported expression. + + @atomic a.b.x = new + @atomic a.b.x += addend + @atomic :release a.b.x = new + @atomic :acquire_release a.b.x += addend + +Perform the store operation expressed on the right atomically and return the +new value. + +With `=`, this operation translates to a `setproperty!(a.b, :x, new)` call. +With any operator also, this operation translates to a `modifyproperty!(a.b, +:x, +, addend)[2]` call. + + @atomic a.b.x max arg2 + @atomic a.b.x + arg2 + @atomic max(a.b.x, arg2) + @atomic :acquire_release max(a.b.x, arg2) + @atomic :acquire_release a.b.x + arg2 + @atomic :acquire_release a.b.x max arg2 + +Perform the binary operation expressed on the right atomically. Store the +result into the field in the first argument and return the values `(old, new)`. + +This operation translates to a `modifyproperty!(a.b, :x, func, arg2)` call. + + +See [Per-field atomics](@ref man-atomics) section in the manual for more details. + +```jldoctest +julia> mutable struct Atomic{T}; @atomic x::T; end + +julia> a = Atomic(1) +Atomic{Int64}(1) + +julia> @atomic a.x # fetch field x of a, with sequential consistency +1 + +julia> @atomic :sequentially_consistent a.x = 2 # set field x of a, with sequential consistency +2 + +julia> @atomic a.x += 1 # increment field x of a, with sequential consistency +3 + +julia> @atomic a.x + 1 # increment field x of a, with sequential consistency +3 => 4 + +julia> @atomic a.x # fetch field x of a, with sequential consistency +4 + +julia> @atomic max(a.x, 10) # change field x of a to the max value, with sequential consistency +4 => 10 + +julia> @atomic a.x max 5 # again change field x of a to the max value, with sequential consistency +10 => 10 +``` + +!!! compat "Julia 1.7" + This functionality requires at least Julia 1.7. +""" +macro atomic(ex) + if !isa(ex, Symbol) && !is_expr(ex, :(::)) + return make_atomic(QuoteNode(:sequentially_consistent), ex) + end + return esc(Expr(:atomic, ex)) +end +macro atomic(order, ex) + order isa QuoteNode || (order = esc(order)) + return make_atomic(order, ex) +end +macro atomic(a1, op, a2) + return make_atomic(QuoteNode(:sequentially_consistent), a1, op, a2) +end +macro atomic(order, a1, op, a2) + order isa QuoteNode || (order = esc(order)) + return make_atomic(order, a1, op, a2) +end +function make_atomic(order, ex) + @nospecialize + if ex isa Expr + if isexpr(ex, :., 2) + l, r = esc(ex.args[1]), esc(ex.args[2]) + return :(getproperty($l, $r, $order)) + elseif isexpr(ex, :call, 3) + return make_atomic(order, ex.args[2], ex.args[1], ex.args[3]) + elseif ex.head === :(=) + l, r = ex.args[1], esc(ex.args[2]) + if is_expr(l, :., 2) + ll, lr = esc(l.args[1]), esc(l.args[2]) + return :(setproperty!($ll, $lr, $r, $order)) + end + end + if length(ex.args) == 2 + if ex.head === :(+=) + op = :+ + elseif ex.head === :(-=) + op = :- + elseif @isdefined string + shead = string(ex.head) + if endswith(shead, '=') + op = Symbol(shead[1:prevind(shead, end)]) + end + end + if @isdefined(op) + return Expr(:ref, make_atomic(order, ex.args[1], op, ex.args[2]), 2) + end + end + end + error("could not parse @atomic expression $ex") +end +function make_atomic(order, a1, op, a2) + @nospecialize + is_expr(a1, :., 2) || error("@atomic modify expression missing field access") + a1l, a1r, op, a2 = esc(a1.args[1]), esc(a1.args[2]), esc(op), esc(a2) + return :(modifyproperty!($a1l, $a1r, $op, $a2, $order)) +end + + +""" + @atomicswap a.b.x = new + @atomicswap :sequentially_consistent a.b.x = new + +Stores `new` into `a.b.x` and returns the old value of `a.b.x`. + +This operation translates to a `swapproperty!(a.b, :x, new)` call. + +See [Per-field atomics](@ref man-atomics) section in the manual for more details. + +```jldoctest +julia> mutable struct Atomic{T}; @atomic x::T; end + +julia> a = Atomic(1) +Atomic{Int64}(1) + +julia> @atomicswap a.x = 2+2 # replace field x of a with 4, with sequential consistency +1 + +julia> @atomic a.x # fetch field x of a, with sequential consistency +4 +``` + +!!! compat "Julia 1.7" + This functionality requires at least Julia 1.7. +""" +macro atomicswap(order, ex) + order isa QuoteNode || (order = esc(order)) + return make_atomicswap(order, ex) +end +macro atomicswap(ex) + return make_atomicswap(QuoteNode(:sequentially_consistent), ex) +end +function make_atomicswap(order, ex) + @nospecialize + is_expr(ex, :(=), 2) || error("@atomicswap expression missing assignment") + l, val = ex.args[1], esc(ex.args[2]) + is_expr(l, :., 2) || error("@atomicswap expression missing field access") + ll, lr = esc(l.args[1]), esc(l.args[2]) + return :(swapproperty!($ll, $lr, $val, $order)) +end + + +""" + @atomicreplace a.b.x expected => desired + @atomicreplace :sequentially_consistent a.b.x expected => desired + @atomicreplace :sequentially_consistent :monotonic a.b.x expected => desired + +Perform the conditional replacement expressed by the pair atomically, returning +the values `(old, success::Bool)`. Where `success` indicates whether the +replacement was completed. + +This operation translates to a `replaceproperty!(a.b, :x, expected, desired)` call. + +See [Per-field atomics](@ref man-atomics) section in the manual for more details. + +```jldoctest +julia> mutable struct Atomic{T}; @atomic x::T; end + +julia> a = Atomic(1) +Atomic{Int64}(1) + +julia> @atomicreplace a.x 1 => 2 # replace field x of a with 2 if it was 1, with sequential consistency +(old = 1, success = true) + +julia> @atomic a.x # fetch field x of a, with sequential consistency +2 + +julia> @atomicreplace a.x 1 => 2 # replace field x of a with 2 if it was 1, with sequential consistency +(old = 2, success = false) + +julia> xchg = 2 => 0; # replace field x of a with 0 if it was 1, with sequential consistency + +julia> @atomicreplace a.x xchg +(old = 2, success = true) + +julia> @atomic a.x # fetch field x of a, with sequential consistency +0 +``` + +!!! compat "Julia 1.7" + This functionality requires at least Julia 1.7. +""" +macro atomicreplace(success_order, fail_order, ex, old_new) + fail_order isa QuoteNode || (fail_order = esc(fail_order)) + success_order isa QuoteNode || (success_order = esc(success_order)) + return make_atomicreplace(success_order, fail_order, ex, old_new) +end +macro atomicreplace(order, ex, old_new) + order isa QuoteNode || (order = esc(order)) + return make_atomicreplace(order, order, ex, old_new) +end +macro atomicreplace(ex, old_new) + return make_atomicreplace(QuoteNode(:sequentially_consistent), QuoteNode(:sequentially_consistent), ex, old_new) +end +function make_atomicreplace(success_order, fail_order, ex, old_new) + @nospecialize + is_expr(ex, :., 2) || error("@atomicreplace expression missing field access") + ll, lr = esc(ex.args[1]), esc(ex.args[2]) + if is_expr(old_new, :call, 3) && old_new.args[1] === :(=>) + exp, rep = esc(old_new.args[2]), esc(old_new.args[3]) + return :(replaceproperty!($ll, $lr, $exp, $rep, $success_order, $fail_order)) + else + old_new = esc(old_new) + return :(replaceproperty!($ll, $lr, $old_new::Pair..., $success_order, $fail_order)) + end +end diff --git a/base/fastmath.jl b/base/fastmath.jl index 62223d5b88516c..05a5ce0503e689 100644 --- a/base/fastmath.jl +++ b/base/fastmath.jl @@ -158,7 +158,7 @@ end # Basic arithmetic -const FloatTypes = Union{Float32,Float64} +const FloatTypes = Union{Float16,Float32,Float64} sub_fast(x::FloatTypes) = neg_float_fast(x) @@ -273,37 +273,18 @@ end # Math functions +exp2_fast(x::Union{Float32,Float64}) = Base.Math.exp2_fast(x) +exp_fast(x::Union{Float32,Float64}) = Base.Math.exp_fast(x) +exp10_fast(x::Union{Float32,Float64}) = Base.Math.exp10_fast(x) # builtins -pow_fast(x::Float32, y::Integer) = ccall("llvm.powi.f32", llvmcall, Float32, (Float32, Int32), x, y) -pow_fast(x::Float64, y::Integer) = ccall("llvm.powi.f64", llvmcall, Float64, (Float64, Int32), x, y) +pow_fast(x::Float32, y::Integer) = ccall("llvm.powi.f32.i32", llvmcall, Float32, (Float32, Int32), x, y) +pow_fast(x::Float64, y::Integer) = ccall("llvm.powi.f64.i32", llvmcall, Float64, (Float64, Int32), x, y) pow_fast(x::FloatTypes, ::Val{p}) where {p} = pow_fast(x, p) # inlines already via llvm.powi @inline pow_fast(x, v::Val) = Base.literal_pow(^, x, v) sqrt_fast(x::FloatTypes) = sqrt_llvm_fast(x) - -# libm - -const libm = Base.libm_name - -for f in (:acosh, :asinh, :atanh, :cbrt, - :cosh, :exp2, :expm1, :log10, :log1p, :log2, - :log, :sinh, :tanh) - f_fast = fast_op[f] - @eval begin - $f_fast(x::Float32) = - ccall(($(string(f,"f")),libm), Float32, (Float32,), x) - $f_fast(x::Float64) = - ccall(($(string(f)),libm), Float64, (Float64,), x) - end -end - -pow_fast(x::Float32, y::Float32) = - ccall(("powf",libm), Float32, (Float32,Float32), x, y) -pow_fast(x::Float64, y::Float64) = - ccall(("pow",libm), Float64, (Float64,Float64), x, y) - sincos_fast(v::FloatTypes) = sincos(v) @inline function sincos_fast(v::Float16) diff --git a/base/file.jl b/base/file.jl index a844f06d0cf546..371a56acf753aa 100644 --- a/base/file.jl +++ b/base/file.jl @@ -8,6 +8,8 @@ export chown, cp, cptree, + diskstat, + hardlink, mkdir, mkpath, mktemp, @@ -34,6 +36,8 @@ export Get the current working directory. +See also: [`cd`](@ref), [`tempdir`](@ref). + # Examples ```julia-repl julia> pwd() @@ -56,7 +60,7 @@ function pwd() elseif rc == Base.UV_ENOBUFS resize!(buf, sz[] - 1) # space for null-terminator implied by StringVector else - uv_error(:cwd, rc) + uv_error("pwd()", rc) end end end @@ -67,6 +71,8 @@ end Set the current working directory. +See also: [`pwd`](@ref), [`mkdir`](@ref), [`mkpath`](@ref), [`mktempdir`](@ref). + # Examples ```julia-repl julia> cd("/home/JuliaUser/Projects/julia") @@ -81,7 +87,9 @@ julia> pwd() ``` """ function cd(dir::AbstractString) - uv_error("chdir $dir", ccall(:uv_chdir, Cint, (Cstring,), dir)) + err = ccall(:uv_chdir, Cint, (Cstring,), dir) + err < 0 && uv_error("cd($(repr(dir)))", err) + return nothing end cd() = cd(homedir()) @@ -173,10 +181,10 @@ function mkdir(path::AbstractString; mode::Integer = 0o777) (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Cint, Ptr{Cvoid}), C_NULL, req, path, checkmode(mode), C_NULL) if ret < 0 - ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req) - uv_error("mkdir", ret) + uv_fs_req_cleanup(req) + uv_error("mkdir($(repr(path)); mode=0o$(string(mode,base=8)))", ret) end - ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req) + uv_fs_req_cleanup(req) return path finally Libc.free(req) @@ -186,21 +194,20 @@ end """ mkpath(path::AbstractString; mode::Unsigned = 0o777) -Create all directories in the given `path`, with permissions `mode`. `mode` defaults to -`0o777`, modified by the current file creation mask. -Return `path`. +Create all intermediate directories in the `path` as required. Directories are created with +the permissions `mode` which defaults to `0o777` and is modified by the current file +creation mask. Unlike [`mkdir`](@ref), `mkpath` does not error if `path` (or parts of it) +already exists. However, an error will be thrown if `path` (or parts of it) points to an +existing file. Return `path`. + +If `path` includes a filename you will probably want to use `mkpath(dirname(path))` to +avoid creating a directory using the filename. # Examples ```julia-repl -julia> mkdir("testingdir") -"testingdir" +julia> cd(mktempdir()) -julia> cd("testingdir") - -julia> pwd() -"/home/JuliaUser/testingdir" - -julia> mkpath("my/test/dir") +julia> mkpath("my/test/dir") # creates three directories "my/test/dir" julia> readdir() @@ -216,6 +223,13 @@ julia> readdir() julia> readdir("test") 1-element Array{String,1}: "dir" + +julia> mkpath("intermediate_dir/actually_a_directory.txt") # creates two directories +"intermediate_dir/actually_a_directory.txt" + +julia> isdir("intermediate_dir/actually_a_directory.txt") +true + ``` """ function mkpath(path::AbstractString; mode::Integer = 0o777) @@ -251,7 +265,7 @@ julia> rm("my", recursive=true) julia> rm("this_file_does_not_exist", force=true) julia> rm("this_file_does_not_exist") -ERROR: IOError: unlink: no such file or directory (ENOENT) +ERROR: IOError: unlink("this_file_does_not_exist"): no such file or directory (ENOENT) Stacktrace: [...] ``` @@ -261,7 +275,8 @@ function rm(path::AbstractString; force::Bool=false, recursive::Bool=false) try @static if Sys.iswindows() # is writable on windows actually means "is deletable" - if (filemode(lstat(path)) & 0o222) == 0 + st = lstat(path) + if ispath(st) && (filemode(st) & 0o222) == 0 chmod(path, 0o777) end end @@ -274,16 +289,25 @@ function rm(path::AbstractString; force::Bool=false, recursive::Bool=false) end else if recursive - for p in readdir(path) - rm(joinpath(path, p), force=force, recursive=true) + try + for p in readdir(path) + rm(joinpath(path, p), force=force, recursive=true) + end + catch err + if !(force && isa(err, IOError) && err.code==Base.UV_EACCES) + rethrow(err) + end end end - @static if Sys.iswindows() - ret = ccall(:_wrmdir, Int32, (Cwstring,), path) - else - ret = ccall(:rmdir, Int32, (Cstring,), path) + req = Libc.malloc(_sizeof_uv_fs) + try + ret = ccall(:uv_fs_rmdir, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Ptr{Cvoid}), C_NULL, req, path, C_NULL) + uv_fs_req_cleanup(req) + ret < 0 && uv_error("rm($(repr(path)))", ret) + nothing + finally + Libc.free(req) end - systemerror(:rmdir, ret != 0, extrainfo=path) end end @@ -298,12 +322,12 @@ function checkfor_mv_cp_cptree(src::AbstractString, dst::AbstractString, txt::Ab if Base.samefile(src, dst) abs_src = islink(src) ? abspath(readlink(src)) : abspath(src) abs_dst = islink(dst) ? abspath(readlink(dst)) : abspath(dst) - throw(ArgumentError(string("'src' and 'dst' refer to the same file/dir.", + throw(ArgumentError(string("'src' and 'dst' refer to the same file/dir. ", "This is not supported.\n ", "`src` refers to: $(abs_src)\n ", "`dst` refers to: $(abs_dst)\n"))) end - rm(dst; recursive=true) + rm(dst; recursive=true, force=true) else throw(ArgumentError(string("'$dst' exists. `force=true` ", "is required to remove '$dst' before $(txt)."))) @@ -311,8 +335,8 @@ function checkfor_mv_cp_cptree(src::AbstractString, dst::AbstractString, txt::Ab end end -function cptree(src::AbstractString, dst::AbstractString; force::Bool=false, - follow_symlinks::Bool=false) +function cptree(src::String, dst::String; force::Bool=false, + follow_symlinks::Bool=false) isdir(src) || throw(ArgumentError("'$src' is not a directory. Use `cp(src, dst)`")) checkfor_mv_cp_cptree(src, dst, "copying"; force=force) mkdir(dst) @@ -328,6 +352,8 @@ function cptree(src::AbstractString, dst::AbstractString; force::Bool=false, end end end +cptree(src::AbstractString, dst::AbstractString; kwargs...) = + cptree(String(src)::String, String(dst)::String; kwargs...) """ cp(src::AbstractString, dst::AbstractString; force::Bool=false, follow_symlinks::Bool=false) @@ -339,6 +365,13 @@ If `follow_symlinks=false`, and `src` is a symbolic link, `dst` will be created symbolic link. If `follow_symlinks=true` and `src` is a symbolic link, `dst` will be a copy of the file or directory `src` refers to. Return `dst`. + +!!! note + The `cp` function is different from the `cp` command. The `cp` function always operates on + the assumption that `dst` is a file, while the command does different things depending + on whether `dst` is a directory or a file. + Using `force=true` when `dst` is a directory will result in loss of all the contents present + in the `dst` directory, and `dst` will become a file that has the contents of `src` instead. """ function cp(src::AbstractString, dst::AbstractString; force::Bool=false, follow_symlinks::Bool=false) @@ -396,6 +429,7 @@ end """ touch(path::AbstractString) + touch(fd::File) Update the last-modified timestamp on a file to the current time. @@ -421,19 +455,14 @@ We can see the [`mtime`](@ref) has been modified by `touch`. function touch(path::AbstractString) f = open(path, JL_O_WRONLY | JL_O_CREAT, 0o0666) try - if Sys.isunix() - ret = ccall(:futimes, Cint, (Cint, Ptr{Cvoid}), fd(f), C_NULL) - systemerror(:futimes, ret != 0, extrainfo=path) - else - t = time() - futime(f,t,t) - end + touch(f) finally close(f) end path end + """ tempdir() @@ -453,7 +482,7 @@ function tempdir() elseif rc == Base.UV_ENOBUFS resize!(buf, sz[] - 1) # space for null-terminator implied by StringVector else - uv_error(:tmpdir, rc) + uv_error("tempdir()", rc) end end end @@ -475,7 +504,7 @@ function prepare_for_deletion(path::AbstractString) try chmod(path, filemode(path) | 0o333) catch; end - for (root, dirs, files) in walkdir(path) + for (root, dirs, files) in walkdir(path; onerror=x->()) for dir in dirs dpath = joinpath(root, dir) try chmod(dpath, filemode(dpath) | 0o333) @@ -522,15 +551,52 @@ end const temp_prefix = "jl_" -if Sys.iswindows() +# Use `Libc.rand()` to generate random strings +function _rand_filename(len = 10) + slug = Base.StringVector(len) + chars = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + for i = 1:len + slug[i] = chars[(Libc.rand() % length(chars)) + 1] + end + return String(slug) +end + + +# Obtain a temporary filename. +function tempname(parent::AbstractString=tempdir(); max_tries::Int = 100, cleanup::Bool=true) + isdir(parent) || throw(ArgumentError("$(repr(parent)) is not a directory")) + + prefix = joinpath(parent, temp_prefix) + filename = nothing + for i in 1:max_tries + filename = string(prefix, _rand_filename()) + if ispath(filename) + filename = nothing + else + break + end + end -function _win_tempname(temppath::AbstractString, uunique::UInt32) + if filename === nothing + error("tempname: max_tries exhausted") + end + + cleanup && temp_cleanup_later(filename) + return filename +end + +if Sys.iswindows() +# While this isn't a true analog of `mkstemp`, it _does_ create an +# empty file for us, ensuring that other simultaneous calls to +# `_win_mkstemp()` won't collide, so it's a better name for the +# function than `tempname()`. +function _win_mkstemp(temppath::AbstractString) tempp = cwstring(temppath) temppfx = cwstring(temp_prefix) tname = Vector{UInt16}(undef, 32767) uunique = ccall(:GetTempFileNameW, stdcall, UInt32, (Ptr{UInt16}, Ptr{UInt16}, UInt32, Ptr{UInt16}), - tempp, temppfx, uunique, tname) + tempp, temppfx, UInt32(0), tname) windowserror("GetTempFileName", uunique == 0) lentname = something(findfirst(iszero, tname)) @assert lentname > 0 @@ -539,49 +605,13 @@ function _win_tempname(temppath::AbstractString, uunique::UInt32) end function mktemp(parent::AbstractString=tempdir(); cleanup::Bool=true) - filename = _win_tempname(parent, UInt32(0)) + filename = _win_mkstemp(parent) cleanup && temp_cleanup_later(filename) return (filename, Base.open(filename, "r+")) end -# generate a random string from random bytes -function _rand_string() - nchars = 10 - A = Vector{UInt8}(undef, nchars) - windowserror("SystemFunction036 (RtlGenRandom)", 0 == ccall( - (:SystemFunction036, :Advapi32), stdcall, UInt8, (Ptr{Cvoid}, UInt32), - A, sizeof(A))) - - slug = Base.StringVector(10) - chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - for i = 1:nchars - slug[i] = chars[(A[i] % length(chars)) + 1] - end - return name = String(slug) -end - -function tempname(parent::AbstractString=tempdir(); cleanup::Bool=true) - isdir(parent) || throw(ArgumentError("$(repr(parent)) is not a directory")) - name = _rand_string() - filename = joinpath(parent, temp_prefix * name) - @assert !ispath(filename) - cleanup && temp_cleanup_later(filename) - return filename -end - else # !windows -# Obtain a temporary filename. -function tempname(parent::AbstractString=tempdir(); cleanup::Bool=true) - isdir(parent) || throw(ArgumentError("$(repr(parent)) is not a directory")) - p = ccall(:tempnam, Cstring, (Cstring, Cstring), parent, temp_prefix) - systemerror(:tempnam, p == C_NULL) - s = unsafe_string(p) - Libc.free(p) - cleanup && temp_cleanup_later(s) - return s -end - # Create and return the name of a temporary file along with an IOStream function mktemp(parent::AbstractString=tempdir(); cleanup::Bool=true) b = joinpath(parent, temp_prefix * "XXXXXX") @@ -591,7 +621,6 @@ function mktemp(parent::AbstractString=tempdir(); cleanup::Bool=true) return (b, fdio(p, true)) end - end # os-test @@ -634,6 +663,11 @@ tempname() Return `(path, io)`, where `path` is the path of a new temporary file in `parent` and `io` is an open file object for this path. The `cleanup` option controls whether the temporary file is automatically deleted when the process exits. + +!!! compat "Julia 1.3" + The `cleanup` keyword argument was added in Julia 1.3. Relatedly, starting from 1.3, + Julia will remove the temporary paths created by `mktemp` when the Julia process exits, + unless `cleanup` is explicitly set to `false`. """ mktemp(parent) @@ -645,6 +679,16 @@ constructed from the given prefix and a random suffix, and return its path. Additionally, any trailing `X` characters may be replaced with random characters. If `parent` does not exist, throw an error. The `cleanup` option controls whether the temporary directory is automatically deleted when the process exits. + +!!! compat "Julia 1.2" + The `prefix` keyword argument was added in Julia 1.2. + +!!! compat "Julia 1.3" + The `cleanup` keyword argument was added in Julia 1.3. Relatedly, starting from 1.3, + Julia will remove the temporary paths created by `mktempdir` when the Julia process + exits, unless `cleanup` is explicitly set to `false`. + +See also: [`mktemp`](@ref), [`mkdir`](@ref). """ function mktempdir(parent::AbstractString=tempdir(); prefix::AbstractString=temp_prefix, cleanup::Bool=true) @@ -661,11 +705,11 @@ function mktempdir(parent::AbstractString=tempdir(); (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Ptr{Cvoid}), C_NULL, req, tpath, C_NULL) if ret < 0 - ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req) - uv_error("mktempdir", ret) + uv_fs_req_cleanup(req) + uv_error("mktempdir($(repr(parent)))", ret) end path = unsafe_string(ccall(:jl_uv_fs_t_path, Cstring, (Ptr{Cvoid},), req)) - ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req) + uv_fs_req_cleanup(req) cleanup && temp_cleanup_later(path) return path finally @@ -679,6 +723,8 @@ end Apply the function `f` to the result of [`mktemp(parent)`](@ref) and remove the temporary file upon completion. + +See also: [`mktempdir`](@ref). """ function mktemp(fn::Function, parent::AbstractString=tempdir()) (tmp_path, tmp_io) = mktemp(parent, cleanup=false) @@ -701,6 +747,11 @@ end Apply the function `f` to the result of [`mktempdir(parent; prefix)`](@ref) and remove the temporary directory all of its contents upon completion. + +See also: [`mktemp`](@ref), [`mkdir`](@ref). + +!!! compat "Julia 1.2" + The `prefix` keyword argument was added in Julia 1.2. """ function mktempdir(fn::Function, parent::AbstractString=tempdir(); prefix::AbstractString=temp_prefix) @@ -802,28 +853,31 @@ julia> readdir(abspath("base"), join=true) """ function readdir(dir::AbstractString; join::Bool=false, sort::Bool=true) # Allocate space for uv_fs_t struct - uv_readdir_req = zeros(UInt8, ccall(:jl_sizeof_uv_fs_t, Int32, ())) - - # defined in sys.c, to call uv_fs_readdir, which sets errno on error. - err = ccall(:uv_fs_scandir, Int32, (Ptr{Cvoid}, Ptr{UInt8}, Cstring, Cint, Ptr{Cvoid}), - C_NULL, uv_readdir_req, dir, 0, C_NULL) - err < 0 && throw(_UVError("readdir", err, "with ", repr(dir))) - - # iterate the listing into entries - entries = String[] - ent = Ref{uv_dirent_t}() - while Base.UV_EOF != ccall(:uv_fs_scandir_next, Cint, (Ptr{Cvoid}, Ptr{uv_dirent_t}), uv_readdir_req, ent) - name = unsafe_string(ent[].name) - push!(entries, join ? joinpath(dir, name) : name) - end + req = Libc.malloc(_sizeof_uv_fs) + try + # defined in sys.c, to call uv_fs_readdir, which sets errno on error. + err = ccall(:uv_fs_scandir, Int32, (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Cint, Ptr{Cvoid}), + C_NULL, req, dir, 0, C_NULL) + err < 0 && uv_error("readdir($(repr(dir)))", err) + + # iterate the listing into entries + entries = String[] + ent = Ref{uv_dirent_t}() + while Base.UV_EOF != ccall(:uv_fs_scandir_next, Cint, (Ptr{Cvoid}, Ptr{uv_dirent_t}), req, ent) + name = unsafe_string(ent[].name) + push!(entries, join ? joinpath(dir, name) : name) + end - # Clean up the request string - ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{UInt8},), uv_readdir_req) + # Clean up the request string + uv_fs_req_cleanup(req) - # sort entries unless opted out - sort && sort!(entries) + # sort entries unless opted out + sort && sort!(entries) - return entries + return entries + finally + Libc.free(req) + end end readdir(; join::Bool=false, sort::Bool=true) = readdir(join ? pwd() : ".", join=join, sort=sort) @@ -911,7 +965,7 @@ end function unlink(p::AbstractString) err = ccall(:jl_fs_unlink, Int32, (Cstring,), p) - uv_error("unlink", err) + err < 0 && uv_error("unlink($(repr(p)))", err) nothing end @@ -949,19 +1003,66 @@ function sendfile(src::AbstractString, dst::AbstractString) end if Sys.iswindows() + const UV_FS_SYMLINK_DIR = 0x0001 const UV_FS_SYMLINK_JUNCTION = 0x0002 + const UV__EPERM = -4048 +end + +""" + hardlink(src::AbstractString, dst::AbstractString) + +Creates a hard link to an existing source file `src` with the name `dst`. The +destination, `dst`, must not exist. + +See also: [`symlink`](@ref). + +!!! compat "Julia 1.8" + This method was added in Julia 1.8. +""" +function hardlink(src::AbstractString, dst::AbstractString) + err = ccall(:jl_fs_hardlink, Int32, (Cstring, Cstring), src, dst) + if err < 0 + msg = "hardlink($(repr(src)), $(repr(dst)))" + uv_error(msg, err) + end + return nothing end """ - symlink(target::AbstractString, link::AbstractString) + symlink(target::AbstractString, link::AbstractString; dir_target = false) Creates a symbolic link to `target` with the name `link`. +On Windows, symlinks must be explicitly declared as referring to a directory +or not. If `target` already exists, by default the type of `link` will be auto- +detected, however if `target` does not exist, this function defaults to creating +a file symlink unless `dir_target` is set to `true`. Note that if the user +sets `dir_target` but `target` exists and is a file, a directory symlink will +still be created, but dereferencing the symlink will fail, just as if the user +creates a file symlink (by calling `symlink()` with `dir_target` set to `false` +before the directory is created) and tries to dereference it to a directory. + +Additionally, there are two methods of making a link on Windows; symbolic links +and junction points. Junction points are slightly more efficient, but do not +support relative paths, so if a relative directory symlink is requested (as +denoted by `isabspath(target)` returning `false`) a symlink will be used, else +a junction point will be used. Best practice for creating symlinks on Windows +is to create them only after the files/directories they reference are already +created. + +See also: [`hardlink`](@ref). + !!! note This function raises an error under operating systems that do not support soft symbolic links, such as Windows XP. + +!!! compat "Julia 1.6" + The `dir_target` keyword argument was added in Julia 1.6. Prior to this, + symlinks to nonexistant paths on windows would always be file symlinks, and + relative symlinks to directories were not supported. """ -function symlink(p::AbstractString, np::AbstractString) +function symlink(target::AbstractString, link::AbstractString; + dir_target::Bool = false) @static if Sys.iswindows() if Sys.windows_version() < Sys.WINDOWS_VISTA_VER error("Windows XP does not support soft symlinks") @@ -969,18 +1070,38 @@ function symlink(p::AbstractString, np::AbstractString) end flags = 0 @static if Sys.iswindows() - if isdir(p) - flags |= UV_FS_SYMLINK_JUNCTION - p = abspath(p) + # If we're going to create a directory link, we need to know beforehand. + # First, if `target` is not an absolute path, let's immediately resolve + # it so that we can peek and see if it's a directory. + resolved_target = target + if !isabspath(target) + resolved_target = joinpath(dirname(link), target) + end + + # If it is a directory (or `dir_target` is set), we'll need to add one + # of `UV_FS_SYMLINK_{DIR,JUNCTION}` to the flags, depending on whether + # `target` is an absolute path or not. + if (ispath(resolved_target) && isdir(resolved_target)) || dir_target + if isabspath(target) + flags |= UV_FS_SYMLINK_JUNCTION + else + flags |= UV_FS_SYMLINK_DIR + end end end - err = ccall(:jl_fs_symlink, Int32, (Cstring, Cstring, Cint), p, np, flags) - @static if Sys.iswindows() - if err < 0 && !isdir(p) - @warn "On Windows, creating file symlinks requires Administrator privileges" maxlog=1 _group=:file + err = ccall(:jl_fs_symlink, Int32, (Cstring, Cstring, Cint), target, link, flags) + if err < 0 + msg = "symlink($(repr(target)), $(repr(link)))" + @static if Sys.iswindows() + # creating file/directory symlinks requires Administrator privileges + # while junction points apparently do not + if flags & UV_FS_SYMLINK_JUNCTION == 0 && err == UV__EPERM + msg = "On Windows, creating symlinks requires Administrator privileges.\n$msg" + end end + uv_error(msg, err) end - uv_error("symlink",err) + return nothing end """ @@ -995,12 +1116,12 @@ function readlink(path::AbstractString) (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Ptr{Cvoid}), C_NULL, req, path, C_NULL) if ret < 0 - ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req) - uv_error("readlink", ret) + uv_fs_req_cleanup(req) + uv_error("readlink($(repr(path)))", ret) @assert false end tgt = unsafe_string(ccall(:jl_uv_fs_t_ptr, Cstring, (Ptr{Cvoid},), req)) - ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req) + uv_fs_req_cleanup(req) return tgt finally Libc.free(req) @@ -1022,7 +1143,7 @@ Return `path`. """ function chmod(path::AbstractString, mode::Integer; recursive::Bool=false) err = ccall(:jl_fs_chmod, Int32, (Cstring, Cint), path, mode) - uv_error("chmod", err) + err < 0 && uv_error("chmod($(repr(path)), 0o$(string(mode, base=8)))", err) if recursive && isdir(path) for p in readdir(path) if !islink(joinpath(path, p)) @@ -1042,6 +1163,60 @@ Return `path`. """ function chown(path::AbstractString, owner::Integer, group::Integer=-1) err = ccall(:jl_fs_chown, Int32, (Cstring, Cint, Cint), path, owner, group) - uv_error("chown",err) + err < 0 && uv_error("chown($(repr(path)), $owner, $group)", err) path end + + +# - http://docs.libuv.org/en/v1.x/fs.html#c.uv_fs_statfs (libuv function docs) +# - http://docs.libuv.org/en/v1.x/fs.html#c.uv_statfs_t (libuv docs of the returned struct) +""" + DiskStat + +Stores information about the disk in bytes. Populate by calling `diskstat`. +""" +struct DiskStat + ftype::UInt64 + bsize::UInt64 + blocks::UInt64 + bfree::UInt64 + bavail::UInt64 + files::UInt64 + ffree::UInt64 + fspare::NTuple{4, UInt64} # reserved +end + +function Base.getproperty(stats::DiskStat, field::Symbol) + total = Int64(getfield(stats, :bsize) * getfield(stats, :blocks)) + available = Int64(getfield(stats, :bsize) * getfield(stats, :bavail)) + field === :total && return total + field === :available && return available + field === :used && return total - available + return getfield(stats, field) +end + +@eval Base.propertynames(stats::DiskStat) = + $((fieldnames(DiskStat)[1:end-1]..., :available, :total, :used)) + +Base.show(io::IO, x::DiskStat) = + print(io, "DiskStat(total=$(x.total), used=$(x.used), available=$(x.available))") + +""" + diskstat(path=pwd()) + +Returns statistics in bytes about the disk that contains the file or directory pointed at by +`path`. If no argument is passed, statistics about the disk that contains the current +working directory are returned. + +!!! compat "Julia 1.8" + This method was added in Julia 1.8. +""" +function diskstat(path::AbstractString=pwd()) + req = zeros(UInt8, _sizeof_uv_fs) + err = ccall(:uv_fs_statfs, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Ptr{Cvoid}), + C_NULL, req, path, C_NULL) + err < 0 && uv_error("diskstat($(repr(path)))", err) + statfs_ptr = ccall(:jl_uv_fs_t_ptr, Ptr{Nothing}, (Ptr{Cvoid},), req) + + return unsafe_load(reinterpret(Ptr{DiskStat}, statfs_ptr)) +end diff --git a/base/filesystem.jl b/base/filesystem.jl index fcc3b3427a7e0c..f338f8733523f4 100644 --- a/base/filesystem.jl +++ b/base/filesystem.jl @@ -4,18 +4,32 @@ module Filesystem -const S_IRUSR = 0o400 -const S_IWUSR = 0o200 -const S_IXUSR = 0o100 -const S_IRWXU = 0o700 -const S_IRGRP = 0o040 -const S_IWGRP = 0o020 -const S_IXGRP = 0o010 -const S_IRWXG = 0o070 -const S_IROTH = 0o004 -const S_IWOTH = 0o002 -const S_IXOTH = 0o001 -const S_IRWXO = 0o007 +const S_IFDIR = 0o040000 # directory +const S_IFCHR = 0o020000 # character device +const S_IFBLK = 0o060000 # block device +const S_IFREG = 0o100000 # regular file +const S_IFIFO = 0o010000 # fifo (named pipe) +const S_IFLNK = 0o120000 # symbolic link +const S_IFSOCK = 0o140000 # socket file +const S_IFMT = 0o170000 + +const S_ISUID = 0o4000 # set UID bit +const S_ISGID = 0o2000 # set GID bit +const S_ENFMT = S_ISGID # file locking enforcement +const S_ISVTX = 0o1000 # sticky bit + +const S_IRUSR = 0o0400 # read by owner +const S_IWUSR = 0o0200 # write by owner +const S_IXUSR = 0o0100 # execute by owner +const S_IRWXU = 0o0700 # mask for owner permissions +const S_IRGRP = 0o0040 # read by group +const S_IWGRP = 0o0020 # write by group +const S_IXGRP = 0o0010 # execute by group +const S_IRWXG = 0o0070 # mask for group permissions +const S_IROTH = 0o0004 # read by other +const S_IWOTH = 0o0002 # write by other +const S_IXOTH = 0o0001 # execute by other +const S_IRWXO = 0o0007 # mask for other permissions export File, StatStruct, @@ -42,7 +56,7 @@ import .Base: IOError, _UVError, _sizeof_uv_fs, check_open, close, eof, eventloop, fd, isopen, bytesavailable, position, read, read!, readavailable, seek, seekend, show, skip, stat, unsafe_read, unsafe_write, write, transcode, uv_error, - rawhandle, OS_HANDLE, INVALID_OS_HANDLE, windowserror, filesize + setup_stdio, rawhandle, OS_HANDLE, INVALID_OS_HANDLE, windowserror, filesize import .Base.RefValue @@ -54,6 +68,9 @@ end # On Windows we use the MAX_PATH = 260 value on Win32. const AVG_PATH = Sys.iswindows() ? 260 : 512 +# helper function to clean up libuv request +uv_fs_req_cleanup(req) = ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req) + include("path.jl") include("stat.jl") include("file.jl") @@ -73,6 +90,7 @@ if OS_HANDLE !== RawFD end rawhandle(file::File) = file.handle +setup_stdio(file::File, ::Bool) = (file, false) # Filesystem.open, not Base.open function open(path::AbstractString, flags::Integer, mode::Integer=0) @@ -83,8 +101,8 @@ function open(path::AbstractString, flags::Integer, mode::Integer=0) (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Int32, Int32, Ptr{Cvoid}), C_NULL, req, path, flags, mode, C_NULL) handle = ccall(:uv_fs_get_result, Cssize_t, (Ptr{Cvoid},), req) - ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req) - uv_error("open", ret) + uv_fs_req_cleanup(req) + ret < 0 && uv_error("open($(repr(path)), $flags, $mode)", ret) finally # conversion to Cstring could cause an exception Libc.free(req) end @@ -245,4 +263,16 @@ end fd(f::File) = f.handle stat(f::File) = stat(f.handle) +function touch(f::File) + @static if Sys.isunix() + ret = ccall(:futimes, Cint, (Cint, Ptr{Cvoid}), fd(f), C_NULL) + systemerror(:futimes, ret != 0) + else + t = time() + futime(f, t, t) + end + f +end + + end diff --git a/base/float.jl b/base/float.jl index 0857d172eaff0a..60850b7e02f64a 100644 --- a/base/float.jl +++ b/base/float.jl @@ -36,6 +36,20 @@ const Inf = Inf64 Inf, Inf64 Positive infinity of type [`Float64`](@ref). + +See also: [`isfinite`](@ref), [`typemax`](@ref), [`NaN`](@ref), [`Inf32`](@ref). + +# Examples +```jldoctest +julia> π/0 +Inf + +julia> +1.0 / -0.0 +-Inf + +julia> ℯ^-Inf +0.0 +``` """ Inf, Inf64 @@ -44,17 +58,89 @@ const NaN = NaN64 NaN, NaN64 A not-a-number value of type [`Float64`](@ref). + +See also: [`isnan`](@ref), [`missing`](@ref), [`NaN32`](@ref), [`Inf`](@ref). + +# Examples +```jldoctest +julia> 0/0 +NaN + +julia> Inf - Inf +NaN + +julia> NaN == NaN, isequal(NaN, NaN), NaN === NaN +(false, true, true) +``` """ NaN, NaN64 +# bit patterns +reinterpret(::Type{Unsigned}, x::Float64) = reinterpret(UInt64, x) +reinterpret(::Type{Unsigned}, x::Float32) = reinterpret(UInt32, x) +reinterpret(::Type{Unsigned}, x::Float16) = reinterpret(UInt16, x) +reinterpret(::Type{Signed}, x::Float64) = reinterpret(Int64, x) +reinterpret(::Type{Signed}, x::Float32) = reinterpret(Int32, x) +reinterpret(::Type{Signed}, x::Float16) = reinterpret(Int16, x) + +sign_mask(::Type{Float64}) = 0x8000_0000_0000_0000 +exponent_mask(::Type{Float64}) = 0x7ff0_0000_0000_0000 +exponent_one(::Type{Float64}) = 0x3ff0_0000_0000_0000 +exponent_half(::Type{Float64}) = 0x3fe0_0000_0000_0000 +significand_mask(::Type{Float64}) = 0x000f_ffff_ffff_ffff + +sign_mask(::Type{Float32}) = 0x8000_0000 +exponent_mask(::Type{Float32}) = 0x7f80_0000 +exponent_one(::Type{Float32}) = 0x3f80_0000 +exponent_half(::Type{Float32}) = 0x3f00_0000 +significand_mask(::Type{Float32}) = 0x007f_ffff + +sign_mask(::Type{Float16}) = 0x8000 +exponent_mask(::Type{Float16}) = 0x7c00 +exponent_one(::Type{Float16}) = 0x3c00 +exponent_half(::Type{Float16}) = 0x3800 +significand_mask(::Type{Float16}) = 0x03ff + +for T in (Float16, Float32, Float64) + @eval significand_bits(::Type{$T}) = $(trailing_ones(significand_mask(T))) + @eval exponent_bits(::Type{$T}) = $(sizeof(T)*8 - significand_bits(T) - 1) + @eval exponent_bias(::Type{$T}) = $(Int(exponent_one(T) >> significand_bits(T))) + # maximum float exponent + @eval exponent_max(::Type{$T}) = $(Int(exponent_mask(T) >> significand_bits(T)) - exponent_bias(T) - 1) + # maximum float exponent without bias + @eval exponent_raw_max(::Type{$T}) = $(Int(exponent_mask(T) >> significand_bits(T))) +end + +""" + exponent_max(T) + +Maximum [`exponent`](@ref) value for a floating point number of type `T`. + +# Examples +```jldoctest +julia> Base.exponent_max(Float64) +1023 +``` + +Note, `exponent_max(T) + 1` is a possible value of the exponent field +with bias, which might be used as sentinel value for `Inf` or `NaN`. +""" +function exponent_max end + +""" + exponent_raw_max(T) + +Maximum value of the [`exponent`](@ref) field for a floating point number of type `T` without bias, +i.e. the maximum integer value representable by [`exponent_bits(T)`](@ref) bits. +""" +function exponent_raw_max end + ## conversions to floating-point ## + +# TODO: deprecate in 2.0 Float16(x::Integer) = convert(Float16, convert(Float32, x)::Float32) -for t in (Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128) - @eval promote_rule(::Type{Float16}, ::Type{$t}) = Float16 -end -promote_rule(::Type{Float16}, ::Type{Bool}) = Float16 -for t1 in (Float32, Float64) +for t1 in (Float16, Float32, Float64) for st in (Int8, Int16, Int32, Int64) @eval begin (::Type{$t1})(x::($st)) = sitofp($t1, x) @@ -68,7 +154,6 @@ for t1 in (Float32, Float64) end end end -(::Type{T})(x::Float16) where {T<:Integer} = T(Float32(x)) Bool(x::Real) = x==0 ? false : x==1 ? true : throw(InexactError(:Bool, Bool, x)) @@ -76,6 +161,8 @@ promote_rule(::Type{Float64}, ::Type{UInt128}) = Float64 promote_rule(::Type{Float64}, ::Type{Int128}) = Float64 promote_rule(::Type{Float32}, ::Type{UInt128}) = Float32 promote_rule(::Type{Float32}, ::Type{Int128}) = Float32 +promote_rule(::Type{Float16}, ::Type{UInt128}) = Float16 +promote_rule(::Type{Float16}, ::Type{Int128}) = Float16 function Float64(x::UInt128) x == 0 && return 0.0 @@ -137,123 +224,17 @@ function Float32(x::Int128) reinterpret(Float32, s | d + y) end -# Float32 -> Float16 algorithm from: -# "Fast Half Float Conversion" by Jeroen van der Zijp -# ftp://ftp.fox-toolkit.org/pub/fasthalffloatconversion.pdf -# -# With adjustments for round-to-nearest, ties to even. -# -let _basetable = Vector{UInt16}(undef, 512), - _shifttable = Vector{UInt8}(undef, 512) - for i = 0:255 - e = i - 127 - if e < -25 # Very small numbers map to zero - _basetable[i|0x000+1] = 0x0000 - _basetable[i|0x100+1] = 0x8000 - _shifttable[i|0x000+1] = 25 - _shifttable[i|0x100+1] = 25 - elseif e < -14 # Small numbers map to denorms - _basetable[i|0x000+1] = 0x0000 - _basetable[i|0x100+1] = 0x8000 - _shifttable[i|0x000+1] = -e-1 - _shifttable[i|0x100+1] = -e-1 - elseif e <= 15 # Normal numbers just lose precision - _basetable[i|0x000+1] = ((e+15)<<10) - _basetable[i|0x100+1] = ((e+15)<<10) | 0x8000 - _shifttable[i|0x000+1] = 13 - _shifttable[i|0x100+1] = 13 - elseif e < 128 # Large numbers map to Infinity - _basetable[i|0x000+1] = 0x7C00 - _basetable[i|0x100+1] = 0xFC00 - _shifttable[i|0x000+1] = 24 - _shifttable[i|0x100+1] = 24 - else # Infinity and NaN's stay Infinity and NaN's - _basetable[i|0x000+1] = 0x7C00 - _basetable[i|0x100+1] = 0xFC00 - _shifttable[i|0x000+1] = 13 - _shifttable[i|0x100+1] = 13 - end - end - global const shifttable = (_shifttable...,) - global const basetable = (_basetable...,) -end +# TODO: optimize +Float16(x::UInt128) = convert(Float16, Float32(x)) +Float16(x::Int128) = convert(Float16, Float32(x)) -function Float16(val::Float32) - f = reinterpret(UInt32, val) - if isnan(val) - t = 0x8000 ⊻ (0x8000 & ((f >> 0x10) % UInt16)) - return reinterpret(Float16, t ⊻ ((f >> 0xd) % UInt16)) - end - i = ((f & ~significand_mask(Float32)) >> significand_bits(Float32)) + 1 - @inbounds sh = shifttable[i] - f &= significand_mask(Float32) - # If `val` is subnormal, the tables are set up to force the - # result to 0, so the significand has an implicit `1` in the - # cases we care about. - f |= significand_mask(Float32) + 0x1 - @inbounds h = (basetable[i] + (f >> sh) & significand_mask(Float16)) % UInt16 - # round - # NOTE: we maybe should ignore NaNs here, but the payload is - # getting truncated anyway so "rounding" it might not matter - nextbit = (f >> (sh-1)) & 1 - if nextbit != 0 && (h & 0x7C00) != 0x7C00 - # Round halfway to even or check lower bits - if h&1 == 1 || (f & ((1<<(sh-1))-1)) != 0 - h += UInt16(1) - end - end - reinterpret(Float16, h) -end - -function Float32(val::Float16) - local ival::UInt32 = reinterpret(UInt16, val) - local sign::UInt32 = (ival & 0x8000) >> 15 - local exp::UInt32 = (ival & 0x7c00) >> 10 - local sig::UInt32 = (ival & 0x3ff) >> 0 - local ret::UInt32 - - if exp == 0 - if sig == 0 - sign = sign << 31 - ret = sign | exp | sig - else - n_bit = 1 - bit = 0x0200 - while (bit & sig) == 0 - n_bit = n_bit + 1 - bit = bit >> 1 - end - sign = sign << 31 - exp = ((-14 - n_bit + 127) << 23) % UInt32 - sig = ((sig & (~bit)) << n_bit) << (23 - 10) - ret = sign | exp | sig - end - elseif exp == 0x1f - if sig == 0 # Inf - if sign == 0 - ret = 0x7f800000 - else - ret = 0xff800000 - end - else # NaN - ret = 0x7fc00000 | (sign<<31) | (sig<<(23-10)) - end - else - sign = sign << 31 - exp = ((exp - 15 + 127) << 23) % UInt32 - sig = sig << (23 - 10) - ret = sign | exp | sig - end - return reinterpret(Float32, ret) -end - -#convert(::Type{Float16}, x::Float32) = fptrunc(Float16, x) +Float16(x::Float32) = fptrunc(Float16, x) +Float16(x::Float64) = fptrunc(Float16, x) Float32(x::Float64) = fptrunc(Float32, x) -Float16(x::Float64) = Float16(Float32(x)) -#convert(::Type{Float32}, x::Float16) = fpext(Float32, x) +Float32(x::Float16) = fpext(Float32, x) Float64(x::Float32) = fpext(Float64, x) -Float64(x::Float16) = Float64(Float32(x)) +Float64(x::Float16) = fpext(Float64, x) AbstractFloat(x::Bool) = Float64(x) AbstractFloat(x::Int8) = Float64(x) @@ -273,6 +254,17 @@ Bool(x::Float16) = x==0 ? false : x==1 ? true : throw(InexactError(:Bool, Bool, float(x) Convert a number or array to a floating point data type. + +See also: [`complex`](@ref), [`oftype`](@ref), [`convert`](@ref). + +# Examples +```jldoctest +julia> float(1:1000) +1.0:1.0:1000.0 + +julia> float(typemax(Int32)) +2.147483647e9 +``` """ float(x) = AbstractFloat(x) @@ -298,23 +290,29 @@ float(::Type{T}) where {T<:AbstractFloat} = T unsafe_trunc(T, x) Return the nearest integral value of type `T` whose absolute value is -less than or equal to `x`. If the value is not representable by `T`, an arbitrary value will -be returned. +less than or equal to the absolute value of `x`. If the value is not representable by `T`, +an arbitrary value will be returned. +See also [`trunc`](@ref). + +# Examples +```jldoctest +julia> unsafe_trunc(Int, -2.2) +-2 + +julia> unsafe_trunc(Int, NaN) +-9223372036854775808 +``` """ function unsafe_trunc end for Ti in (Int8, Int16, Int32, Int64) @eval begin - unsafe_trunc(::Type{$Ti}, x::Float16) = unsafe_trunc($Ti, Float32(x)) - unsafe_trunc(::Type{$Ti}, x::Float32) = fptosi($Ti, x) - unsafe_trunc(::Type{$Ti}, x::Float64) = fptosi($Ti, x) + unsafe_trunc(::Type{$Ti}, x::IEEEFloat) = fptosi($Ti, x) end end for Ti in (UInt8, UInt16, UInt32, UInt64) @eval begin - unsafe_trunc(::Type{$Ti}, x::Float16) = unsafe_trunc($Ti, Float32(x)) - unsafe_trunc(::Type{$Ti}, x::Float32) = fptoui($Ti, x) - unsafe_trunc(::Type{$Ti}, x::Float64) = fptoui($Ti, x) + unsafe_trunc(::Type{$Ti}, x::IEEEFloat) = fptoui($Ti, x) end end @@ -351,35 +349,25 @@ unsafe_trunc(::Type{Int128}, x::Float16) = unsafe_trunc(Int128, Float32(x)) # matches convert methods # also determines floor, ceil, round -trunc(::Type{Signed}, x::Float32) = trunc(Int,x) -trunc(::Type{Signed}, x::Float64) = trunc(Int,x) -trunc(::Type{Unsigned}, x::Float32) = trunc(UInt,x) -trunc(::Type{Unsigned}, x::Float64) = trunc(UInt,x) -trunc(::Type{Integer}, x::Float32) = trunc(Int,x) -trunc(::Type{Integer}, x::Float64) = trunc(Int,x) -trunc(::Type{T}, x::Float16) where {T<:Integer} = trunc(T, Float32(x)) +trunc(::Type{Signed}, x::IEEEFloat) = trunc(Int,x) +trunc(::Type{Unsigned}, x::IEEEFloat) = trunc(UInt,x) +trunc(::Type{Integer}, x::IEEEFloat) = trunc(Int,x) # fallbacks floor(::Type{T}, x::AbstractFloat) where {T<:Integer} = trunc(T,round(x, RoundDown)) -floor(::Type{T}, x::Float16) where {T<:Integer} = floor(T, Float32(x)) ceil(::Type{T}, x::AbstractFloat) where {T<:Integer} = trunc(T,round(x, RoundUp)) -ceil(::Type{T}, x::Float16) where {T<:Integer} = ceil(T, Float32(x)) round(::Type{T}, x::AbstractFloat) where {T<:Integer} = trunc(T,round(x, RoundNearest)) -round(::Type{T}, x::Float16) where {T<:Integer} = round(T, Float32(x)) - -round(x::Float64, r::RoundingMode{:ToZero}) = trunc_llvm(x) -round(x::Float32, r::RoundingMode{:ToZero}) = trunc_llvm(x) -round(x::Float64, r::RoundingMode{:Down}) = floor_llvm(x) -round(x::Float32, r::RoundingMode{:Down}) = floor_llvm(x) -round(x::Float64, r::RoundingMode{:Up}) = ceil_llvm(x) -round(x::Float32, r::RoundingMode{:Up}) = ceil_llvm(x) -round(x::Float64, r::RoundingMode{:Nearest}) = rint_llvm(x) -round(x::Float32, r::RoundingMode{:Nearest}) = rint_llvm(x) - -round(x::Float16, r::RoundingMode{:ToZero}) = Float16(round(Float32(x), r)) -round(x::Float16, r::RoundingMode{:Down}) = Float16(round(Float32(x), r)) -round(x::Float16, r::RoundingMode{:Up}) = Float16(round(Float32(x), r)) -round(x::Float16, r::RoundingMode{:Nearest}) = Float16(round(Float32(x), r)) + +# Bool +trunc(::Type{Bool}, x::AbstractFloat) = (-1 < x < 2) ? 1 <= x : throw(InexactError(:trunc, Bool, x)) +floor(::Type{Bool}, x::AbstractFloat) = (0 <= x < 2) ? 1 <= x : throw(InexactError(:floor, Bool, x)) +ceil(::Type{Bool}, x::AbstractFloat) = (-1 < x <= 1) ? 0 < x : throw(InexactError(:ceil, Bool, x)) +round(::Type{Bool}, x::AbstractFloat) = (-0.5 <= x < 1.5) ? 0.5 < x : throw(InexactError(:round, Bool, x)) + +round(x::IEEEFloat, r::RoundingMode{:ToZero}) = trunc_llvm(x) +round(x::IEEEFloat, r::RoundingMode{:Down}) = floor_llvm(x) +round(x::IEEEFloat, r::RoundingMode{:Up}) = ceil_llvm(x) +round(x::IEEEFloat, r::RoundingMode{:Nearest}) = rint_llvm(x) ## floating point promotions ## promote_rule(::Type{Float32}, ::Type{Float16}) = Float32 @@ -390,40 +378,20 @@ widen(::Type{Float16}) = Float32 widen(::Type{Float32}) = Float64 ## floating point arithmetic ## --(x::Float64) = neg_float(x) --(x::Float32) = neg_float(x) --(x::Float16) = reinterpret(Float16, reinterpret(UInt16, x) ⊻ 0x8000) +-(x::IEEEFloat) = neg_float(x) -for op in (:+, :-, :*, :/, :\, :^) - @eval ($op)(a::Float16, b::Float16) = Float16(($op)(Float32(a), Float32(b))) -end -+(x::Float32, y::Float32) = add_float(x, y) -+(x::Float64, y::Float64) = add_float(x, y) --(x::Float32, y::Float32) = sub_float(x, y) --(x::Float64, y::Float64) = sub_float(x, y) -*(x::Float32, y::Float32) = mul_float(x, y) -*(x::Float64, y::Float64) = mul_float(x, y) -/(x::Float32, y::Float32) = div_float(x, y) -/(x::Float64, y::Float64) = div_float(x, y) - -muladd(x::Float32, y::Float32, z::Float32) = muladd_float(x, y, z) -muladd(x::Float64, y::Float64, z::Float64) = muladd_float(x, y, z) -function muladd(a::Float16, b::Float16, c::Float16) - Float16(muladd(Float32(a), Float32(b), Float32(c))) -end ++(x::T, y::T) where {T<:IEEEFloat} = add_float(x, y) +-(x::T, y::T) where {T<:IEEEFloat} = sub_float(x, y) +*(x::T, y::T) where {T<:IEEEFloat} = mul_float(x, y) +/(x::T, y::T) where {T<:IEEEFloat} = div_float(x, y) + +muladd(x::T, y::T, z::T) where {T<:IEEEFloat} = muladd_float(x, y, z) # TODO: faster floating point div? # TODO: faster floating point fld? # TODO: faster floating point mod? -for func in (:div,:fld,:cld,:rem,:mod) - @eval begin - $func(a::Float16,b::Float16) = Float16($func(Float32(a),Float32(b))) - end -end - -rem(x::Float32, y::Float32) = rem_float(x, y) -rem(x::Float64, y::Float64) = rem_float(x, y) +rem(x::T, y::T) where {T<:IEEEFloat} = rem_float(x, y) cld(x::T, y::T) where {T<:AbstractFloat} = -fld(-x,y) @@ -439,32 +407,24 @@ function mod(x::T, y::T) where T<:AbstractFloat end ## floating point comparisons ## -function ==(x::Float16, y::Float16) - ix = reinterpret(UInt16,x) - iy = reinterpret(UInt16,y) - if (ix|iy)&0x7fff > 0x7c00 #isnan(x) || isnan(y) - return false - end - if (ix|iy)&0x7fff == 0x0000 - return true - end - return ix == iy +==(x::T, y::T) where {T<:IEEEFloat} = eq_float(x, y) +!=(x::T, y::T) where {T<:IEEEFloat} = ne_float(x, y) +<( x::T, y::T) where {T<:IEEEFloat} = lt_float(x, y) +<=(x::T, y::T) where {T<:IEEEFloat} = le_float(x, y) + +isequal(x::T, y::T) where {T<:IEEEFloat} = fpiseq(x, y) + +# interpret as sign-magnitude integer +@inline function _fpint(x) + IntT = inttype(typeof(x)) + ix = reinterpret(IntT, x) + return ifelse(ix < zero(IntT), ix ⊻ typemax(IntT), ix) end -==(x::Float32, y::Float32) = eq_float(x, y) -==(x::Float64, y::Float64) = eq_float(x, y) -!=(x::Float32, y::Float32) = ne_float(x, y) -!=(x::Float64, y::Float64) = ne_float(x, y) -<( x::Float32, y::Float32) = lt_float(x, y) -<( x::Float64, y::Float64) = lt_float(x, y) -<=(x::Float32, y::Float32) = le_float(x, y) -<=(x::Float64, y::Float64) = le_float(x, y) - -isequal(x::Float32, y::Float32) = fpiseq(x, y) -isequal(x::Float64, y::Float64) = fpiseq(x, y) -isless( x::Float32, y::Float32) = fpislt(x, y) -isless( x::Float64, y::Float64) = fpislt(x, y) -for op in (:<, :<=, :isless) - @eval ($op)(a::Float16, b::Float16) = ($op)(Float32(a), Float32(b)) + +@inline function isless(a::T, b::T) where T<:IEEEFloat + (isnan(a) || isnan(b)) && return !isnan(a) + + return _fpint(a) < _fpint(b) end # Exact Float (Tf) vs Integer (Ti) comparisons @@ -523,22 +483,20 @@ for op in (:(==), :<, :<=) end -abs(x::Float16) = reinterpret(Float16, reinterpret(UInt16, x) & 0x7fff) -abs(x::Float32) = abs_float(x) -abs(x::Float64) = abs_float(x) +abs(x::IEEEFloat) = abs_float(x) """ isnan(f) -> Bool Test whether a number value is a NaN, an indeterminate value which is neither an infinity nor a finite number ("not a number"). + +See also: [`iszero`](@ref), [`isone`](@ref), [`isinf`](@ref), [`ismissing`](@ref). """ isnan(x::AbstractFloat) = (x != x)::Bool -isnan(x::Float16) = reinterpret(UInt16,x)&0x7fff > 0x7c00 -isnan(x::Real) = false +isnan(x::Number) = false isfinite(x::AbstractFloat) = x - x == 0 -isfinite(x::Float16) = reinterpret(UInt16,x)&0x7c00 != 0x7c00 isfinite(x::Real) = decompose(x)[3] != 0 isfinite(x::Integer) = true @@ -546,33 +504,165 @@ isfinite(x::Integer) = true isinf(f) -> Bool Test whether a number is infinite. + +See also: [`Inf`](@ref), [`iszero`](@ref), [`isfinite`](@ref), [`isnan`](@ref). """ isinf(x::Real) = !isnan(x) & !isfinite(x) -## hashing small, built-in numeric types ## +const hx_NaN = hash_uint64(reinterpret(UInt64, NaN)) +let Tf = Float64, Tu = UInt64, Ti = Int64 + @eval function hash(x::$Tf, h::UInt) + # see comments on trunc and hash(Real, UInt) + if $(Tf(typemin(Ti))) <= x < $(Tf(typemax(Ti))) + xi = fptosi($Ti, x) + if isequal(xi, x) + return hash(xi, h) + end + elseif $(Tf(typemin(Tu))) <= x < $(Tf(typemax(Tu))) + xu = fptoui($Tu, x) + if isequal(xu, x) + return hash(xu, h) + end + elseif isnan(x) + return hx_NaN ⊻ h # NaN does not have a stable bit pattern + end + return hash_uint64(bitcast(UInt64, x)) - 3h + end +end + +hash(x::Float32, h::UInt) = hash(Float64(x), h) +hash(x::Float16, h::UInt) = hash(Float64(x), h) -hx(a::UInt64, b::Float64, h::UInt) = hash_uint64((3a + reinterpret(UInt64,b)) - h) -const hx_NaN = hx(UInt64(0), NaN, UInt(0 )) +## generic hashing for rational values ## -hash(x::UInt64, h::UInt) = hx(x, Float64(x), h) -hash(x::Int64, h::UInt) = hx(reinterpret(UInt64, abs(x)), Float64(x), h) -hash(x::Float64, h::UInt) = isnan(x) ? (hx_NaN ⊻ h) : hx(fptoui(UInt64, abs(x)), x, h) +function hash(x::Real, h::UInt) + # decompose x as num*2^pow/den + num, pow, den = decompose(x) + + # handle special values + num == 0 && den == 0 && return hash(NaN, h) + num == 0 && return hash(ifelse(den > 0, 0.0, -0.0), h) + den == 0 && return hash(ifelse(num > 0, Inf, -Inf), h) + + # normalize decomposition + if den < 0 + num = -num + den = -den + end + z = trailing_zeros(num) + if z != 0 + num >>= z + pow += z + end + z = trailing_zeros(den) + if z != 0 + den >>= z + pow -= z + end + + # handle values representable as Int64, UInt64, Float64 + if den == 1 + left = ndigits0z(num,2) + pow + right = trailing_zeros(num) + pow + if -1074 <= right + if 0 <= right && left <= 64 + left <= 63 && return hash(Int64(num) << Int(pow), h) + signbit(num) == signbit(den) && return hash(UInt64(num) << Int(pow), h) + end # typemin(Int64) handled by Float64 case + left <= 1024 && left - right <= 53 && return hash(ldexp(Float64(num),pow), h) + end + end + + # handle generic rational values + h = hash_integer(den, h) + h = hash_integer(pow, h) + h = hash_integer(num, h) + return h +end + +#= +`decompose(x)`: non-canonical decomposition of rational values as `num*2^pow/den`. + +The decompose function is the point where rational-valued numeric types that support +hashing hook into the hashing protocol. `decompose(x)` should return three integer +values `num, pow, den`, such that the value of `x` is mathematically equal to + + num*2^pow/den + +The decomposition need not be canonical in the sense that it just needs to be *some* +way to express `x` in this form, not any particular way – with the restriction that +`num` and `den` may not share any odd common factors. They may, however, have powers +of two in common – the generic hashing code will normalize those as necessary. + +Special values: + + - `x` is zero: `num` should be zero and `den` should have the same sign as `x` + - `x` is infinite: `den` should be zero and `num` should have the same sign as `x` + - `x` is not a number: `num` and `den` should both be zero +=# + +decompose(x::Integer) = x, 0, 1 + +function decompose(x::Float16)::NTuple{3,Int} + isnan(x) && return 0, 0, 0 + isinf(x) && return ifelse(x < 0, -1, 1), 0, 0 + n = reinterpret(UInt16, x) + s = (n & 0x03ff) % Int16 + e = ((n & 0x7c00) >> 10) % Int + s |= Int16(e != 0) << 10 + d = ifelse(signbit(x), -1, 1) + s, e - 25 + (e == 0), d +end + +function decompose(x::Float32)::NTuple{3,Int} + isnan(x) && return 0, 0, 0 + isinf(x) && return ifelse(x < 0, -1, 1), 0, 0 + n = reinterpret(UInt32, x) + s = (n & 0x007fffff) % Int32 + e = ((n & 0x7f800000) >> 23) % Int + s |= Int32(e != 0) << 23 + d = ifelse(signbit(x), -1, 1) + s, e - 150 + (e == 0), d +end + +function decompose(x::Float64)::Tuple{Int64, Int, Int} + isnan(x) && return 0, 0, 0 + isinf(x) && return ifelse(x < 0, -1, 1), 0, 0 + n = reinterpret(UInt64, x) + s = (n & 0x000fffffffffffff) % Int64 + e = ((n & 0x7ff0000000000000) >> 52) % Int + s |= Int64(e != 0) << 52 + d = ifelse(signbit(x), -1, 1) + s, e - 1075 + (e == 0), d +end -hash(x::Union{Bool,Int8,UInt8,Int16,UInt16,Int32,UInt32}, h::UInt) = hash(Int64(x), h) -hash(x::Float32, h::UInt) = hash(Float64(x), h) """ - precision(num::AbstractFloat) + precision(num::AbstractFloat; base::Integer=2) + precision(T::Type; base::Integer=2) Get the precision of a floating point number, as defined by the effective number of bits in -the significand. +the significand, or the precision of a floating-point type `T` (its current default, if +`T` is a variable-precision type like [`BigFloat`](@ref)). + +If `base` is specified, then it returns the maximum corresponding +number of significand digits in that base. + +!!! compat "Julia 1.8" + The `base` keyword requires at least Julia 1.8. """ function precision end -precision(::Type{Float16}) = 11 -precision(::Type{Float32}) = 24 -precision(::Type{Float64}) = 53 -precision(::T) where {T<:AbstractFloat} = precision(T) +_precision(::Type{Float16}) = 11 +_precision(::Type{Float32}) = 24 +_precision(::Type{Float64}) = 53 +function _precision(x, base::Integer=2) + base > 1 || throw(DomainError(base, "`base` cannot be less than 2.")) + p = _precision(x) + return base == 2 ? Int(p) : floor(Int, p / log2(base)) +end +precision(::Type{T}; base::Integer=2) where {T<:AbstractFloat} = _precision(T, base) +precision(::T; base::Integer=2) where {T<:AbstractFloat} = precision(T; base) """ uabs(x::Integer) @@ -590,7 +680,7 @@ uabs(x::BitSigned) = unsigned(abs(x)) nextfloat(x::AbstractFloat, n::Integer) The result of `n` iterative applications of `nextfloat` to `x` if `n >= 0`, or `-n` -applications of `prevfloat` if `n < 0`. +applications of [`prevfloat`](@ref) if `n < 0`. """ function nextfloat(f::IEEEFloat, d::Integer) F = typeof(f) @@ -635,6 +725,8 @@ end Return the smallest floating point number `y` of the same type as `x` such `x < y`. If no such `y` exists (e.g. if `x` is `Inf` or `NaN`), then return `x`. + +See also: [`prevfloat`](@ref), [`eps`](@ref), [`issubnormal`](@ref). """ nextfloat(x::AbstractFloat) = nextfloat(x,1) @@ -642,7 +734,7 @@ nextfloat(x::AbstractFloat) = nextfloat(x,1) prevfloat(x::AbstractFloat, n::Integer) The result of `n` iterative applications of `prevfloat` to `x` if `n >= 0`, or `-n` -applications of `nextfloat` if `n < 0`. +applications of [`nextfloat`](@ref) if `n < 0`. """ prevfloat(x::AbstractFloat, d::Integer) = nextfloat(x, -d) @@ -655,7 +747,7 @@ such `y` exists (e.g. if `x` is `-Inf` or `NaN`), then return `x`. prevfloat(x::AbstractFloat) = nextfloat(x,-1) for Ti in (Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128) - for Tf in (Float32, Float64) + for Tf in (Float16, Float32, Float64) if Ti <: Unsigned || sizeof(Ti) < sizeof(Tf) # Here `Tf(typemin(Ti))-1` is exact, so we can compare the lower-bound # directly. `Tf(typemax(Ti))+1` is either always exactly representable, or @@ -711,6 +803,10 @@ function issubnormal(x::T) where {T<:IEEEFloat} (y & exponent_mask(T) == 0) & (y & significand_mask(T) != 0) end +ispow2(x::AbstractFloat) = !iszero(x) && frexp(x)[1] == 0.5 +iseven(x::AbstractFloat) = isinteger(x) && (abs(x) > maxintfloat(x) || iseven(Integer(x))) +isodd(x::AbstractFloat) = isinteger(x) && abs(x) ≤ maxintfloat(x) && isodd(Integer(x)) + @eval begin typemin(::Type{Float16}) = $(bitcast(Float16, 0xfc00)) typemax(::Type{Float16}) = $(Inf16) @@ -760,6 +856,8 @@ floatmin(x::T) where {T<:AbstractFloat} = floatmin(T) Return the largest finite number representable by the floating-point type `T`. +See also: [`typemax`](@ref), [`floatmin`](@ref), [`eps`](@ref). + # Examples ```jldoctest julia> floatmax(Float16) @@ -770,6 +868,9 @@ julia> floatmax(Float32) julia> floatmax() 1.7976931348623157e308 + +julia> typemax(Float64) +Inf ``` """ floatmax(x::T) where {T<:AbstractFloat} = floatmax(T) @@ -824,6 +925,8 @@ is the nearest floating point number to ``y``, then |y-x| \\leq \\operatorname{eps}(x)/2. ``` +See also: [`nextfloat`](@ref), [`issubnormal`](@ref), [`floatmax`](@ref). + # Examples ```jldoctest julia> eps(1.0) @@ -851,46 +954,21 @@ eps(::AbstractFloat) ## byte order swaps for arbitrary-endianness serialization/deserialization ## bswap(x::IEEEFloat) = bswap_int(x) -# bit patterns -reinterpret(::Type{Unsigned}, x::Float64) = reinterpret(UInt64, x) -reinterpret(::Type{Unsigned}, x::Float32) = reinterpret(UInt32, x) -reinterpret(::Type{Signed}, x::Float64) = reinterpret(Int64, x) -reinterpret(::Type{Signed}, x::Float32) = reinterpret(Int32, x) - -sign_mask(::Type{Float64}) = 0x8000_0000_0000_0000 -exponent_mask(::Type{Float64}) = 0x7ff0_0000_0000_0000 -exponent_one(::Type{Float64}) = 0x3ff0_0000_0000_0000 -exponent_half(::Type{Float64}) = 0x3fe0_0000_0000_0000 -significand_mask(::Type{Float64}) = 0x000f_ffff_ffff_ffff - -sign_mask(::Type{Float32}) = 0x8000_0000 -exponent_mask(::Type{Float32}) = 0x7f80_0000 -exponent_one(::Type{Float32}) = 0x3f80_0000 -exponent_half(::Type{Float32}) = 0x3f00_0000 -significand_mask(::Type{Float32}) = 0x007f_ffff - -sign_mask(::Type{Float16}) = 0x8000 -exponent_mask(::Type{Float16}) = 0x7c00 -exponent_one(::Type{Float16}) = 0x3c00 -exponent_half(::Type{Float16}) = 0x3800 -significand_mask(::Type{Float16}) = 0x03ff - -for T in (Float16, Float32, Float64) - @eval significand_bits(::Type{$T}) = $(trailing_ones(significand_mask(T))) - @eval exponent_bits(::Type{$T}) = $(sizeof(T)*8 - significand_bits(T) - 1) - @eval exponent_bias(::Type{$T}) = $(Int(exponent_one(T) >> significand_bits(T))) - # maximum float exponent - @eval exponent_max(::Type{$T}) = $(Int(exponent_mask(T) >> significand_bits(T)) - exponent_bias(T)) - # maximum float exponent without bias - @eval exponent_raw_max(::Type{$T}) = $(Int(exponent_mask(T) >> significand_bits(T))) -end - # integer size of float uinttype(::Type{Float64}) = UInt64 uinttype(::Type{Float32}) = UInt32 uinttype(::Type{Float16}) = UInt16 +inttype(::Type{Float64}) = Int64 +inttype(::Type{Float32}) = Int32 +inttype(::Type{Float16}) = Int16 +# float size of integer +floattype(::Type{UInt64}) = Float64 +floattype(::Type{UInt32}) = Float32 +floattype(::Type{UInt16}) = Float16 +floattype(::Type{Int64}) = Float64 +floattype(::Type{Int32}) = Float32 +floattype(::Type{Int16}) = Float16 -Base.iszero(x::Float16) = reinterpret(UInt16, x) & ~sign_mask(Float16) == 0x0000 ## Array operations on floating point numbers ## diff --git a/base/floatfuncs.jl b/base/floatfuncs.jl index 97f72134c28f45..4276ec0daecaf1 100644 --- a/base/floatfuncs.jl +++ b/base/floatfuncs.jl @@ -97,9 +97,8 @@ julia> round(357.913; sigdigits=4, base=2) Rounding to specified digits in bases other than 2 can be inexact when operating on binary floating point numbers. For example, the [`Float64`](@ref) value represented by `1.15` is actually *less* than 1.15, yet will be - rounded to 1.2. + rounded to 1.2. For example: - # Examples ```jldoctest; setup = :(using Printf) julia> x = 1.15 1.15 @@ -166,6 +165,16 @@ function _round_invstep(x, invstep, r::RoundingMode) return y end +# round x to multiples of 1/(invstepsqrt^2) +# Using square root of step prevents overflowing +function _round_invstepsqrt(x, invstepsqrt, r::RoundingMode) + y = round((x * invstepsqrt) * invstepsqrt, r) / invstepsqrt / invstepsqrt + if !isfinite(y) + return x + end + return y +end + # round x to multiples of step function _round_step(x, step, r::RoundingMode) # TODO: use div with rounding mode @@ -186,10 +195,15 @@ function _round_digits(x, r::RoundingMode, digits::Integer, base) fx = float(x) if digits >= 0 invstep = oftype(fx, base)^digits - _round_invstep(fx, invstep, r) + if isfinite(invstep) + return _round_invstep(fx, invstep, r) + else + invstepsqrt = oftype(fx, base)^oftype(fx, digits/2) + return _round_invstepsqrt(fx, invstepsqrt, r) + end else step = oftype(fx, base)^-digits - _round_step(fx, step, r) + return _round_step(fx, step, r) end end @@ -221,17 +235,23 @@ function round(x::T, ::RoundingMode{:NearestTiesUp}) where {T <: AbstractFloat} copysign(floor((x + (T(0.25) - eps(T(0.5)))) + (T(0.25) + eps(T(0.5)))), x) end +function Base.round(x::AbstractFloat, ::typeof(RoundFromZero)) + signbit(x) ? round(x, RoundDown) : round(x, RoundUp) +end + # isapprox: approximate equality of numbers """ isapprox(x, y; atol::Real=0, rtol::Real=atol>0 ? 0 : √eps, nans::Bool=false[, norm::Function]) -Inexact equality comparison: `true` if `norm(x-y) <= max(atol, rtol*max(norm(x), norm(y)))`. The -default `atol` is zero and the default `rtol` depends on the types of `x` and `y`. The keyword -argument `nans` determines whether or not NaN values are considered equal (defaults to false). +Inexact equality comparison. Two numbers compare equal if their relative distance *or* their +absolute distance is within tolerance bounds: `isapprox` returns `true` if +`norm(x-y) <= max(atol, rtol*max(norm(x), norm(y)))`. The default `atol` is zero and the +default `rtol` depends on the types of `x` and `y`. The keyword argument `nans` determines +whether or not NaN values are considered equal (defaults to false). For real or complex floating-point values, if an `atol > 0` is not specified, `rtol` defaults to the square root of [`eps`](@ref) of the type of `x` or `y`, whichever is bigger (least precise). -This corresponds to requiring equality of about half of the significand digits. Otherwise, +This corresponds to requiring equality of about half of the significant digits. Otherwise, e.g. for integer arguments or if an `atol > 0` is supplied, `rtol` defaults to zero. The `norm` keyword defaults to `abs` for numeric `(x,y)` and to `LinearAlgebra.norm` for @@ -253,16 +273,22 @@ for example, in `x - y ≈ 0`, `atol=1e-9` is an absurdly small tolerance if `x` but an absurdly large tolerance if `x` is the [radius of a Hydrogen atom](https://en.wikipedia.org/wiki/Bohr_radius) in meters. +!!! compat "Julia 1.6" + Passing the `norm` keyword argument when comparing numeric (non-array) arguments + requires Julia 1.6 or later. # Examples ```jldoctest -julia> 0.1 ≈ (0.1 - 1e-10) +julia> isapprox(0.1, 0.15; atol=0.05) true -julia> isapprox(10, 11; atol = 2) +julia> isapprox(0.1, 0.15; rtol=0.34) true -julia> isapprox([10.0^9, 1.0], [10.0^9, 2.0]) +julia> isapprox(0.1, 0.15; rtol=0.33) +false + +julia> 0.1 + 1e-10 ≈ 0.1 true julia> 1e-10 ≈ 0 @@ -270,6 +296,9 @@ false julia> isapprox(1e-10, 0, atol=1e-8) true + +julia> isapprox([10.0^9, 1.0], [10.0^9, 2.0]) # using `norm` +true ``` """ function isapprox(x::Number, y::Number; @@ -284,6 +313,9 @@ end Create a function that compares its argument to `x` using `≈`, i.e. a function equivalent to `y -> y ≈ x`. The keyword arguments supported here are the same as those in the 2-argument `isapprox`. + +!!! compat "Julia 1.5" + This method requires Julia 1.5 or later. """ isapprox(y; kwargs...) = x -> isapprox(x, y; kwargs...) @@ -313,30 +345,88 @@ significantly more expensive than `x*y+z`. `fma` is used to improve accuracy in algorithms. See [`muladd`](@ref). """ function fma end +function fma_emulated(a::Float32, b::Float32, c::Float32)::Float32 + ab = Float64(a) * b + res = ab+c + reinterpret(UInt64, res)&0x1fff_ffff!=0x1000_0000 && return res + # yes error compensation is necessary. It sucks + reslo = abs(c)>abs(ab) ? ab-(res - c) : c-(res - ab) + res = iszero(reslo) ? res : (signbit(reslo) ? prevfloat(res) : nextfloat(res)) + return res +end + +""" Splits a Float64 into a hi bit and a low bit where the high bit has 27 trailing 0s and the low bit has 26 trailing 0s""" +@inline function splitbits(x::Float64) + hi = reinterpret(Float64, reinterpret(UInt64, x) & 0xffff_ffff_f800_0000) + return hi, x-hi +end -fma_libm(x::Float32, y::Float32, z::Float32) = - ccall(("fmaf", libm_name), Float32, (Float32,Float32,Float32), x, y, z) -fma_libm(x::Float64, y::Float64, z::Float64) = - ccall(("fma", libm_name), Float64, (Float64,Float64,Float64), x, y, z) +function twomul(a::Float64, b::Float64) + ahi, alo = splitbits(a) + bhi, blo = splitbits(b) + abhi = a*b + blohi, blolo = splitbits(blo) + ablo = alo*blohi - (((abhi - ahi*bhi) - alo*bhi) - ahi*blo) + blolo*alo + return abhi, ablo +end + +function fma_emulated(a::Float64, b::Float64,c::Float64) + abhi, ablo = @inline twomul(a,b) + if !isfinite(abhi+c) || isless(abs(abhi), nextfloat(0x1p-969)) || issubnormal(a) || issubnormal(b) + aandbfinite = isfinite(a) && isfinite(b) + if !(isfinite(c) && aandbfinite) + return aandbfinite ? c : abhi+c + end + (iszero(a) || iszero(b)) && return abhi+c + # The checks above satisfy exponent's nothrow precondition + bias = Math._exponent_finite_nonzero(a) + Math._exponent_finite_nonzero(b) + c_denorm = ldexp(c, -bias) + if isfinite(c_denorm) + # rescale a and b to [1,2), equivalent to ldexp(a, -exponent(a)) + issubnormal(a) && (a *= 0x1p52) + issubnormal(b) && (b *= 0x1p52) + a = reinterpret(Float64, (reinterpret(UInt64, a) & ~Base.exponent_mask(Float64)) | Base.exponent_one(Float64)) + b = reinterpret(Float64, (reinterpret(UInt64, b) & ~Base.exponent_mask(Float64)) | Base.exponent_one(Float64)) + c = c_denorm + abhi, ablo = twomul(a,b) + # abhi <= 4 -> isfinite(r) (α) + r = abhi+c + # s ≈ 0 (β) + s = (abs(abhi) > abs(c)) ? (abhi-r+c+ablo) : (c-r+abhi+ablo) + # α ⩓ β -> isfinite(sumhi) (γ) + sumhi = r+s + # If result is subnormal, ldexp will cause double rounding because subnormals have fewer mantisa bits. + # As such, we need to check whether round to even would lead to double rounding and manually round sumhi to avoid it. + if issubnormal(ldexp(sumhi, bias)) + sumlo = r-sumhi+s + # finite: See γ + # non-zero: If sumhi == ±0., then ldexp(sumhi, bias) == ±0, + # so we don't take this branch. + bits_lost = -bias-Math._exponent_finite_nonzero(sumhi)-1022 + sumhiInt = reinterpret(UInt64, sumhi) + if (bits_lost != 1) ⊻ (sumhiInt&1 == 1) + sumhi = nextfloat(sumhi, cmp(sumlo,0)) + end + end + return ldexp(sumhi, bias) + end + isinf(abhi) && signbit(c) == signbit(a*b) && return abhi + # fall through + end + r = abhi+c + s = (abs(abhi) > abs(c)) ? (abhi-r+c+ablo) : (c-r+abhi+ablo) + return r+s +end fma_llvm(x::Float32, y::Float32, z::Float32) = fma_float(x, y, z) fma_llvm(x::Float64, y::Float64, z::Float64) = fma_float(x, y, z) + # Disable LLVM's fma if it is incorrect, e.g. because LLVM falls back -# onto a broken system libm; if so, use openlibm's fma instead -# 1.0000305f0 = 1 + 1/2^15 -# 1.0000000009313226 = 1 + 1/2^30 -# If fma_llvm() clobbers the rounding mode, the result of 0.1 + 0.2 will be 0.3 -# instead of the properly-rounded 0.30000000000000004; check after calling fma -if (Sys.ARCH !== :i686 && fma_llvm(1.0000305f0, 1.0000305f0, -1.0f0) == 6.103609f-5 && - (fma_llvm(1.0000000009313226, 1.0000000009313226, -1.0) == - 1.8626451500983188e-9) && 0.1 + 0.2 == 0.30000000000000004) - fma(x::Float32, y::Float32, z::Float32) = fma_llvm(x,y,z) - fma(x::Float64, y::Float64, z::Float64) = fma_llvm(x,y,z) -else - fma(x::Float32, y::Float32, z::Float32) = fma_libm(x,y,z) - fma(x::Float64, y::Float64, z::Float64) = fma_libm(x,y,z) -end +# onto a broken system libm; if so, use a software emulated fma +@assume_effects :consistent fma(x::Float32, y::Float32, z::Float32) = Core.Intrinsics.have_fma(Float32) ? fma_llvm(x,y,z) : fma_emulated(x,y,z) +@assume_effects :consistent fma(x::Float64, y::Float64, z::Float64) = Core.Intrinsics.have_fma(Float64) ? fma_llvm(x,y,z) : fma_emulated(x,y,z) + function fma(a::Float16, b::Float16, c::Float16) - Float16(fma(Float32(a), Float32(b), Float32(c))) + Float16(muladd(Float32(a), Float32(b), Float32(c))) #don't use fma if the hardware doesn't have it. end # This is necessary at least on 32-bit Intel Linux, since fma_llvm may diff --git a/base/gcutils.jl b/base/gcutils.jl index 51e3943877444c..d17301a1be9b07 100644 --- a/base/gcutils.jl +++ b/base/gcutils.jl @@ -50,8 +50,7 @@ function finalizer(@nospecialize(f), @nospecialize(o)) return o end -function finalizer(f::Ptr{Cvoid}, o::T) where T - @_inline_meta +function finalizer(f::Ptr{Cvoid}, o::T) where T @inline if !ismutable(o) error("objects of type ", typeof(o), " cannot be finalized") end @@ -65,8 +64,8 @@ end Immediately run finalizers registered for object `x`. """ -finalize(@nospecialize(o)) = ccall(:jl_finalize_th, Cvoid, (Ptr{Cvoid}, Any,), - Core.getptls(), o) +finalize(@nospecialize(o)) = ccall(:jl_finalize_th, Cvoid, (Any, Any,), + current_task(), o) """ Base.GC @@ -106,6 +105,27 @@ Control whether garbage collection is enabled using a boolean argument (`true` f """ enable(on::Bool) = ccall(:jl_gc_enable, Int32, (Int32,), on) != 0 +""" + GC.enable_finalizers(on::Bool) + +Increment or decrement the counter that controls the running of finalizers on +the current Task. Finalizers will only run when the counter is at zero. (Set +`true` for enabling, `false` for disabling). They may still run concurrently on +another Task or thread. +""" +enable_finalizers(on::Bool) = on ? enable_finalizers() : disable_finalizers() + +function enable_finalizers() @inline + ccall(:jl_gc_enable_finalizers_internal, Cvoid, ()) + if Core.Intrinsics.atomic_pointerref(cglobal(:jl_gc_have_pending_finalizers, Cint), :monotonic) != 0 + ccall(:jl_gc_run_pending_finalizers, Cvoid, (Ptr{Cvoid},), C_NULL) + end +end + +function disable_finalizers() @inline + ccall(:jl_gc_disable_finalizers_internal, Cvoid, ()) +end + """ GC.@preserve x1 x2 ... xn expr @@ -177,4 +197,13 @@ collection to run. """ safepoint() = ccall(:jl_gc_safepoint, Cvoid, ()) +""" + GC.enable_logging(on::Bool) + +When turned on, print statistics about each GC to stderr. +""" +function enable_logging(on::Bool=true) + ccall(:jl_enable_gc_logging, Cvoid, (Cint,), on) +end + end # module GC diff --git a/base/generator.jl b/base/generator.jl index 203d1dbba11990..d11742fe5b72f0 100644 --- a/base/generator.jl +++ b/base/generator.jl @@ -40,7 +40,7 @@ Generator(::Type{T}, iter::I) where {T,I} = Generator{I,Type{T}}(T, iter) Generator(::Type{T}, I1, I2, Is...) where {T} = Generator(a->T(a...), zip(I1, I2, Is...)) function iterate(g::Generator, s...) - @_inline_meta + @inline y = iterate(g.iter, s...) y === nothing && return nothing y = y::Tuple{Any, Any} # try to give inference some idea of what to expect about the behavior of the next line @@ -51,7 +51,10 @@ length(g::Generator) = length(g.iter) size(g::Generator) = size(g.iter) axes(g::Generator) = axes(g.iter) ndims(g::Generator) = ndims(g.iter) - +keys(g::Generator) = keys(g.iter) +last(g::Generator) = g.f(last(g.iter)) +isempty(g::Generator) = isempty(g.iter) +isdone(g::Generator, state...) = isdone(g.iter, state...) ## iterator traits diff --git a/base/gmp.jl b/base/gmp.jl index be2d8128edde52..5d3cabac87e40f 100644 --- a/base/gmp.jl +++ b/base/gmp.jl @@ -4,13 +4,13 @@ module GMP export BigInt -import .Base: *, +, -, /, <, <<, >>, >>>, <=, ==, >, >=, ^, (~), (&), (|), xor, +import .Base: *, +, -, /, <, <<, >>, >>>, <=, ==, >, >=, ^, (~), (&), (|), xor, nand, nor, binomial, cmp, convert, div, divrem, factorial, cld, fld, gcd, gcdx, lcm, mod, - ndigits, promote_rule, rem, show, isqrt, string, powermod, - sum, trailing_zeros, trailing_ones, count_ones, tryparse_internal, + ndigits, promote_rule, rem, show, isqrt, string, powermod, sum, prod, + trailing_zeros, trailing_ones, count_ones, count_zeros, tryparse_internal, bin, oct, dec, hex, isequal, invmod, _prevpow2, _nextpow2, ndigits0zpb, widen, signed, unsafe_trunc, trunc, iszero, isone, big, flipsign, signbit, - sign, hastypemax, isodd, digits! + sign, hastypemax, isodd, iseven, digits!, hash, hash_integer if Clong == Int32 const ClongMax = Union{Int8, Int16, Int32} @@ -94,10 +94,10 @@ const ALLOC_OVERFLOW_FUNCTION = Ref(false) function __init__() try if version().major != VERSION.major || bits_per_limb() != BITS_PER_LIMB - msg = bits_per_limb() != BITS_PER_LIMB ? error : warn - msg("The dynamically loaded GMP library (v\"$(version())\" with __gmp_bits_per_limb == $(bits_per_limb()))\n", - "does not correspond to the compile time version (v\"$VERSION\" with __gmp_bits_per_limb == $BITS_PER_LIMB).\n", - "Please rebuild Julia.") + msg = """The dynamically loaded GMP library (v\"$(version())\" with __gmp_bits_per_limb == $(bits_per_limb())) + does not correspond to the compile time version (v\"$VERSION\" with __gmp_bits_per_limb == $BITS_PER_LIMB). + Please rebuild Julia.""" + bits_per_limb() != BITS_PER_LIMB ? @error(msg) : @warn(msg) end ccall((:__gmp_set_memory_functions, :libgmp), Cvoid, @@ -132,7 +132,7 @@ module MPZ # - a method modifying its input has a "!" appendend to its name, according to Julia's conventions # - some convenient methods are added (in addition to the pure MPZ ones), e.g. `add(a, b) = add!(BigInt(), a, b)` # and `add!(x, a) = add!(x, x, a)`. -using .Base.GMP: BigInt, Limb, BITS_PER_LIMB +using ..GMP: BigInt, Limb, BITS_PER_LIMB const mpz_t = Ref{BigInt} const bitcnt_t = Culong @@ -178,7 +178,9 @@ ui_sub!(x::BigInt, a, b::BigInt) = (ccall((:__gmpz_ui_sub, :libgmp), Cvoid, (mpz ui_sub(a, b::BigInt) = ui_sub!(BigInt(), a, b) for op in (:scan1, :scan0) - @eval $op(a::BigInt, b) = Int(ccall($(gmpz(op)), Culong, (mpz_t, Culong), a, b)) + # when there is no meaningful answer, ccall returns typemax(Culong), where Culong can + # be UInt32 (Windows) or UInt64; we return -1 in this case for all architectures + @eval $op(a::BigInt, b) = Int(signed(ccall($(gmpz(op)), Culong, (mpz_t, Culong), a, b))) end mul_si!(x::BigInt, a::BigInt, b) = (ccall((:__gmpz_mul_si, :libgmp), Cvoid, (mpz_t, mpz_t, Clong), x, a, b); x) @@ -203,7 +205,7 @@ for (op, T) in ((:fac_ui, Culong), (:set_ui, Culong), (:set_si, Clong), (:set_d, end end -popcount(a::BigInt) = Int(ccall((:__gmpz_popcount, :libgmp), Culong, (mpz_t,), a)) +popcount(a::BigInt) = Int(signed(ccall((:__gmpz_popcount, :libgmp), Culong, (mpz_t,), a))) mpn_popcount(d::Ptr{Limb}, s::Integer) = Int(ccall((:__gmpn_popcount, :libgmp), Culong, (Ptr{Limb}, Csize_t), d, s)) mpn_popcount(a::BigInt) = mpn_popcount(a.d, abs(a.size)) @@ -292,14 +294,14 @@ BigInt(x::Union{Clong,Int32}) = MPZ.set_si(x) BigInt(x::Union{Culong,UInt32}) = MPZ.set_ui(x) BigInt(x::Bool) = BigInt(UInt(x)) -unsafe_trunc(::Type{BigInt}, x::Union{Float32,Float64}) = MPZ.set_d(x) +unsafe_trunc(::Type{BigInt}, x::Union{Float16,Float32,Float64}) = MPZ.set_d(x) -function BigInt(x::Union{Float32,Float64}) +function BigInt(x::Float64) isinteger(x) || throw(InexactError(:BigInt, BigInt, x)) unsafe_trunc(BigInt,x) end -function trunc(::Type{BigInt}, x::Union{Float32,Float64}) +function trunc(::Type{BigInt}, x::Union{Float16,Float32,Float64}) isfinite(x) || throw(InexactError(:trunc, BigInt, x)) unsafe_trunc(BigInt,x) end @@ -308,21 +310,21 @@ BigInt(x::Float16) = BigInt(Float64(x)) BigInt(x::Float32) = BigInt(Float64(x)) function BigInt(x::Integer) - x == 0 && return BigInt(Culong(0)) + # On 64-bit Windows, `Clong` is `Int32`, not `Int64`, so construction of + # `Int64` constants, e.g. `BigInt(3)`, uses this method. + isbits(x) && typemin(Clong) <= x <= typemax(Clong) && return BigInt((x % Clong)::Clong) nd = ndigits(x, base=2) z = MPZ.realloc2(nd) - s = sign(x) - s == -1 && (x = -x) - x = unsigned(x) + ux = unsigned(x < 0 ? -x : x) size = 0 limbnbits = sizeof(Limb) << 3 while nd > 0 size += 1 - unsafe_store!(z.d, x % Limb, size) - x >>>= limbnbits + unsafe_store!(z.d, ux % Limb, size) + ux >>= limbnbits nd -= limbnbits end - z.size = s*size + z.size = x < 0 ? -size : size z end @@ -343,6 +345,7 @@ end rem(x::Integer, ::Type{BigInt}) = BigInt(x) isodd(x::BigInt) = MPZ.tstbit(x, 0) +iseven(x::BigInt) = !isodd(x) function (::Type{T})(x::BigInt) where T<:Base.BitUnsigned if sizeof(T) < sizeof(Limb) @@ -551,10 +554,30 @@ end >>(x::BigInt, c::UInt) = c == 0 ? x : MPZ.fdiv_q_2exp(x, c) >>>(x::BigInt, c::UInt) = x >> c -trailing_zeros(x::BigInt) = MPZ.scan1(x, 0) -trailing_ones(x::BigInt) = MPZ.scan0(x, 0) +function trailing_zeros(x::BigInt) + c = MPZ.scan1(x, 0) + c == -1 && throw(DomainError(x, "`x` must be non-zero")) + c +end + +function trailing_ones(x::BigInt) + c = MPZ.scan0(x, 0) + c == -1 && throw(DomainError(x, "`x` must not be equal to -1")) + c +end + +function count_ones(x::BigInt) + c = MPZ.popcount(x) + c == -1 && throw(DomainError(x, "`x` cannot be negative")) + c +end -count_ones(x::BigInt) = MPZ.popcount(x) +# generic definition is not used to provide a better error message +function count_zeros(x::BigInt) + c = MPZ.popcount(~x) + c == -1 && throw(DomainError(x, "`x` must be negative")) + c +end """ count_ones_abs(x::BigInt) @@ -564,6 +587,7 @@ Number of ones in the binary representation of abs(x). count_ones_abs(x::BigInt) = iszero(x) ? 0 : MPZ.mpn_popcount(x) divrem(x::BigInt, y::BigInt) = MPZ.tdiv_qr(x, y) +divrem(x::BigInt, y::Integer) = MPZ.tdiv_qr(x, big(y)) cmp(x::BigInt, y::BigInt) = sign(MPZ.cmp(x, y)) cmp(x::BigInt, y::ClongMax) = sign(MPZ.cmp_si(x, y)) @@ -631,13 +655,26 @@ function gcdx(a::BigInt, b::BigInt) g, s, t end -sum(arr::AbstractArray{BigInt}) = foldl(MPZ.add!, arr; init=BigInt(0)) -# Note: a similar implementation for `prod` won't be efficient: -# 1) the time complexity of the allocations is negligible compared to the multiplications -# 2) assuming arr contains similarly sized BigInts, the multiplications are much more -# performant when doing e.g. ((a1*a2)*(a3*a4))*(...) rather than a1*(a2*(a3*(...))), -# which is exactly what the default implementation of `prod` does, via `mapreduce` -# (which maybe could be slightly optimized for BigInt). ++(x::BigInt, y::BigInt, rest::BigInt...) = sum(tuple(x, y, rest...)) +sum(arr::Union{AbstractArray{BigInt}, Tuple{BigInt, Vararg{BigInt}}}) = + foldl(MPZ.add!, arr; init=BigInt(0)) + +function prod(arr::AbstractArray{BigInt}) + # compute first the needed number of bits for the result, + # to avoid re-allocations; + # GMP will always request n+m limbs for the result in MPZ.mul!, + # if the arguments have n and m limbs; so we add all the bits + # taken by the array elements, and add BITS_PER_LIMB to that, + # to account for the rounding to limbs in MPZ.mul! + # (BITS_PER_LIMB-1 would typically be enough, to which we add + # 1 for the initial multiplication by init=1 in foldl) + nbits = GC.@preserve arr sum(arr; init=BITS_PER_LIMB) do x + abs(x.size) * BITS_PER_LIMB - leading_zeros(unsafe_load(x.d)) + end + init = BigInt(; nbits) + MPZ.set_si!(init, 1) + foldl(MPZ.mul!, arr; init) +end factorial(x::BigInt) = isneg(x) ? BigInt(0) : MPZ.fac_ui(x) @@ -700,7 +737,7 @@ function digits!(a::AbstractVector{T}, n::BigInt; base::Integer = 10) where {T<: i, j = firstindex(a)-1, length(s)+1 lasti = min(lastindex(a), firstindex(a) + length(s)-1 - isneg(n)) while i < lasti - # base ≤ 36: 0-9, plus a-z for 10-35 + # base ≤ 36: 0-9, plus a-z for 10-35 # base > 36: 0-9, plus A-Z for 10-35 and a-z for 36..61 x = s[j -= 1] a[i += 1] = base ≤ 36 ? (x>0x39 ? x-0x57 : x-0x30) : (x>0x39 ? (x>0x60 ? x-0x3d : x-0x37) : x-0x30) @@ -755,13 +792,237 @@ Base.add_with_overflow(a::BigInt, b::BigInt) = a + b, false Base.sub_with_overflow(a::BigInt, b::BigInt) = a - b, false Base.mul_with_overflow(a::BigInt, b::BigInt) = a * b, false -function Base.deepcopy_internal(x::BigInt, stackdict::IdDict) - if haskey(stackdict, x) - return stackdict[x] +Base.deepcopy_internal(x::BigInt, stackdict::IdDict) = get!(() -> MPZ.set(x), stackdict, x) + +## streamlined hashing for BigInt, by avoiding allocation from shifts ## + +if Limb === UInt + # this condition is true most (all?) of the time, and in this case we can define + # an optimized version for BigInt of hash_integer (used e.g. for Rational{BigInt}), + # and of hash + + using .Base: hash_uint + + function hash_integer(n::BigInt, h::UInt) + GC.@preserve n begin + s = n.size + s == 0 && return hash_integer(0, h) + p = convert(Ptr{UInt}, n.d) + b = unsafe_load(p) + h ⊻= hash_uint(ifelse(s < 0, -b, b) ⊻ h) + for k = 2:abs(s) + h ⊻= hash_uint(unsafe_load(p, k) ⊻ h) + end + return h + end + end + + _divLimb(n) = UInt === UInt64 ? n >>> 6 : n >>> 5 + _modLimb(n) = UInt === UInt64 ? n & 63 : n & 31 + + function hash(x::BigInt, h::UInt) + GC.@preserve x begin + sz = x.size + sz == 0 && return hash(0, h) + ptr = Ptr{UInt}(x.d) + if sz == 1 + return hash(unsafe_load(ptr), h) + elseif sz == -1 + limb = unsafe_load(ptr) + limb <= typemin(Int) % UInt && return hash(-(limb % Int), h) + end + pow = trailing_zeros(x) + nd = Base.ndigits0z(x, 2) + idx = _divLimb(pow) + 1 + shift = _modLimb(pow) % UInt + upshift = BITS_PER_LIMB - shift + asz = abs(sz) + if shift == 0 + limb = unsafe_load(ptr, idx) + else + limb1 = unsafe_load(ptr, idx) + limb2 = idx < asz ? unsafe_load(ptr, idx+1) : UInt(0) + limb = limb2 << upshift | limb1 >> shift + end + if nd <= 1024 && nd - pow <= 53 + return hash(ldexp(flipsign(Float64(limb), sz), pow), h) + end + h = hash_integer(1, h) + h = hash_integer(pow, h) + h ⊻= hash_uint(flipsign(limb, sz) ⊻ h) + for idx = idx+1:asz + if shift == 0 + limb = unsafe_load(ptr, idx) + else + limb1 = limb2 + if idx == asz + limb = limb1 >> shift + limb == 0 && break # don't hash leading zeros + else + limb2 = unsafe_load(ptr, idx+1) + limb = limb2 << upshift | limb1 >> shift + end + end + h ⊻= hash_uint(limb ⊻ h) + end + return h + end + end +end + +module MPQ + +# Rational{BigInt} +import .Base: unsafe_rational, __throw_rational_argerror_zero +import ..GMP: BigInt, MPZ, Limb, isneg + +gmpq(op::Symbol) = (Symbol(:__gmpq_, op), :libgmp) + +mutable struct _MPQ + num_alloc::Cint + num_size::Cint + num_d::Ptr{Limb} + den_alloc::Cint + den_size::Cint + den_d::Ptr{Limb} + # to prevent GC + rat::Rational{BigInt} +end + +const mpq_t = Ref{_MPQ} + +_MPQ(x::BigInt,y::BigInt) = _MPQ(x.alloc, x.size, x.d, + y.alloc, y.size, y.d, + unsafe_rational(BigInt, x, y)) +_MPQ() = _MPQ(BigInt(), BigInt()) +_MPQ(x::Rational{BigInt}) = _MPQ(x.num, x.den) + +function sync_rational!(xq::_MPQ) + xq.rat.num.alloc = xq.num_alloc + xq.rat.num.size = xq.num_size + xq.rat.num.d = xq.num_d + xq.rat.den.alloc = xq.den_alloc + xq.rat.den.size = xq.den_size + xq.rat.den.d = xq.den_d + return xq.rat +end + +function Rational{BigInt}(num::BigInt, den::BigInt) + if iszero(den) + iszero(num) && __throw_rational_argerror_zero(BigInt) + return set_si(flipsign(1, num), 0) + end + xq = _MPQ(MPZ.set(num), MPZ.set(den)) + ccall((:__gmpq_canonicalize, :libgmp), Cvoid, (mpq_t,), xq) + return sync_rational!(xq) +end + +# define set, set_ui, set_si, set_z, and their inplace versions +function set!(z::Rational{BigInt}, x::Rational{BigInt}) + zq = _MPQ(z) + ccall((:__gmpq_set, :libgmp), Cvoid, (mpq_t, mpq_t), zq, _MPQ(x)) + return sync_rational!(zq) +end + +function set_z!(z::Rational{BigInt}, x::BigInt) + zq = _MPQ(z) + ccall((:__gmpq_set_z, :libgmp), Cvoid, (mpq_t, MPZ.mpz_t), zq, x) + return sync_rational!(zq) +end + +for (op, T) in ((:set, Rational{BigInt}), (:set_z, BigInt)) + op! = Symbol(op, :!) + @eval $op(a::$T) = $op!(unsafe_rational(BigInt(), BigInt()), a) +end + +# note that rationals returned from set_ui and set_si are not checked, +# set_ui(0, 0) will return 0//0 without errors, just like unsafe_rational +for (op, T1, T2) in ((:set_ui, Culong, Culong), (:set_si, Clong, Culong)) + op! = Symbol(op, :!) + @eval begin + function $op!(z::Rational{BigInt}, a, b) + zq = _MPQ(z) + ccall($(gmpq(op)), Cvoid, (mpq_t, $T1, $T2), zq, a, b) + return sync_rational!(zq) + end + $op(a, b) = $op!(unsafe_rational(BigInt(), BigInt()), a, b) + end +end + +# define add, sub, mul, div, and their inplace versions +function add!(z::Rational{BigInt}, x::Rational{BigInt}, y::Rational{BigInt}) + if iszero(x.den) || iszero(y.den) + if iszero(x.den) && iszero(y.den) && isneg(x.num) != isneg(y.num) + throw(DivideError()) + end + return set!(z, iszero(x.den) ? x : y) + end + zq = _MPQ(z) + ccall((:__gmpq_add, :libgmp), Cvoid, + (mpq_t,mpq_t,mpq_t), zq, _MPQ(x), _MPQ(y)) + return sync_rational!(zq) +end + +function sub!(z::Rational{BigInt}, x::Rational{BigInt}, y::Rational{BigInt}) + if iszero(x.den) || iszero(y.den) + if iszero(x.den) && iszero(y.den) && isneg(x.num) == isneg(y.num) + throw(DivideError()) + end + iszero(x.den) && return set!(z, x) + return set_si!(z, flipsign(-1, y.num), 0) + end + zq = _MPQ(z) + ccall((:__gmpq_sub, :libgmp), Cvoid, + (mpq_t,mpq_t,mpq_t), zq, _MPQ(x), _MPQ(y)) + return sync_rational!(zq) +end + +function mul!(z::Rational{BigInt}, x::Rational{BigInt}, y::Rational{BigInt}) + if iszero(x.den) || iszero(y.den) + if iszero(x.num) || iszero(y.num) + throw(DivideError()) + end + return set_si!(z, ifelse(xor(isneg(x.num), isneg(y.num)), -1, 1), 0) + end + zq = _MPQ(z) + ccall((:__gmpq_mul, :libgmp), Cvoid, + (mpq_t,mpq_t,mpq_t), zq, _MPQ(x), _MPQ(y)) + return sync_rational!(zq) +end + +function div!(z::Rational{BigInt}, x::Rational{BigInt}, y::Rational{BigInt}) + if iszero(x.den) + if iszero(y.den) + throw(DivideError()) + end + isneg(y.num) || return set!(z, x) + return set_si!(z, flipsign(-1, x.num), 0) + elseif iszero(y.den) + return set_si!(z, 0, 1) + elseif iszero(y.num) + if iszero(x.num) + throw(DivideError()) + end + return set_si!(z, flipsign(1, x.num), 0) + end + zq = _MPQ(z) + ccall((:__gmpq_div, :libgmp), Cvoid, + (mpq_t,mpq_t,mpq_t), zq, _MPQ(x), _MPQ(y)) + return sync_rational!(zq) +end + +for (fJ, fC) in ((:+, :add), (:-, :sub), (:*, :mul), (://, :div)) + fC! = Symbol(fC, :!) + @eval begin + ($fC!)(x::Rational{BigInt}, y::Rational{BigInt}) = $fC!(x, x, y) + (Base.$fJ)(x::Rational{BigInt}, y::Rational{BigInt}) = $fC!(unsafe_rational(BigInt(), BigInt()), x, y) end - y = MPZ.set(x) - stackdict[x] = y - return y end +function Base.cmp(x::Rational{BigInt}, y::Rational{BigInt}) + Int(ccall((:__gmpq_cmp, :libgmp), Cint, (mpq_t, mpq_t), _MPQ(x), _MPQ(y))) +end + +end # MPQ module + end # module diff --git a/base/hashing.jl b/base/hashing.jl index f40ccb50f0f5bd..746017f978dcb0 100644 --- a/base/hashing.jl +++ b/base/hashing.jl @@ -3,17 +3,19 @@ ## hashing a single value ## """ - hash(x[, h::UInt]) + hash(x[, h::UInt]) -> UInt Compute an integer hash code such that `isequal(x,y)` implies `hash(x)==hash(y)`. The optional second argument `h` is a hash code to be mixed with the result. New types should implement the 2-argument form, typically by calling the 2-argument `hash` method recursively in order to mix hashes of the contents with each other (and with `h`). -Typically, any type that implements `hash` should also implement its own `==` (hence -`isequal`) to guarantee the property mentioned above. Types supporting subtraction +Typically, any type that implements `hash` should also implement its own [`==`](@ref) (hence +[`isequal`](@ref)) to guarantee the property mentioned above. Types supporting subtraction (operator `-`) should also implement [`widen`](@ref), which is required to hash values inside heterogeneous arrays. + +See also: [`objectid`](@ref), [`Dict`](@ref), [`Set`](@ref). """ hash(x::Any) = hash(x, zero(UInt)) hash(w::WeakRef, h::UInt) = hash(w.value, h) @@ -22,6 +24,8 @@ hash(w::WeakRef, h::UInt) = hash(w.value, h) hash(@nospecialize(x), h::UInt) = hash_uint(3h - objectid(x)) +hash(x::Symbol) = objectid(x) + ## core data hashing functions ## function hash_64_64(n::UInt64) @@ -66,6 +70,23 @@ else hash_uint(x::UInt) = hash_32_32(x) end +## efficient value-based hashing of integers ## + +hash(x::Int64, h::UInt) = hash_uint64(bitcast(UInt64, x)) - 3h +hash(x::UInt64, h::UInt) = hash_uint64(x) - 3h +hash(x::Union{Bool,Int8,UInt8,Int16,UInt16,Int32,UInt32}, h::UInt) = hash(Int64(x), h) + +function hash_integer(n::Integer, h::UInt) + h ⊻= hash_uint((n % UInt) ⊻ h) + n = abs(n) + n >>>= sizeof(UInt) << 3 + while n != 0 + h ⊻= hash_uint((n % UInt) ⊻ h) + n >>>= sizeof(UInt) << 3 + end + return h +end + ## symbol & expression hashing ## if UInt === UInt64 diff --git a/base/hashing2.jl b/base/hashing2.jl deleted file mode 100644 index f7ea3838aa0969..00000000000000 --- a/base/hashing2.jl +++ /dev/null @@ -1,232 +0,0 @@ -# This file is a part of Julia. License is MIT: https://julialang.org/license - -## efficient value-based hashing of integers ## - -function hash_integer(n::Integer, h::UInt) - h ⊻= hash_uint((n % UInt) ⊻ h) - n = abs(n) - n >>>= sizeof(UInt) << 3 - while n != 0 - h ⊻= hash_uint((n % UInt) ⊻ h) - n >>>= sizeof(UInt) << 3 - end - return h -end - -# this condition is true most (all?) of the time, and in this case we can define -# an optimized version of the above hash_integer(::Integer, ::UInt) method for BigInt -if GMP.Limb === UInt - # used e.g. for Rational{BigInt} - function hash_integer(n::BigInt, h::UInt) - GC.@preserve n begin - s = n.size - s == 0 && return hash_integer(0, h) - p = convert(Ptr{UInt}, n.d) - b = unsafe_load(p) - h ⊻= hash_uint(ifelse(s < 0, -b, b) ⊻ h) - for k = 2:abs(s) - h ⊻= hash_uint(unsafe_load(p, k) ⊻ h) - end - return h - end - end -end - -## generic hashing for rational values ## - -function hash(x::Real, h::UInt) - # decompose x as num*2^pow/den - num, pow, den = decompose(x) - - # handle special values - num == 0 && den == 0 && return hash(NaN, h) - num == 0 && return hash(ifelse(den > 0, 0.0, -0.0), h) - den == 0 && return hash(ifelse(num > 0, Inf, -Inf), h) - - # normalize decomposition - if den < 0 - num = -num - den = -den - end - z = trailing_zeros(num) - if z != 0 - num >>= z - pow += z - end - z = trailing_zeros(den) - if z != 0 - den >>= z - pow -= z - end - - # handle values representable as Int64, UInt64, Float64 - if den == 1 - left = ndigits0z(num,2) + pow - right = trailing_zeros(num) + pow - if -1074 <= right - if 0 <= right && left <= 64 - left <= 63 && return hash(Int64(num) << Int(pow), h) - signbit(num) == signbit(den) && return hash(UInt64(num) << Int(pow), h) - end # typemin(Int64) handled by Float64 case - left <= 1024 && left - right <= 53 && return hash(ldexp(Float64(num),pow), h) - end - end - - # handle generic rational values - h = hash_integer(den, h) - h = hash_integer(pow, h) - h = hash_integer(num, h) - return h -end - -## streamlined hashing for BigInt, by avoiding allocation from shifts ## - -if GMP.Limb === UInt - _divLimb(n) = UInt === UInt64 ? n >>> 6 : n >>> 5 - _modLimb(n) = UInt === UInt64 ? n & 63 : n & 31 - - function hash(x::BigInt, h::UInt) - GC.@preserve x begin - sz = x.size - sz == 0 && return hash(0, h) - ptr = Ptr{UInt}(x.d) - if sz == 1 - return hash(unsafe_load(ptr), h) - elseif sz == -1 - limb = unsafe_load(ptr) - limb <= typemin(Int) % UInt && return hash(-(limb % Int), h) - end - pow = trailing_zeros(x) - nd = ndigits0z(x, 2) - idx = _divLimb(pow) + 1 - shift = _modLimb(pow) % UInt - upshift = GMP.BITS_PER_LIMB - shift - asz = abs(sz) - if shift == 0 - limb = unsafe_load(ptr, idx) - else - limb1 = unsafe_load(ptr, idx) - limb2 = idx < asz ? unsafe_load(ptr, idx+1) : UInt(0) - limb = limb2 << upshift | limb1 >> shift - end - if nd <= 1024 && nd - pow <= 53 - return hash(ldexp(flipsign(Float64(limb), sz), pow), h) - end - h = hash_integer(1, h) - h = hash_integer(pow, h) - h ⊻= hash_uint(flipsign(limb, sz) ⊻ h) - for idx = idx+1:asz - if shift == 0 - limb = unsafe_load(ptr, idx) - else - limb1 = limb2 - if idx == asz - limb = limb1 >> shift - limb == 0 && break # don't hash leading zeros - else - limb2 = unsafe_load(ptr, idx+1) - limb = limb2 << upshift | limb1 >> shift - end - end - h ⊻= hash_uint(limb ⊻ h) - end - return h - end - end -end - -#= -`decompose(x)`: non-canonical decomposition of rational values as `num*2^pow/den`. - -The decompose function is the point where rational-valued numeric types that support -hashing hook into the hashing protocol. `decompose(x)` should return three integer -values `num, pow, den`, such that the value of `x` is mathematically equal to - - num*2^pow/den - -The decomposition need not be canonical in the sense that it just needs to be *some* -way to express `x` in this form, not any particular way – with the restriction that -`num` and `den` may not share any odd common factors. They may, however, have powers -of two in common – the generic hashing code will normalize those as necessary. - -Special values: - - - `x` is zero: `num` should be zero and `den` should have the same sign as `x` - - `x` is infinite: `den` should be zero and `num` should have the same sign as `x` - - `x` is not a number: `num` and `den` should both be zero -=# - -decompose(x::Integer) = x, 0, 1 -decompose(x::Rational) = numerator(x), 0, denominator(x) - -function decompose(x::Float16)::NTuple{3,Int} - isnan(x) && return 0, 0, 0 - isinf(x) && return ifelse(x < 0, -1, 1), 0, 0 - n = reinterpret(UInt16, x) - s = (n & 0x03ff) % Int16 - e = ((n & 0x7c00) >> 10) % Int - s |= Int16(e != 0) << 10 - d = ifelse(signbit(x), -1, 1) - s, e - 25 + (e == 0), d -end - -function decompose(x::Float32)::NTuple{3,Int} - isnan(x) && return 0, 0, 0 - isinf(x) && return ifelse(x < 0, -1, 1), 0, 0 - n = reinterpret(UInt32, x) - s = (n & 0x007fffff) % Int32 - e = ((n & 0x7f800000) >> 23) % Int - s |= Int32(e != 0) << 23 - d = ifelse(signbit(x), -1, 1) - s, e - 150 + (e == 0), d -end - -function decompose(x::Float64)::Tuple{Int64, Int, Int} - isnan(x) && return 0, 0, 0 - isinf(x) && return ifelse(x < 0, -1, 1), 0, 0 - n = reinterpret(UInt64, x) - s = (n & 0x000fffffffffffff) % Int64 - e = ((n & 0x7ff0000000000000) >> 52) % Int - s |= Int64(e != 0) << 52 - d = ifelse(signbit(x), -1, 1) - s, e - 1075 + (e == 0), d -end - -function decompose(x::BigFloat)::Tuple{BigInt, Int, Int} - isnan(x) && return 0, 0, 0 - isinf(x) && return x.sign, 0, 0 - x == 0 && return 0, 0, x.sign - s = BigInt() - s.size = cld(x.prec, 8*sizeof(GMP.Limb)) # limbs - b = s.size * sizeof(GMP.Limb) # bytes - ccall((:__gmpz_realloc2, :libgmp), Cvoid, (Ref{BigInt}, Culong), s, 8b) # bits - ccall(:memcpy, Ptr{Cvoid}, (Ptr{Cvoid}, Ptr{Cvoid}, Csize_t), s.d, x.d, b) # bytes - s, x.exp - 8b, x.sign -end - -## streamlined hashing for smallish rational types ## - -function hash(x::Rational{<:BitInteger64}, h::UInt) - num, den = Base.numerator(x), Base.denominator(x) - den == 1 && return hash(num, h) - den == 0 && return hash(ifelse(num > 0, Inf, -Inf), h) - if isodd(den) - pow = trailing_zeros(num) - num >>= pow - else - pow = trailing_zeros(den) - den >>= pow - pow = -pow - if den == 1 && abs(num) < 9007199254740992 - return hash(ldexp(Float64(num),pow),h) - end - end - h = hash_integer(den, h) - h = hash_integer(pow, h) - h = hash_integer(num, h) - return h -end - -## hashing Float16s ## - -hash(x::Float16, h::UInt) = hash(Float64(x), h) diff --git a/base/iddict.jl b/base/iddict.jl index 3ea3a01f6244b0..7247a85c9afc80 100644 --- a/base/iddict.jl +++ b/base/iddict.jl @@ -1,10 +1,27 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + """ IdDict([itr]) -`IdDict{K,V}()` constructs a hash table using object-id as hash and +`IdDict{K,V}()` constructs a hash table using [`objectid`](@ref) as hash and `===` as equality with keys of type `K` and values of type `V`. -See [`Dict`](@ref) for further help. +See [`Dict`](@ref) for further help. In the example below, The `Dict` +keys are all `isequal` and therefore get hashed the same, so they get overwritten. +The `IdDict` hashes by object-id, and thus preserves the 3 different keys. + +# Examples +```julia-repl +julia> Dict(true => "yes", 1 => "no", 1.0 => "maybe") +Dict{Real, String} with 1 entry: + 1.0 => "maybe" + +julia> IdDict(true => "yes", 1 => "no", 1.0 => "maybe") +IdDict{Any, String} with 3 entries: + true => "yes" + 1.0 => "maybe" + 1 => "no" +``` """ mutable struct IdDict{K,V} <: AbstractDict{K,V} ht::Vector{Any} diff --git a/base/idset.jl b/base/idset.jl index cec8ed96caff8d..0a4d4275b42315 100644 --- a/base/idset.jl +++ b/base/idset.jl @@ -1,3 +1,5 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + # Like Set, but using IdDict mutable struct IdSet{T} <: AbstractSet{T} dict::IdDict{T,Nothing} @@ -10,6 +12,7 @@ IdSet{T}(itr) where {T} = union!(IdSet{T}(), itr) IdSet() = IdSet{Any}() copymutable(s::IdSet) = typeof(s)(s) +emptymutable(s::IdSet{T}, ::Type{U}=T) where {T,U} = IdSet{U}() copy(s::IdSet) = typeof(s)(s) isempty(s::IdSet) = isempty(s.dict) diff --git a/base/indices.jl b/base/indices.jl index d337d7979b20af..8cea043569ae62 100644 --- a/base/indices.jl +++ b/base/indices.jl @@ -23,11 +23,11 @@ A linear indexing style uses one integer index to describe the position in the a (even if it's a multidimensional array) and column-major ordering is used to efficiently access the elements. This means that requesting [`eachindex`](@ref) from an array that is `IndexLinear` will return -a simple one-dimensional range, even if it is multidimensional. +a simple one-dimensional range, even if it is multidimensional. A custom array that reports its `IndexStyle` as `IndexLinear` only needs to implement indexing (and indexed assignment) with a single `Int` index; -all other indexing expressions — including multidimensional accesses — will +all other indexing expressions — including multidimensional accesses — will be recomputed to the linear index. For example, if `A` were a `2×3` custom matrix with linear indexing, and we referenced `A[1, 3]`, this would be recomputed to the equivalent linear index and call `A[5]` since `2*1 + 3 = 5`. @@ -50,13 +50,13 @@ a range of [`CartesianIndices`](@ref). A `N`-dimensional custom array that reports its `IndexStyle` as `IndexCartesian` needs to implement indexing (and indexed assignment) with exactly `N` `Int` indices; -all other indexing expressions — including linear indexing — will +all other indexing expressions — including linear indexing — will be recomputed to the equivalent Cartesian location. For example, if `A` were a `2×3` custom matrix with cartesian indexing, and we referenced `A[5]`, this would be recomputed to the equivalent Cartesian index and call `A[1, 3]` since `5 = 2*1 + 3`. It is significantly more expensive to compute Cartesian indices from a linear index than it is -to go the other way. The former operation requires division — a very costly operation — whereas +to go the other way. The former operation requires division — a very costly operation — whereas the latter only uses multiplication and addition and is essentially free. This asymmetry means it is far more costly to use linear indexing with an `IndexCartesian` array than it is to use Cartesian indexing with an `IndexLinear` array. @@ -239,6 +239,9 @@ setindex_shape_check(X::AbstractArray) = setindex_shape_check(X::AbstractArray, i::Integer) = (length(X)==i || throw_setindex_mismatch(X, (i,))) +setindex_shape_check(X::AbstractArray{<:Any, 0}, i::Integer...) = + (length(X) == prod(i) || throw_setindex_mismatch(X, i)) + setindex_shape_check(X::AbstractArray{<:Any,1}, i::Integer) = (length(X)==i || throw_setindex_mismatch(X, (i,))) @@ -256,7 +259,7 @@ function setindex_shape_check(X::AbstractArray{<:Any,2}, i::Integer, j::Integer) end setindex_shape_check(::Any...) = - throw(ArgumentError("indexed assignment with a single value to many locations is not supported; perhaps use broadcasting `.=` instead?")) + throw(ArgumentError("indexed assignment with a single value to possibly many locations is not supported; perhaps use broadcasting `.=` instead?")) # convert to a supported index type (array or Int) """ @@ -318,16 +321,16 @@ which they index. To support those cases, `to_indices(A, I)` calls given tuple of indices and the dimensional indices of `A` in tandem. As such, not all index types are guaranteed to propagate to `Base.to_index`. """ -to_indices(A, I::Tuple) = (@_inline_meta; to_indices(A, axes(A), I)) -to_indices(A, I::Tuple{Any}) = (@_inline_meta; to_indices(A, (eachindex(IndexLinear(), A),), I)) +to_indices(A, I::Tuple) = (@inline; to_indices(A, axes(A), I)) +to_indices(A, I::Tuple{Any}) = (@inline; to_indices(A, (eachindex(IndexLinear(), A),), I)) # In simple cases, we know that we don't need to use axes(A), optimize those. # Having this here avoids invalidations from multidimensional.jl: to_indices(A, I::Tuple{Vararg{Union{Integer, CartesianIndex}}}) to_indices(A, I::Tuple{}) = () to_indices(A, I::Tuple{Vararg{Int}}) = I -to_indices(A, I::Tuple{Vararg{Integer}}) = (@_inline_meta; to_indices(A, (), I)) +to_indices(A, I::Tuple{Vararg{Integer}}) = (@inline; to_indices(A, (), I)) to_indices(A, inds, ::Tuple{}) = () to_indices(A, inds, I::Tuple{Any, Vararg{Any}}) = - (@_inline_meta; (to_index(A, I[1]), to_indices(A, _maybetail(inds), tail(I))...)) + (@inline; (to_index(A, I[1]), to_indices(A, _maybetail(inds), tail(I))...)) _maybetail(::Tuple{}) = () _maybetail(t::Tuple) = tail(t) @@ -348,25 +351,23 @@ struct Slice{T<:AbstractUnitRange} <: AbstractUnitRange{Int} indices::T end Slice(S::Slice) = S +Slice{T}(S::Slice) where {T<:AbstractUnitRange} = Slice{T}(T(S.indices)) + axes(S::Slice) = (IdentityUnitRange(S.indices),) -unsafe_indices(S::Slice) = (IdentityUnitRange(S.indices),) axes1(S::Slice) = IdentityUnitRange(S.indices) axes(S::Slice{<:OneTo}) = (S.indices,) -unsafe_indices(S::Slice{<:OneTo}) = (S.indices,) axes1(S::Slice{<:OneTo}) = S.indices first(S::Slice) = first(S.indices) last(S::Slice) = last(S.indices) size(S::Slice) = (length(S.indices),) length(S::Slice) = length(S.indices) -unsafe_length(S::Slice) = unsafe_length(S.indices) -getindex(S::Slice, i::Int) = (@_inline_meta; @boundscheck checkbounds(S, i); i) -getindex(S::Slice, i::AbstractUnitRange{<:Integer}) = (@_inline_meta; @boundscheck checkbounds(S, i); i) -getindex(S::Slice, i::StepRange{<:Integer}) = (@_inline_meta; @boundscheck checkbounds(S, i); i) +getindex(S::Slice, i::Int) = (@inline; @boundscheck checkbounds(S, i); i) +getindex(S::Slice, i::AbstractUnitRange{<:Integer}) = (@inline; @boundscheck checkbounds(S, i); i) +getindex(S::Slice, i::StepRange{<:Integer}) = (@inline; @boundscheck checkbounds(S, i); i) show(io::IO, r::Slice) = print(io, "Base.Slice(", r.indices, ")") iterate(S::Slice, s...) = iterate(S.indices, s...) - """ IdentityUnitRange(range::AbstractUnitRange) @@ -378,25 +379,28 @@ struct IdentityUnitRange{T<:AbstractUnitRange} <: AbstractUnitRange{Int} indices::T end IdentityUnitRange(S::IdentityUnitRange) = S +IdentityUnitRange{T}(S::IdentityUnitRange) where {T<:AbstractUnitRange} = IdentityUnitRange{T}(T(S.indices)) + # IdentityUnitRanges are offset and thus have offset axes, so they are their own axes axes(S::IdentityUnitRange) = (S,) -unsafe_indices(S::IdentityUnitRange) = (S,) axes1(S::IdentityUnitRange) = S axes(S::IdentityUnitRange{<:OneTo}) = (S.indices,) -unsafe_indices(S::IdentityUnitRange{<:OneTo}) = (S.indices,) axes1(S::IdentityUnitRange{<:OneTo}) = S.indices first(S::IdentityUnitRange) = first(S.indices) last(S::IdentityUnitRange) = last(S.indices) size(S::IdentityUnitRange) = (length(S.indices),) length(S::IdentityUnitRange) = length(S.indices) -unsafe_length(S::IdentityUnitRange) = unsafe_length(S.indices) -getindex(S::IdentityUnitRange, i::Int) = (@_inline_meta; @boundscheck checkbounds(S, i); i) -getindex(S::IdentityUnitRange, i::AbstractUnitRange{<:Integer}) = (@_inline_meta; @boundscheck checkbounds(S, i); i) -getindex(S::IdentityUnitRange, i::StepRange{<:Integer}) = (@_inline_meta; @boundscheck checkbounds(S, i); i) +getindex(S::IdentityUnitRange, i::Int) = (@inline; @boundscheck checkbounds(S, i); i) +getindex(S::IdentityUnitRange, i::AbstractUnitRange{<:Integer}) = (@inline; @boundscheck checkbounds(S, i); i) +getindex(S::IdentityUnitRange, i::StepRange{<:Integer}) = (@inline; @boundscheck checkbounds(S, i); i) show(io::IO, r::IdentityUnitRange) = print(io, "Base.IdentityUnitRange(", r.indices, ")") iterate(S::IdentityUnitRange, s...) = iterate(S.indices, s...) +# For OneTo, the values and indices of the values are identical, so this may be defined in Base. +# In general such an indexing operation would produce offset ranges +getindex(S::OneTo, I::IdentityUnitRange{<:AbstractUnitRange{<:Integer}}) = (@inline; @boundscheck checkbounds(S, I); I) + """ LinearIndices(A::AbstractArray) @@ -447,37 +451,42 @@ julia> linear[1,2] struct LinearIndices{N,R<:NTuple{N,AbstractUnitRange{Int}}} <: AbstractArray{Int,N} indices::R end +convert(::Type{LinearIndices{N,R}}, inds::LinearIndices{N}) where {N,R<:NTuple{N,AbstractUnitRange{Int}}} = + LinearIndices{N,R}(convert(R, inds.indices)) LinearIndices(::Tuple{}) = LinearIndices{0,typeof(())}(()) LinearIndices(inds::NTuple{N,AbstractUnitRange{<:Integer}}) where {N} = LinearIndices(map(r->convert(AbstractUnitRange{Int}, r), inds)) -LinearIndices(sz::NTuple{N,<:Integer}) where {N} = LinearIndices(map(Base.OneTo, sz)) LinearIndices(inds::NTuple{N,Union{<:Integer,AbstractUnitRange{<:Integer}}}) where {N} = - LinearIndices(map(i->first(i):last(i), inds)) + LinearIndices(map(_convert2ind, inds)) LinearIndices(A::Union{AbstractArray,SimpleVector}) = LinearIndices(axes(A)) -promote_rule(::Type{LinearIndices{N,R1}}, ::Type{LinearIndices{N,R2}}) where {N,R1,R2} = - LinearIndices{N,indices_promote_type(R1,R2)} +_convert2ind(i::Integer) = Base.OneTo(i) +_convert2ind(ind::AbstractUnitRange) = first(ind):last(ind) function indices_promote_type(::Type{Tuple{R1,Vararg{R1,N}}}, ::Type{Tuple{R2,Vararg{R2,N}}}) where {R1,R2,N} R = promote_type(R1, R2) - Tuple{R,Vararg{R,N}} + return Tuple{R, Vararg{R, N}} end -convert(::Type{LinearIndices{N,R}}, inds::LinearIndices{N}) where {N,R} = - LinearIndices(convert(R, inds.indices)) +promote_rule(::Type{LinearIndices{N,R1}}, ::Type{LinearIndices{N,R2}}) where {N,R1,R2} = + LinearIndices{N,indices_promote_type(R1,R2)} +promote_rule(a::Type{Slice{T1}}, b::Type{Slice{T2}}) where {T1,T2} = + el_same(promote_type(T1, T2), a, b) +promote_rule(a::Type{IdentityUnitRange{T1}}, b::Type{IdentityUnitRange{T2}}) where {T1,T2} = + el_same(promote_type(T1, T2), a, b) # AbstractArray implementation IndexStyle(::Type{<:LinearIndices}) = IndexLinear() axes(iter::LinearIndices) = map(axes1, iter.indices) -size(iter::LinearIndices) = map(unsafe_length, iter.indices) +size(iter::LinearIndices) = map(length, iter.indices) function getindex(iter::LinearIndices, i::Int) - @_inline_meta + @inline @boundscheck checkbounds(iter, i) i end function getindex(iter::LinearIndices, i::AbstractRange{<:Integer}) - @_inline_meta + @inline @boundscheck checkbounds(iter, i) @inbounds isa(iter, LinearIndices{1}) ? iter.indices[1][i] : (first(iter):last(iter))[i] end @@ -488,6 +497,6 @@ iterate(iter::LinearIndices, i=1) = i > length(iter) ? nothing : (i, i+1) # Needed since firstindex and lastindex are defined in terms of LinearIndices first(iter::LinearIndices) = 1 -first(iter::LinearIndices{1}) = (@_inline_meta; first(axes1(iter.indices[1]))) -last(iter::LinearIndices) = (@_inline_meta; length(iter)) -last(iter::LinearIndices{1}) = (@_inline_meta; last(axes1(iter.indices[1]))) +first(iter::LinearIndices{1}) = (@inline; first(axes1(iter.indices[1]))) +last(iter::LinearIndices) = (@inline; length(iter)) +last(iter::LinearIndices{1}) = (@inline; last(axes1(iter.indices[1]))) diff --git a/base/initdefs.jl b/base/initdefs.jl index 9a6c1a7a707041..4106ef4eb7777d 100644 --- a/base/initdefs.jl +++ b/base/initdefs.jl @@ -81,9 +81,8 @@ Here is an overview of some of the subdirectories that may exist in a depot: * `packages`: Contains packages, some of which were explicitly installed and some which are implicit dependencies. Maintained by `Pkg.jl`. * `registries`: Contains package registries. By default only `General`. Maintained by `Pkg.jl`. -See also: -[`JULIA_DEPOT_PATH`](@ref JULIA_DEPOT_PATH), and -[Code Loading](@ref Code-Loading). +See also [`JULIA_DEPOT_PATH`](@ref JULIA_DEPOT_PATH), and +[Code Loading](@ref code-loading). """ const DEPOT_PATH = String[] @@ -101,7 +100,7 @@ function init_depot_path() if haskey(ENV, "JULIA_DEPOT_PATH") str = ENV["JULIA_DEPOT_PATH"] isempty(str) && return - for path in split(str, Sys.iswindows() ? ';' : ':') + for path in eachsplit(str, Sys.iswindows() ? ';' : ':') if isempty(path) append_default_depot_path!(DEPOT_PATH) else @@ -161,16 +160,20 @@ have special meanings: The fully expanded value of `LOAD_PATH` that is searched for projects and packages can be seen by calling the `Base.load_path()` function. -See also: +See also [`JULIA_LOAD_PATH`](@ref JULIA_LOAD_PATH), [`JULIA_PROJECT`](@ref JULIA_PROJECT), [`JULIA_DEPOT_PATH`](@ref JULIA_DEPOT_PATH), and -[Code Loading](@ref Code-Loading). +[Code Loading](@ref code-loading). """ const LOAD_PATH = copy(DEFAULT_LOAD_PATH) # HOME_PROJECT is no longer used, here just to avoid breaking things const HOME_PROJECT = Ref{Union{String,Nothing}}(nothing) -const ACTIVE_PROJECT = Ref{Union{String,Nothing}}(nothing) +const ACTIVE_PROJECT = Ref{Union{String,Nothing}}(nothing) # Modify this only via `Base.set_active_project(proj)` +## Watchers for when the active project changes (e.g., Revise) +# Each should be a thunk, i.e., `f()`. To determine the current active project, +# the thunk can query `Base.active_project()`. +const active_project_callbacks = [] function current_project(dir::AbstractString) # look for project file in current dir and parents @@ -199,7 +202,7 @@ end function parse_load_path(str::String) envs = String[] isempty(str) && return envs - for env in split(str, Sys.iswindows() ? ';' : ':') + for env in eachsplit(str, Sys.iswindows() ? ';' : ':') if isempty(env) for env′ in DEFAULT_LOAD_PATH env′ in envs || push!(envs, env′) @@ -232,10 +235,11 @@ function init_active_project() project = (JLOptions().project != C_NULL ? unsafe_string(Base.JLOptions().project) : get(ENV, "JULIA_PROJECT", nothing)) - ACTIVE_PROJECT[] = + set_active_project( project === nothing ? nothing : project == "" ? nothing : - project == "@." ? current_project() : abspath(expanduser(project)) + startswith(project, "@") ? load_path_expand(project) : abspath(expanduser(project)) + ) end ## load path expansion: turn LOAD_PATH entries into concrete paths ## @@ -247,7 +251,7 @@ function load_path_expand(env::AbstractString)::Union{String, Nothing} # if you put a `@` in LOAD_PATH manually, it's expanded late env == "@" && return active_project(false) env == "@." && return current_project() - env == "@stdlib" && return Sys.STDLIB::String + env == "@stdlib" && return Sys.STDLIB env = replace(env, '#' => VERSION.major, count=1) env = replace(env, '#' => VERSION.minor, count=1) env = replace(env, '#' => VERSION.patch, count=1) @@ -278,6 +282,11 @@ function load_path_expand(env::AbstractString)::Union{String, Nothing} end load_path_expand(::Nothing) = nothing +""" + active_project() + +Return the path of the active `Project.toml` file. See also [`Base.set_active_project`](@ref). +""" function active_project(search_load_path::Bool=true) for project in (ACTIVE_PROJECT[],) project == "@" && continue @@ -302,7 +311,32 @@ function active_project(search_load_path::Bool=true) end end +""" + set_active_project(projfile::Union{AbstractString,Nothing}) + +Set the active `Project.toml` file to `projfile`. See also [`Base.active_project`](@ref). +""" +function set_active_project(projfile::Union{AbstractString,Nothing}) + ACTIVE_PROJECT[] = projfile + for f in active_project_callbacks + try + Base.invokelatest(f) + catch + @error "active project callback $f failed" maxlog=1 + end + end +end + + +""" + load_path() + +Return the fully expanded value of [`LOAD_PATH`](@ref) that is searched for projects and +packages. +""" function load_path() + cache = LOADING_CACHE[] + cache !== nothing && return cache.load_path paths = String[] for env in LOAD_PATH path = load_path_expand(env) diff --git a/base/int.jl b/base/int.jl index b3acd29c6492fd..41e53e990be5b3 100644 --- a/base/int.jl +++ b/base/int.jl @@ -87,15 +87,22 @@ signed(::Type{T}) where {T<:Signed} = T (+)(x::T, y::T) where {T<:BitInteger} = add_int(x, y) (*)(x::T, y::T) where {T<:BitInteger} = mul_int(x, y) +negate(x) = -x +negate(x::Unsigned) = -convert(Signed, x) +#widenegate(x) = -convert(widen(signed(typeof(x))), x) + inv(x::Integer) = float(one(x)) / float(x) (/)(x::T, y::T) where {T<:Integer} = float(x) / float(y) # skip promotion for system integer types (/)(x::BitInteger, y::BitInteger) = float(x) / float(y) """ - isodd(x::Integer) -> Bool + isodd(x::Number) -> Bool + +Return `true` if `x` is an odd integer (that is, an integer not divisible by 2), and `false` otherwise. -Return `true` if `x` is odd (that is, not divisible by 2), and `false` otherwise. +!!! compat "Julia 1.7" + Non-`Integer` arguments require Julia 1.7 or later. # Examples ```jldoctest @@ -106,12 +113,16 @@ julia> isodd(10) false ``` """ -isodd(n::Integer) = rem(n, 2) != 0 +isodd(n::Number) = isreal(n) && isodd(real(n)) +isodd(n::Real) = isinteger(n) && !iszero(rem(Integer(n), 2)) """ - iseven(x::Integer) -> Bool + iseven(x::Number) -> Bool + +Return `true` if `x` is an even integer (that is, an integer divisible by 2), and `false` otherwise. -Return `true` if `x` is even (that is, divisible by 2), and `false` otherwise. +!!! compat "Julia 1.7" + Non-`Integer` arguments require Julia 1.7 or later. # Examples ```jldoctest @@ -122,7 +133,8 @@ julia> iseven(10) true ``` """ -iseven(n::Integer) = !isodd(n) +iseven(n::Number) = isreal(n) && iseven(real(n)) +iseven(n::Real) = isinteger(n) && iszero(rem(Integer(n), 2)) signbit(x::Integer) = x < 0 signbit(x::Unsigned) = false @@ -152,6 +164,8 @@ when `abs` is applied to the minimum representable value of a signed integer. That is, when `x == typemin(typeof(x))`, `abs(x) == x < 0`, not `-x` as might be expected. +See also: [`abs2`](@ref), [`unsigned`](@ref), [`sign`](@ref). + # Examples ```jldoctest julia> abs(-3) @@ -176,12 +190,17 @@ abs(x::Signed) = flipsign(x,x) Convert a number to an unsigned integer. If the argument is signed, it is reinterpreted as unsigned without checking for negative values. + +See also: [`signed`](@ref), [`sign`](@ref), [`signbit`](@ref). + # Examples ```jldoctest julia> unsigned(-2) 0xfffffffffffffffe + julia> unsigned(2) 0x0000000000000002 + julia> signed(unsigned(-2)) -2 ``` @@ -194,6 +213,8 @@ unsigned(x::BitSigned) = reinterpret(typeof(convert(Unsigned, zero(x))), x) Convert a number to a signed integer. If the argument is unsigned, it is reinterpreted as signed without checking for overflow. + +See also: [`unsigned`](@ref), [`sign`](@ref), [`signbit`](@ref). """ signed(x) = x % typeof(convert(Signed, zero(x))) signed(x::BitUnsigned) = reinterpret(typeof(convert(Signed, zero(x))), x) @@ -231,6 +252,8 @@ exceptions, see note below). type, and so rounding error may occur. In particular, if the exact result is very close to `y`, then it may be rounded to `y`. +See also: [`rem`](@ref), [`div`](@ref), [`fld`](@ref), [`mod1`](@ref), [`invmod`](@ref). + ```jldoctest julia> mod(8, 3) 2 @@ -246,6 +269,10 @@ julia> mod(eps(), 3) julia> mod(-eps(), 3) 3.0 + +julia> mod.(-5:5, 3)' +1×11 adjoint(::Vector{Int64}) with eltype Int64: + 1 2 0 1 2 0 1 2 0 1 2 ``` """ function mod(x::T, y::T) where T<:Integer @@ -270,6 +297,8 @@ rem(x::T, y::T) where {T<:BitUnsigned64} = checked_urem_int(x, y) Bitwise not. +See also: [`!`](@ref), [`&`](@ref), [`|`](@ref). + # Examples ```jldoctest julia> ~4 @@ -291,6 +320,8 @@ Bitwise and. Implements [three-valued logic](https://en.wikipedia.org/wiki/Three returning [`missing`](@ref) if one operand is `missing` and the other is `true`. Add parentheses for function application form: `(&)(x, y)`. +See also: [`|`](@ref), [`xor`](@ref), [`&&`](@ref). + # Examples ```jldoctest julia> 4 & 10 @@ -314,6 +345,8 @@ false Bitwise or. Implements [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic), returning [`missing`](@ref) if one operand is `missing` and the other is `false`. +See also: [`&`](@ref), [`xor`](@ref), [`||`](@ref). + # Examples ```jldoctest julia> 4 | 10 @@ -367,6 +400,9 @@ Number of ones in the binary representation of `x`. ```jldoctest julia> count_ones(7) 3 + +julia> count_ones(Int32(-1)) +32 ``` """ count_ones(x::BitInteger) = (ctpop_int(x) % Int)::Int @@ -406,6 +442,9 @@ Number of zeros in the binary representation of `x`. ```jldoctest julia> count_zeros(Int32(2 ^ 16 - 1)) 16 + +julia> count_zeros(-1) +0 ``` """ count_zeros(x::Integer) = count_ones(~x) @@ -496,6 +535,8 @@ A negative value of `k` will rotate to the right instead. !!! compat "Julia 1.5" This function requires Julia 1.5 or later. +See also: [`<<`](@ref), [`circshift`](@ref), [`BitArray`](@ref). + ```jldoctest julia> bitrotate(UInt8(114), 2) 0xc9 @@ -552,12 +593,26 @@ unsafe_trunc(::Type{T}, x::Integer) where {T<:Integer} = rem(x, T) trunc(x; sigdigits::Integer= [, base = 10]) `trunc(x)` returns the nearest integral value of the same type as `x` whose absolute value -is less than or equal to `x`. +is less than or equal to the absolute value of `x`. `trunc(T, x)` converts the result to type `T`, throwing an `InexactError` if the value is not representable. -`digits`, `sigdigits` and `base` work as for [`round`](@ref). +Keywords `digits`, `sigdigits` and `base` work as for [`round`](@ref). + +See also: [`%`](@ref rem), [`floor`](@ref), [`unsigned`](@ref), [`unsafe_trunc`](@ref). + +# Examples +```jldoctest +julia> trunc(2.22) +2.0 + +julia> trunc(-2.22, digits=1) +-2.2 + +julia> trunc(Int, -2.22) +-2 +``` """ function trunc end @@ -572,7 +627,7 @@ equal to `x`. `floor(T, x)` converts the result to type `T`, throwing an `InexactError` if the value is not representable. -`digits`, `sigdigits` and `base` work as for [`round`](@ref). +Keywords `digits`, `sigdigits` and `base` work as for [`round`](@ref). """ function floor end @@ -587,7 +642,7 @@ equal to `x`. `ceil(T, x)` converts the result to type `T`, throwing an `InexactError` if the value is not representable. -`digits`, `sigdigits` and `base` work as for [`round`](@ref). +Keywords `digits`, `sigdigits` and `base` work as for [`round`](@ref). """ function ceil end @@ -600,10 +655,19 @@ floor(::Type{T}, x::Integer) where {T<:Integer} = convert(T, x) """ @int128_str str - @int128_str(str) -`@int128_str` parses a string into a Int128 -Throws an `ArgumentError` if the string is not a valid integer +Parse `str` as an [`Int128`](@ref). +Throw an `ArgumentError` if the string is not a valid integer. + +# Examples +```jldoctest +julia> int128"123456789123" +123456789123 + +julia> int128"123456789123.4" +ERROR: LoadError: ArgumentError: invalid base 10 digit '.' in "123456789123.4" +[...] +``` """ macro int128_str(s) return parse(Int128, s) @@ -611,10 +675,19 @@ end """ @uint128_str str - @uint128_str(str) -`@uint128_str` parses a string into a UInt128 -Throws an `ArgumentError` if the string is not a valid integer +Parse `str` as an [`UInt128`](@ref). +Throw an `ArgumentError` if the string is not a valid integer. + +# Examples +``` +julia> uint128"123456789123" +0x00000000000000000000001cbe991a83 + +julia> uint128"-123456789123" +ERROR: LoadError: ArgumentError: invalid base 10 digit '-' in "-123456789123" +[...] +``` """ macro uint128_str(s) return parse(UInt128, s) @@ -622,7 +695,6 @@ end """ @big_str str - @big_str(str) Parse a string into a [`BigInt`](@ref) or [`BigFloat`](@ref), and throw an `ArgumentError` if the string is not a valid number. @@ -635,28 +707,37 @@ julia> big"123_456" julia> big"7891.5" 7891.5 + +julia> big"_" +ERROR: ArgumentError: invalid number format _ for BigInt or BigFloat +[...] ``` """ macro big_str(s) + message = "invalid number format $s for BigInt or BigFloat" + throw_error = :(throw(ArgumentError($message))) if '_' in s # remove _ in s[2:end-1] bf = IOBuffer(maxsize=lastindex(s)) - print(bf, s[1]) + c = s[1] + print(bf, c) + is_prev_underscore = (c == '_') + is_prev_dot = (c == '.') for c in SubString(s, 2, lastindex(s)-1) c != '_' && print(bf, c) + c == '_' && is_prev_dot && return throw_error + c == '.' && is_prev_underscore && return throw_error + is_prev_underscore = (c == '_') + is_prev_dot = (c == '.') end print(bf, s[end]) - seekstart(bf) - n = tryparse(BigInt, String(take!(bf))) - n === nothing || return n - else - n = tryparse(BigInt, s) - n === nothing || return n - n = tryparse(BigFloat, s) - n === nothing || return n + s = String(take!(bf)) end - message = "invalid number format $s for BigInt or BigFloat" - return :(throw(ArgumentError($message))) + n = tryparse(BigInt, s) + n === nothing || return n + n = tryparse(BigFloat, s) + n === nothing || return n + return throw_error end ## integer promotions ## @@ -700,6 +781,8 @@ function typemin end The highest value representable by the given (real) numeric `DataType`. +See also: [`floatmax`](@ref), [`typemin`](@ref), [`eps`](@ref). + # Examples ```jldoctest julia> typemax(Int8) @@ -707,6 +790,12 @@ julia> typemax(Int8) julia> typemax(UInt32) 0xffffffff + +julia> typemax(Float64) +Inf + +julia> floatmax(Float32) # largest finite floating point number +3.4028235f38 ``` """ function typemax end diff --git a/base/intfuncs.jl b/base/intfuncs.jl index 198f73191ecb8d..059091b8bf8b1b 100644 --- a/base/intfuncs.jl +++ b/base/intfuncs.jl @@ -3,9 +3,9 @@ ## number-theoretic functions ## """ - gcd(x,y) + gcd(x, y...) -Greatest common (positive) divisor (or zero if `x` and `y` are both zero). +Greatest common (positive) divisor (or zero if all arguments are zero). The arguments may be integer and rational numbers. !!! compat "Julia 1.4" @@ -13,26 +13,29 @@ The arguments may be integer and rational numbers. # Examples ```jldoctest -julia> gcd(6,9) +julia> gcd(6, 9) 3 -julia> gcd(6,-9) +julia> gcd(6, -9) 3 -julia> gcd(6,0) +julia> gcd(6, 0) 6 -julia> gcd(0,0) +julia> gcd(0, 0) 0 -julia> gcd(1//3,2//3) +julia> gcd(1//3, 2//3) 1//3 -julia> gcd(1//3,-2//3) +julia> gcd(1//3, -2//3) 1//3 -julia> gcd(1//3,2) +julia> gcd(1//3, 2) 1//3 + +julia> gcd(0, 0, 10, 15) +5 ``` """ function gcd(a::T, b::T) where T<:Integer @@ -44,11 +47,21 @@ function gcd(a::T, b::T) where T<:Integer checked_abs(a) end -# binary GCD (aka Stein's) algorithm -# about 1.7x (2.1x) faster for random Int64s (Int128s) function gcd(a::T, b::T) where T<:BitInteger a == 0 && return checked_abs(b) b == 0 && return checked_abs(a) + r = _gcd(a, b) + signbit(r) && __throw_gcd_overflow(a, b) + return r +end +@noinline __throw_gcd_overflow(a, b) = + throw(OverflowError(LazyString("gcd(", a, ", ", b, ") overflows"))) + +# binary GCD (aka Stein's) algorithm +# about 1.7x (2.1x) faster for random Int64s (Int128s) +# Unfortunately, we need to manually annotate this as `@assume_effects :terminates_locally` to work around #41694. +# Since this is used in the Rational constructor, constant folding is something we do care about here. +@assume_effects :terminates_locally function _gcd(a::T, b::T) where T<:BitInteger za = trailing_zeros(a) zb = trailing_zeros(b) k = min(za, zb) @@ -62,16 +75,13 @@ function gcd(a::T, b::T) where T<:BitInteger v >>= trailing_zeros(v) end r = u << k - # T(r) would throw InexactError; we want OverflowError instead - r > typemax(T) && __throw_gcd_overflow(a, b) - r % T + return r % T end -@noinline __throw_gcd_overflow(a, b) = throw(OverflowError("gcd($a, $b) overflows")) """ - lcm(x,y) + lcm(x, y...) -Least common (non-negative) multiple. +Least common (positive) multiple (or zero if any argument is zero). The arguments may be integer and rational numbers. !!! compat "Julia 1.4" @@ -79,30 +89,33 @@ The arguments may be integer and rational numbers. # Examples ```jldoctest -julia> lcm(2,3) +julia> lcm(2, 3) 6 -julia> lcm(-2,3) +julia> lcm(-2, 3) 6 -julia> lcm(0,3) +julia> lcm(0, 3) 0 -julia> lcm(0,0) +julia> lcm(0, 0) 0 -julia> lcm(1//3,2//3) +julia> lcm(1//3, 2//3) 2//3 -julia> lcm(1//3,-2//3) +julia> lcm(1//3, -2//3) 2//3 -julia> lcm(1//3,2) +julia> lcm(1//3, 2) 2//1 + +julia> lcm(1, 3, 5, 7) +105 ``` """ function lcm(a::T, b::T) where T<:Integer - # explicit a==0 test is to handle case of lcm(0,0) correctly + # explicit a==0 test is to handle case of lcm(0, 0) correctly # explicit b==0 test is to handle case of lcm(typemin(T),0) correctly if a == 0 || b == 0 return zero(a) @@ -111,8 +124,9 @@ function lcm(a::T, b::T) where T<:Integer end end -gcd(a::Union{Integer,Rational}) = a -lcm(a::Union{Integer,Rational}) = a +gcd(a::Integer) = checked_abs(a) +gcd(a::Rational) = checked_abs(a.num) // a.den +lcm(a::Union{Integer,Rational}) = gcd(a) gcd(a::Unsigned, b::Signed) = gcd(promote(a, abs(b))...) gcd(a::Signed, b::Unsigned) = gcd(promote(abs(a), b)...) gcd(a::Real, b::Real) = gcd(promote(a,b)...) @@ -128,7 +142,7 @@ lcm(abc::AbstractArray{<:Real}) = reduce(lcm, abc; init=one(eltype(abc))) function gcd(abc::AbstractArray{<:Integer}) a = zero(eltype(abc)) for b in abc - a = gcd(a,b) + a = gcd(a, b) if a == 1 return a end @@ -136,13 +150,13 @@ function gcd(abc::AbstractArray{<:Integer}) return a end -# return (gcd(a,b),x,y) such that ax+by == gcd(a,b) +# return (gcd(a, b), x, y) such that ax+by == gcd(a, b) """ - gcdx(x,y) + gcdx(a, b) -Computes the greatest common (positive) divisor of `x` and `y` and their Bézout +Computes the greatest common (positive) divisor of `a` and `b` and their Bézout coefficients, i.e. the integer coefficients `u` and `v` that satisfy -``ux+vy = d = gcd(x,y)``. ``gcdx(x,y)`` returns ``(d,u,v)``. +``ua+vb = d = gcd(a, b)``. ``gcdx(a, b)`` returns ``(d, u, v)``. The arguments may be integer and rational numbers. @@ -169,8 +183,8 @@ julia> gcdx(240, 46) their `typemax`, and the identity then holds only via the unsigned integers' modulo arithmetic. """ -function gcdx(a::U, b::V) where {U<:Integer, V<:Integer} - T = promote_type(U, V) +function gcdx(a::Integer, b::Integer) + T = promote_type(typeof(a), typeof(b)) # a0, b0 = a, b s0, s1 = oneunit(T), zero(T) t0, t1 = s1, s0 @@ -191,33 +205,43 @@ gcdx(a::T, b::T) where T<:Real = throw(MethodError(gcdx, (a,b))) # multiplicative inverse of n mod m, error if none """ - invmod(x,m) + invmod(n, m) -Take the inverse of `x` modulo `m`: `y` such that ``x y = 1 \\pmod m``, -with ``div(x,y) = 0``. This is undefined for ``m = 0``, or if -``gcd(x,m) \\neq 1``. +Take the inverse of `n` modulo `m`: `y` such that ``n y = 1 \\pmod m``, +and ``div(y,m) = 0``. This will throw an error if ``m = 0``, or if +``gcd(n,m) \\neq 1``. # Examples ```jldoctest -julia> invmod(2,5) +julia> invmod(2, 5) 3 -julia> invmod(2,3) +julia> invmod(2, 3) 2 -julia> invmod(5,6) +julia> invmod(5, 6) 5 ``` """ function invmod(n::Integer, m::Integer) + iszero(m) && throw(DomainError(m, "`m` must not be 0.")) + if n isa Signed && hastypemax(typeof(n)) + # work around inconsistencies in gcdx + # https://github.com/JuliaLang/julia/issues/33781 + T = promote_type(typeof(n), typeof(m)) + n == typemin(typeof(n)) && m == typeof(n)(-1) && return T(0) + n == typeof(n)(-1) && m == typemin(typeof(n)) && return T(-1) + end g, x, y = gcdx(n, m) g != 1 && throw(DomainError((n, m), "Greatest common divisor is $g.")) - m == 0 && throw(DomainError(m, "`m` must not be 0.")) # Note that m might be negative here. - # For unsigned T, x might be close to typemax; add m to force a wrap-around. - r = mod(x + m, m) - # The postcondition is: mod(r * n, m) == mod(T(1), m) && div(r, m) == 0 - r + if n isa Unsigned && hastypemax(typeof(n)) && x > typemax(n)>>1 + # x might have wrapped if it would have been negative + # adding back m forces a correction + x += m + end + # The postcondition is: mod(result * n, m) == mod(T(1), m) && div(result, m) == 0 + return mod(x, m) end # ^ for any x supporting * @@ -287,14 +311,15 @@ end const HWReal = Union{Int8,Int16,Int32,Int64,UInt8,UInt16,UInt32,UInt64,Float32,Float64} const HWNumber = Union{HWReal, Complex{<:HWReal}, Rational{<:HWReal}} -# Core.Compiler has complicated logic to inline x^2 and x^3 for -# numeric types. In terms of Val we can do it much more simply. +# Inline x^2 and x^3 for Val # (The first argument prevents unexpected behavior if a function ^ # is defined that is not equal to Base.^) @inline literal_pow(::typeof(^), x::HWNumber, ::Val{0}) = one(x) @inline literal_pow(::typeof(^), x::HWNumber, ::Val{1}) = x @inline literal_pow(::typeof(^), x::HWNumber, ::Val{2}) = x*x @inline literal_pow(::typeof(^), x::HWNumber, ::Val{3}) = x*x*x +@inline literal_pow(::typeof(^), x::HWNumber, ::Val{-1}) = inv(x) +@inline literal_pow(::typeof(^), x::HWNumber, ::Val{-2}) = (i=inv(x); i*i) # don't use the inv(x) transformation here since float^p is slightly more accurate @inline literal_pow(::typeof(^), x::AbstractFloat, ::Val{p}) where {p} = x^p @@ -302,11 +327,15 @@ const HWNumber = Union{HWReal, Complex{<:HWReal}, Rational{<:HWReal}} # for other types, define x^-n as inv(x)^n so that negative literal powers can # be computed in a type-stable way even for e.g. integers. -@inline @generated function literal_pow(f::typeof(^), x, ::Val{p}) where {p} +@inline function literal_pow(f::typeof(^), x, ::Val{p}) where {p} if p < 0 - :(literal_pow(^, inv(x), $(Val{-p}()))) + if x isa BitInteger64 + f(Float64(x), p) # inv would cause rounding, while Float64^Integer is able to compensate the inverse + else + f(inv(x), -p) + end else - :(f(x,$p)) + f(x, p) end end @@ -368,9 +397,11 @@ _prevpow2(x::Unsigned) = one(x) << unsigned((sizeof(x)<<3)-leading_zeros(x)-1) _prevpow2(x::Integer) = reinterpret(typeof(x),x < 0 ? -_prevpow2(unsigned(-x)) : _prevpow2(unsigned(x))) """ - ispow2(n::Integer) -> Bool + ispow2(n::Number) -> Bool -Test whether `n` is a power of two. +Test whether `n` is an integer power of two. + +See also [`count_ones`](@ref), [`prevpow`](@ref), [`nextpow`](@ref). # Examples ```jldoctest @@ -379,8 +410,22 @@ true julia> ispow2(5) false + +julia> ispow2(4.5) +false + +julia> ispow2(0.25) +true + +julia> ispow2(1//8) +true ``` + +!!! compat "Julia 1.6" + Support for non-`Integer` arguments was added in Julia 1.6. """ +ispow2(x::Number) = isreal(x) && ispow2(real(x)) + ispow2(x::Integer) = x > 0 && count_ones(x) == 1 """ @@ -389,6 +434,8 @@ ispow2(x::Integer) = x > 0 && count_ones(x) == 1 The smallest `a^n` not less than `x`, where `n` is a non-negative integer. `a` must be greater than 1, and `x` must be greater than 0. +See also [`prevpow`](@ref). + # Examples ```jldoctest julia> nextpow(2, 7) @@ -403,8 +450,6 @@ julia> nextpow(5, 20) julia> nextpow(4, 16) 16 ``` - -See also [`prevpow`](@ref). """ function nextpow(a::Real, x::Real) x <= 0 && throw(DomainError(x, "`x` must be positive.")) @@ -415,9 +460,16 @@ function nextpow(a::Real, x::Real) a <= 1 && throw(DomainError(a, "`a` must be greater than 1.")) x <= 1 && return one(a) n = ceil(Integer,log(a, x)) + # round-off error of log can go either direction, so need some checks p = a^(n-1) - # guard against roundoff error, e.g., with a=5 and x=125 - p >= x ? p : a^n + x > typemax(p) && throw(DomainError(x,"argument is beyond the range of type of the base")) + p >= x && return p + wp = a^n + wp > p || throw(OverflowError("result is beyond the range of type of the base")) + wp >= x && return wp + wwp = a^(n+1) + wwp > wp || throw(OverflowError("result is beyond the range of type of the base")) + return wwp end """ @@ -426,6 +478,8 @@ end The largest `a^n` not greater than `x`, where `n` is a non-negative integer. `a` must be greater than 1, and `x` must not be less than 1. +See also [`nextpow`](@ref), [`isqrt`](@ref). + # Examples ```jldoctest julia> prevpow(2, 7) @@ -440,16 +494,25 @@ julia> prevpow(5, 20) julia> prevpow(4, 16) 16 ``` -See also [`nextpow`](@ref). """ -function prevpow(a::Real, x::Real) +function prevpow(a::T, x::Real) where T <: Real x < 1 && throw(DomainError(x, "`x` must be ≥ 1.")) # See comment in nextpos() for a == special case. a == 2 && isa(x, Integer) && return _prevpow2(x) a <= 1 && throw(DomainError(a, "`a` must be greater than 1.")) n = floor(Integer,log(a, x)) - p = a^(n+1) - p <= x ? p : a^n + # round-off error of log can go either direction, so need some checks + p = a^n + x > typemax(p) && throw(DomainError(x,"argument is beyond the range of type of the base")) + if a isa Integer + wp, overflow = mul_with_overflow(a, p) + wp <= x && !overflow && return wp + else + wp = a^(n+1) + wp <= x && return wp + end + p <= x && return p + return a^(n-1) end ## ndigits (number of digits) in base 10 ## @@ -582,6 +645,8 @@ Compute the number of digits in integer `n` written in base `base` (`base` must not be in `[-1, 0, 1]`), optionally padded with zeros to a specified size (the result will never be less than `pad`). +See also [`digits`](@ref), [`count_ones`](@ref). + # Examples ```jldoctest julia> ndigits(12345) @@ -595,81 +660,121 @@ julia> string(1022, base=16) julia> ndigits(123, pad=5) 5 + +julia> ndigits(-123) +3 ``` """ ndigits(x::Integer; base::Integer=10, pad::Integer=1) = max(pad, ndigits0z(x, base)) ## integer to string functions ## -function bin(x::Unsigned, pad::Integer, neg::Bool) - i = neg + max(pad,sizeof(x)<<3-leading_zeros(x)) - a = StringVector(i) +function bin(x::Unsigned, pad::Int, neg::Bool) + m = 8 * sizeof(x) - leading_zeros(x) + n = neg + max(pad, m) + a = StringVector(n) + # for i in 0x0:UInt(n-1) # automatic vectorization produces redundant codes + # @inbounds a[n - i] = 0x30 + (((x >> i) % UInt8)::UInt8 & 0x1) + # end + i = n + @inbounds while i >= 4 + b = UInt32((x % UInt8)::UInt8) + d = 0x30303030 + ((b * 0x08040201) >> 0x3) & 0x01010101 + a[i-3] = (d >> 0x00) % UInt8 + a[i-2] = (d >> 0x08) % UInt8 + a[i-1] = (d >> 0x10) % UInt8 + a[i] = (d >> 0x18) % UInt8 + x >>= 0x4 + i -= 4 + end while i > neg - @inbounds a[i] = 48+(x&0x1) - x >>= 1 + @inbounds a[i] = 0x30 + ((x % UInt8)::UInt8 & 0x1) + x >>= 0x1 i -= 1 end if neg; @inbounds a[1]=0x2d; end String(a) end -function oct(x::Unsigned, pad::Integer, neg::Bool) - i = neg + max(pad,div((sizeof(x)<<3)-leading_zeros(x)+2,3)) - a = StringVector(i) +function oct(x::Unsigned, pad::Int, neg::Bool) + m = div(8 * sizeof(x) - leading_zeros(x) + 2, 3) + n = neg + max(pad, m) + a = StringVector(n) + i = n while i > neg - @inbounds a[i] = 48+(x&0x7) - x >>= 3 + @inbounds a[i] = 0x30 + ((x % UInt8)::UInt8 & 0x7) + x >>= 0x3 i -= 1 end if neg; @inbounds a[1]=0x2d; end String(a) end -function dec(x::Unsigned, pad::Integer, neg::Bool) - i = neg + ndigits(x, base=10, pad=pad) - a = StringVector(i) - while i > neg - @inbounds a[i] = 48+rem(x,10) - x = oftype(x,div(x,10)) - i -= 1 +# 2-digit decimal characters ("00":"99") +const _dec_d100 = UInt16[(0x30 + i % 10) << 0x8 + (0x30 + i ÷ 10) for i = 0:99] + +function dec(x::Unsigned, pad::Int, neg::Bool) + n = neg + ndigits(x, pad=pad) + a = StringVector(n) + i = n + @inbounds while i >= 2 + d, r = divrem(x, 0x64) + d100 = _dec_d100[(r % Int)::Int + 1] + a[i-1] = d100 % UInt8 + a[i] = (d100 >> 0x8) % UInt8 + x = oftype(x, d) + i -= 2 + end + if i > neg + @inbounds a[i] = 0x30 + (rem(x, 0xa) % UInt8)::UInt8 end if neg; @inbounds a[1]=0x2d; end String(a) end -function hex(x::Unsigned, pad::Integer, neg::Bool) - i = neg + max(pad,(sizeof(x)<<1)-(leading_zeros(x)>>2)) - a = StringVector(i) - while i > neg - d = x & 0xf - @inbounds a[i] = 48+d+39*(d>9) - x >>= 4 - i -= 1 +function hex(x::Unsigned, pad::Int, neg::Bool) + m = 2 * sizeof(x) - (leading_zeros(x) >> 2) + n = neg + max(pad, m) + a = StringVector(n) + i = n + while i >= 2 + b = (x % UInt8)::UInt8 + d1, d2 = b >> 0x4, b & 0xf + @inbounds a[i-1] = d1 + ifelse(d1 > 0x9, 0x57, 0x30) + @inbounds a[i] = d2 + ifelse(d2 > 0x9, 0x57, 0x30) + x >>= 0x8 + i -= 2 + end + if i > neg + d = (x % UInt8)::UInt8 & 0xf + @inbounds a[i] = d + ifelse(d > 0x9, 0x57, 0x30) end if neg; @inbounds a[1]=0x2d; end String(a) end -const base36digits = ['0':'9';'a':'z'] -const base62digits = ['0':'9';'A':'Z';'a':'z'] +const base36digits = UInt8['0':'9';'a':'z'] +const base62digits = UInt8['0':'9';'A':'Z';'a':'z'] -function _base(b::Integer, x::Integer, pad::Integer, neg::Bool) - (x >= 0) | (b < 0) || throw(DomainError(x, "For negative `x`, `b` must be negative.")) - 2 <= abs(b) <= 62 || throw(DomainError(b, "base must satisfy 2 ≤ abs(base) ≤ 62")) +function _base(base::Integer, x::Integer, pad::Int, neg::Bool) + (x >= 0) | (base < 0) || throw(DomainError(x, "For negative `x`, `base` must be negative.")) + 2 <= abs(base) <= 62 || throw(DomainError(base, "base must satisfy 2 ≤ abs(base) ≤ 62")) + b = (base % Int)::Int digits = abs(b) <= 36 ? base36digits : base62digits - i = neg + ndigits(x, base=b, pad=pad) - a = StringVector(i) + n = neg + ndigits(x, base=b, pad=pad) + a = StringVector(n) + i = n @inbounds while i > neg if b > 0 - a[i] = digits[1+rem(x,b)] + a[i] = digits[1 + (rem(x, b) % Int)::Int] x = div(x,b) else - a[i] = digits[1+mod(x,-b)] + a[i] = digits[1 + (mod(x, -b) % Int)::Int] x = cld(x,b) end i -= 1 end - if neg; a[1]='-'; end + if neg; @inbounds a[1]=0x2d; end String(a) end @@ -682,15 +787,19 @@ split_sign(n::Unsigned) = n, false Convert an integer `n` to a string in the given `base`, optionally specifying a number of digits to pad to. +See also [`digits`](@ref), [`bitstring`](@ref), [`count_zeros`](@ref). + +# Examples ```jldoctest julia> string(5, base = 13, pad = 4) "0005" -julia> string(13, base = 5, pad = 4) -"0023" +julia> string(-13, base = 5, pad = 4) +"-0023" ``` """ function string(n::Integer; base::Integer = 10, pad::Integer = 1) + pad = (min(max(pad, typemin(Int)), typemax(Int)) % Int)::Int if base == 2 (n_positive, neg) = split_sign(n) bin(n_positive, pad, neg) @@ -713,24 +822,36 @@ string(b::Bool) = b ? "true" : "false" """ bitstring(n) -A string giving the literal bit representation of a number. +A string giving the literal bit representation of a primitive type. + +See also [`count_ones`](@ref), [`count_zeros`](@ref), [`digits`](@ref). # Examples ```jldoctest -julia> bitstring(4) -"0000000000000000000000000000000000000000000000000000000000000100" +julia> bitstring(Int32(4)) +"00000000000000000000000000000100" julia> bitstring(2.2) "0100000000000001100110011001100110011001100110011001100110011010" ``` """ -function bitstring end - -bitstring(x::Union{Bool,Int8,UInt8}) = string(reinterpret(UInt8,x), pad = 8, base = 2) -bitstring(x::Union{Int16,UInt16,Float16}) = string(reinterpret(UInt16,x), pad = 16, base = 2) -bitstring(x::Union{Char,Int32,UInt32,Float32}) = string(reinterpret(UInt32,x), pad = 32, base = 2) -bitstring(x::Union{Int64,UInt64,Float64}) = string(reinterpret(UInt64,x), pad = 64, base = 2) -bitstring(x::Union{Int128,UInt128}) = string(reinterpret(UInt128,x), pad = 128, base = 2) +function bitstring(x::T) where {T} + isprimitivetype(T) || throw(ArgumentError("$T not a primitive type")) + sz = sizeof(T) * 8 + str = StringVector(sz) + i = sz + @inbounds while i >= 4 + b = UInt32(sizeof(T) == 1 ? bitcast(UInt8, x) : trunc_int(UInt8, x)) + d = 0x30303030 + ((b * 0x08040201) >> 0x3) & 0x01010101 + str[i-3] = (d >> 0x00) % UInt8 + str[i-2] = (d >> 0x08) % UInt8 + str[i-1] = (d >> 0x10) % UInt8 + str[i] = (d >> 0x18) % UInt8 + x = lshr_int(x, 4) + i -= 4 + end + return String(str) +end """ digits([T<:Integer], n::Integer; base::T = 10, pad::Integer = 1) @@ -739,9 +860,12 @@ Return an array with element type `T` (default `Int`) of the digits of `n` in th base, optionally padded with zeros to a specified size. More significant digits are at higher indices, such that `n == sum(digits[k]*base^(k-1) for k=1:length(digits))`. +See also [`ndigits`](@ref), [`digits!`](@ref), +and for base 2 also [`bitstring`](@ref), [`count_ones`](@ref). + # Examples ```jldoctest -julia> digits(10, base = 10) +julia> digits(10) 2-element Vector{Int64}: 0 1 @@ -753,14 +877,18 @@ julia> digits(10, base = 2) 0 1 -julia> digits(10, base = 2, pad = 6) -6-element Vector{Int64}: - 0 - 1 - 0 - 1 - 0 - 0 +julia> digits(-256, base = 10, pad = 5) +5-element Vector{Int64}: + -6 + -5 + -2 + 0 + 0 + +julia> n = rand(-999:999); + +julia> n == evalpoly(13, digits(n, base = 13)) +true ``` """ digits(n::Integer; base::Integer = 10, pad::Integer = 1) = @@ -773,10 +901,11 @@ end """ hastypemax(T::Type) -> Bool -Return `true` if and only if `typemax(T)` is defined. +Return true if and only if the extrema `typemax(T)` and `typemin(T)` are defined. """ hastypemax(::Base.BitIntegerType) = true -hastypemax(::Type{T}) where {T} = applicable(typemax, T) +hastypemax(::Type{Bool}) = true +hastypemax(::Type{T}) where {T} = applicable(typemax, T) && applicable(typemin, T) """ digits!(array, n::Integer; base::Integer = 10) @@ -787,14 +916,14 @@ the array length. If the array length is excessive, the excess portion is filled # Examples ```jldoctest -julia> digits!([2,2,2,2], 10, base = 2) +julia> digits!([2, 2, 2, 2], 10, base = 2) 4-element Vector{Int64}: 0 1 0 1 -julia> digits!([2,2,2,2,2,2], 10, base = 2) +julia> digits!([2, 2, 2, 2, 2, 2], 10, base = 2) 6-element Vector{Int64}: 0 1 @@ -866,6 +995,8 @@ Factorial of `n`. If `n` is an [`Integer`](@ref), the factorial is computed as a integer (promoted to at least 64 bits). Note that this may overflow if `n` is not small, but you can use `factorial(big(n))` to compute the result exactly in arbitrary precision. +See also [`binomial`](@ref). + # Examples ```jldoctest julia> factorial(6) @@ -880,9 +1011,6 @@ julia> factorial(big(21)) 51090942171709440000 ``` -# See also -* [`binomial`](@ref) - # External links * [Factorial](https://en.wikipedia.org/wiki/Factorial) on Wikipedia. """ @@ -912,6 +1040,8 @@ If ``n`` is negative, then it is defined in terms of the identity \\binom{n}{k} = (-1)^k \\binom{k-n-1}{k} ``` +See also [`factorial`](@ref). + # Examples ```jldoctest julia> binomial(5, 3) @@ -924,9 +1054,6 @@ julia> binomial(-5, 3) -35 ``` -# See also -* [`factorial`](@ref) - # External links * [Binomial coefficient](https://en.wikipedia.org/wiki/Binomial_coefficient) on Wikipedia. """ @@ -935,7 +1062,7 @@ function binomial(n::T, k::T) where T<:Integer k < 0 && return zero(T) sgn = one(T) if n < 0 - n = -n + k -1 + n = -n + k - one(T) if isodd(k) sgn = -sgn end @@ -946,15 +1073,15 @@ function binomial(n::T, k::T) where T<:Integer if k > (n>>1) k = (n - k) end - x::T = nn = n - k + 1 - nn += 1 - rr = 2 + x = nn = n - k + one(T) + nn += one(T) + rr = T(2) while rr <= k xt = div(widemul(x, nn), rr) x = xt % T x == xt || throw(OverflowError("binomial($n0, $k0) overflows")) - rr += 1 - nn += 1 + rr += one(T) + nn += one(T) end - convert(T, copysign(x, sgn)) + copysign(x, sgn) end diff --git a/base/io.jl b/base/io.jl index 4c0cf6d7cf19e7..3fac2287bdabf4 100644 --- a/base/io.jl +++ b/base/io.jl @@ -15,12 +15,12 @@ struct EOFError <: Exception end A system call failed with an error code (in the `errno` global variable). """ struct SystemError <: Exception - prefix::AbstractString + prefix::String errnum::Int32 extrainfo SystemError(p::AbstractString, e::Integer, extrainfo) = new(p, e, extrainfo) SystemError(p::AbstractString, e::Integer) = new(p, e, nothing) - SystemError(p::AbstractString) = new(p, Libc.errno()) + SystemError(p::AbstractString) = new(p, Libc.errno(), nothing) end lock(::IO) = nothing @@ -60,9 +60,50 @@ function isopen end Close an I/O stream. Performs a [`flush`](@ref) first. """ function close end + +""" + closewrite(stream) + +Shutdown the write half of a full-duplex I/O stream. Performs a [`flush`](@ref) +first. Notify the other end that no more data will be written to the underlying +file. This is not supported by all IO types. + +# Examples +```jldoctest +julia> io = Base.BufferStream(); # this never blocks, so we can read and write on the same Task + +julia> write(io, "request"); + +julia> # calling `read(io)` here would block forever + +julia> closewrite(io); + +julia> read(io, String) +"request" +``` +""" +function closewrite end + +""" + flush(stream) + +Commit all currently buffered writes to the given stream. +""" function flush end -function wait_readnb end -function wait_close end + +""" + bytesavailable(io) + +Return the number of bytes available for reading before a read from this stream or buffer will block. + +# Examples +```jldoctest +julia> io = IOBuffer("JuliaLang is a GitHub organization"); + +julia> bytesavailable(io) +34 +``` +""" function bytesavailable end """ @@ -73,15 +114,15 @@ data has already been buffered. The result is a `Vector{UInt8}`. !!! warning The amount of data returned is implementation-dependent; for example it can -depend on the internal choice of buffer size. Other functions such as [`read`](@ref) -should generally be used instead. + depend on the internal choice of buffer size. Other functions such as [`read`](@ref) + should generally be used instead. """ function readavailable end """ isreadable(io) -> Bool -Return `true` if the specified IO object is readable (if that can be determined). +Return `false` if the specified IO object is not readable. # Examples ```jldoctest @@ -99,12 +140,12 @@ true julia> rm("myfile.txt") ``` """ -function isreadable end +isreadable(io::IO) = isopen(io) """ iswritable(io) -> Bool -Return `true` if the specified IO object is writable (if that can be determined). +Return `false` if the specified IO object is not writable. # Examples ```jldoctest @@ -122,10 +163,23 @@ false julia> rm("myfile.txt") ``` """ -function iswritable end -function copy end +iswritable(io::IO) = isopen(io) + +""" + eof(stream) -> Bool + +Test whether an I/O stream is at end-of-file. If the stream is not yet exhausted, this +function will block to wait for more data if necessary, and then return `false`. Therefore +it is always safe to read one byte after seeing `eof` return `false`. `eof` will return +`false` as long as buffered data is still available, even if the remote end of a connection +is closed. +""" function eof end +function copy end +function wait_readnb end +function wait_close end + """ read(io::IO, T) @@ -136,7 +190,7 @@ Note that Julia does not convert the endianness for you. Use [`ntoh`](@ref) or read(io::IO, String) -Read the entirety of `io`, as a `String`. +Read the entirety of `io`, as a `String` (see also [`readchomp`](@ref)). # Examples ```jldoctest @@ -307,7 +361,7 @@ function open_flags(; end """ - open(f::Function, args...; kwargs....) + open(f::Function, args...; kwargs...) Apply the function `f` to the result of `open(args...; kwargs...)` and close the resulting file descriptor upon completion. @@ -357,67 +411,41 @@ end function pipe_reader end function pipe_writer end +for f in (:flush, :closewrite, :iswritable) + @eval $(f)(io::AbstractPipe) = $(f)(pipe_writer(io)::IO) +end write(io::AbstractPipe, byte::UInt8) = write(pipe_writer(io)::IO, byte) +write(to::IO, from::AbstractPipe) = write(to, pipe_reader(from)) unsafe_write(io::AbstractPipe, p::Ptr{UInt8}, nb::UInt) = unsafe_write(pipe_writer(io)::IO, p, nb)::Union{Int,UInt} buffer_writes(io::AbstractPipe, args...) = buffer_writes(pipe_writer(io)::IO, args...) -flush(io::AbstractPipe) = flush(pipe_writer(io)::IO) +for f in ( + # peek/mark interface + :mark, :unmark, :reset, :ismarked, + # Simple reader functions + :read, :readavailable, :bytesavailable, :reseteof, :isreadable) + @eval $(f)(io::AbstractPipe) = $(f)(pipe_reader(io)::IO) +end read(io::AbstractPipe, byte::Type{UInt8}) = read(pipe_reader(io)::IO, byte)::UInt8 unsafe_read(io::AbstractPipe, p::Ptr{UInt8}, nb::UInt) = unsafe_read(pipe_reader(io)::IO, p, nb) -read(io::AbstractPipe) = read(pipe_reader(io)::IO) readuntil(io::AbstractPipe, arg::UInt8; kw...) = readuntil(pipe_reader(io)::IO, arg; kw...) readuntil(io::AbstractPipe, arg::AbstractChar; kw...) = readuntil(pipe_reader(io)::IO, arg; kw...) readuntil(io::AbstractPipe, arg::AbstractString; kw...) = readuntil(pipe_reader(io)::IO, arg; kw...) readuntil(io::AbstractPipe, arg::AbstractVector; kw...) = readuntil(pipe_reader(io)::IO, arg; kw...) readuntil_vector!(io::AbstractPipe, target::AbstractVector, keep::Bool, out) = readuntil_vector!(pipe_reader(io)::IO, target, keep, out) readbytes!(io::AbstractPipe, target::AbstractVector{UInt8}, n=length(target)) = readbytes!(pipe_reader(io)::IO, target, n) - -for f in ( - # peek/mark interface - :mark, :unmark, :reset, :ismarked, - # Simple reader functions - :readavailable, :isreadable) - @eval $(f)(io::AbstractPipe) = $(f)(pipe_reader(io)::IO) -end peek(io::AbstractPipe, ::Type{T}) where {T} = peek(pipe_reader(io)::IO, T)::T +wait_readnb(io::AbstractPipe, nb::Int) = wait_readnb(pipe_reader(io)::IO, nb) +eof(io::AbstractPipe) = eof(pipe_reader(io)::IO)::Bool -iswritable(io::AbstractPipe) = iswritable(pipe_writer(io)::IO) isopen(io::AbstractPipe) = isopen(pipe_writer(io)::IO) || isopen(pipe_reader(io)::IO) close(io::AbstractPipe) = (close(pipe_writer(io)::IO); close(pipe_reader(io)::IO)) -wait_readnb(io::AbstractPipe, nb::Int) = wait_readnb(pipe_reader(io)::IO, nb) wait_close(io::AbstractPipe) = (wait_close(pipe_writer(io)::IO); wait_close(pipe_reader(io)::IO)) -""" - bytesavailable(io) - -Return the number of bytes available for reading before a read from this stream or buffer will block. - -# Examples -```jldoctest -julia> io = IOBuffer("JuliaLang is a GitHub organization"); - -julia> bytesavailable(io) -34 -``` -""" -bytesavailable(io::AbstractPipe) = bytesavailable(pipe_reader(io)::IO) - -""" - eof(stream) -> Bool - -Test whether an I/O stream is at end-of-file. If the stream is not yet exhausted, this -function will block to wait for more data if necessary, and then return `false`. Therefore -it is always safe to read one byte after seeing `eof` return `false`. `eof` will return -`false` as long as buffered data is still available, even if the remote end of a connection -is closed. -""" -eof(io::AbstractPipe) = eof(pipe_reader(io)::IO)::Bool -reseteof(io::AbstractPipe) = reseteof(pipe_reader(io)::IO) - # Exception-safe wrappers (io = open(); try f(io) finally close(io)) -write(filename::AbstractString, a1, args...) = open(io->write(io, a1, args...), filename, "w") +write(filename::AbstractString, a1, args...) = open(io->write(io, a1, args...), convert(String, filename)::String, "w") """ read(filename::AbstractString, args...) @@ -429,9 +457,9 @@ Open a file and read its contents. `args` is passed to `read`: this is equivalen Read the entire contents of a file as a string. """ -read(filename::AbstractString, args...) = open(io->read(io, args...), filename) +read(filename::AbstractString, args...) = open(io->read(io, args...), convert(String, filename)::String) -read(filename::AbstractString, ::Type{T}) where {T} = open(io->read(io, T), filename) +read(filename::AbstractString, ::Type{T}) where {T} = open(io->read(io, T), convert(String, filename)::String) """ read!(stream::IO, array::AbstractArray) @@ -441,7 +469,7 @@ Read binary data from an I/O stream or file, filling in `array`. """ function read! end -read!(filename::AbstractString, a) = open(io->read!(io, a), filename) +read!(filename::AbstractString, a) = open(io->read!(io, a), convert(String, filename)::String) """ readuntil(stream::IO, delim; keep::Bool = false) @@ -468,7 +496,7 @@ julia> readuntil("my_file.txt", '.', keep = true) julia> rm("my_file.txt") ``` """ -readuntil(filename::AbstractString, args...; kw...) = open(io->readuntil(io, args...; kw...), filename) +readuntil(filename::AbstractString, args...; kw...) = open(io->readuntil(io, args...; kw...), convert(String, filename)::String) """ readline(io::IO=stdin; keep::Bool=false) @@ -496,6 +524,14 @@ julia> readline("my_file.txt", keep=true) julia> rm("my_file.txt") ``` +```julia-repl +julia> print("Enter your name: ") +Enter your name: + +julia> your_name = readline() +Logan +"Logan" +``` """ function readline(filename::AbstractString; keep::Bool=false) open(filename) do f @@ -521,7 +557,8 @@ end Read all lines of an I/O stream or a file as a vector of strings. Behavior is equivalent to saving the result of reading [`readline`](@ref) repeatedly with the same -arguments and saving the resulting lines as a vector of strings. +arguments and saving the resulting lines as a vector of strings. See also +[`eachline`](@ref) to iterate over the lines without reading them all at once. # Examples ```jldoctest @@ -883,8 +920,9 @@ end function readuntil(io::IO, target::AbstractString; keep::Bool=false) # small-string target optimizations - isempty(target) && return "" - c, rest = Iterators.peel(target) + x = Iterators.peel(target) + isnothing(x) && return "" + c, rest = x if isempty(rest) && c <= '\x7f' return readuntil_string(io, c % UInt8, keep) end @@ -987,6 +1025,13 @@ retained. When called with a file name, the file is opened once at the beginning iteration and closed at the end. If iteration is interrupted, the file will be closed when the `EachLine` object is garbage collected. +To iterate over each line of a `String`, `eachline(IOBuffer(str))` can be used. + +[`Iterators.reverse`](@ref) can be used on an `EachLine` object to read the lines +in reverse order (for files, buffers, and other I/O streams supporting [`seek`](@ref)), +and [`first`](@ref) or [`last`](@ref) can be used to extract the initial or final +lines, respectively. + # Examples ```jldoctest julia> open("my_file.txt", "w") do io @@ -1000,6 +1045,9 @@ JuliaLang is a GitHub organization. It has many members. julia> rm("my_file.txt"); ``` + +!!! compat "Julia 1.8" + Julia 1.8 is required to use `Iterators.reverse` or `last` with `eachline` iterators. """ function eachline(stream::IO=stdin; keep::Bool=false) EachLine(stream, keep=keep)::EachLine @@ -1019,6 +1067,117 @@ eltype(::Type{<:EachLine}) = String IteratorSize(::Type{<:EachLine}) = SizeUnknown() +isdone(itr::EachLine, state...) = eof(itr.stream) + +# Reverse-order iteration for the EachLine iterator for seekable streams, +# which works by reading the stream from the end in 4kiB chunks. +function iterate(r::Iterators.Reverse{<:EachLine}) + p0 = position(r.itr.stream) + seekend(r.itr.stream) # may throw if io is non-seekable + p = position(r.itr.stream) + # chunks = circular buffer of 4kiB blocks read from end of stream + chunks = empty!(Vector{Vector{UInt8}}(undef, 2)) # allocate space for 2 buffers (common case) + inewline = jnewline = 0 + while p > p0 && inewline == 0 # read chunks until we find a newline or we read whole file + chunk = Vector{UInt8}(undef, min(4096, p-p0)) + p -= length(chunk) + readbytes!(seek(r.itr.stream, p), chunk) + pushfirst!(chunks, chunk) + inewline = something(findlast(==(UInt8('\n')), chunk), 0) + if length(chunks) == 1 && inewline == length(chunks[1]) + # found newline at end of file … keep looking + jnewline = inewline + inewline = something(findprev(==(UInt8('\n')), chunk, inewline-1), 0) + end + end + return iterate(r, (; p0, p, chunks, ichunk=1, inewline, jchunk=length(chunks), jnewline = jnewline == 0 && !isempty(chunks) ? length(chunks[end]) : jnewline)) +end +function iterate(r::Iterators.Reverse{<:EachLine}, state) + function _stripnewline(keep, pos, data) + # strip \n or \r\n from data[pos] by decrementing pos + if !keep && pos > 0 && data[pos] == UInt8('\n') + pos -= 1 + pos -= pos > 0 && data[pos] == UInt8('\r') + end + return pos + end + # state tuple: p0 = initial file position, p = current position, + # chunks = circular array of chunk buffers, + # current line is from chunks[ichunk][inewline+1] to chunks[jchunk][jnewline] + p0, p, chunks, ichunk, inewline, jchunk, jnewline = state + if inewline == 0 # no newline found, remaining line = rest of chunks (if any) + isempty(chunks) && return (r.itr.ondone(); nothing) + buf = IOBuffer(sizehint = ichunk==jchunk ? jnewline : 4096) + while ichunk != jchunk + write(buf, chunks[ichunk]) + ichunk = ichunk == length(chunks) ? 1 : ichunk + 1 + end + chunk = chunks[jchunk] + write(buf, view(chunk, 1:jnewline)) + buf.size = _stripnewline(r.itr.keep, buf.size, buf.data) + empty!(chunks) # will cause next iteration to terminate + seekend(r.itr.stream) # reposition to end of stream for isdone + s = String(take!(buf)) + else + # extract the string from chunks[ichunk][inewline+1] to chunks[jchunk][jnewline] + if ichunk == jchunk # common case: current and previous newline in same chunk + chunk = chunks[ichunk] + s = String(view(chunk, inewline+1:_stripnewline(r.itr.keep, jnewline, chunk))) + else + buf = IOBuffer(sizehint=max(128, length(chunks[ichunk])-inewline+jnewline)) + write(buf, view(chunks[ichunk], inewline+1:length(chunks[ichunk]))) + i = ichunk + while true + i = i == length(chunks) ? 1 : i + 1 + i == jchunk && break + write(buf, chunks[i]) + end + write(buf, view(chunks[jchunk], 1:jnewline)) + buf.size = _stripnewline(r.itr.keep, buf.size, buf.data) + s = String(take!(buf)) + + # overwrite obsolete chunks (ichunk+1:jchunk) + i = jchunk + while i != ichunk + chunk = chunks[i] + p -= length(resize!(chunk, min(4096, p-p0))) + readbytes!(seek(r.itr.stream, p), chunk) + i = i == 1 ? length(chunks) : i - 1 + end + end + + # find the newline previous to inewline + jchunk = ichunk + jnewline = inewline + while true + inewline = something(findprev(==(UInt8('\n')), chunks[ichunk], inewline-1), 0) + inewline > 0 && break + ichunk = ichunk == 1 ? length(chunks) : ichunk - 1 + ichunk == jchunk && break # found nothing — may need to read more chunks + inewline = length(chunks[ichunk])+1 # start for next findprev + end + + # read more chunks to look for a newline (should rarely happen) + if inewline == 0 && p > p0 + ichunk = jchunk + 1 + while true + chunk = Vector{UInt8}(undef, min(4096, p-p0)) + p -= length(chunk) + readbytes!(seek(r.itr.stream, p), chunk) + insert!(chunks, ichunk, chunk) + inewline = something(findlast(==(UInt8('\n')), chunk), 0) + (p == p0 || inewline > 0) && break + end + end + end + return (s, (; p0, p, chunks, ichunk, inewline, jchunk, jnewline)) +end +isdone(r::Iterators.Reverse{<:EachLine}, state) = isempty(state.chunks) +isdone(r::Iterators.Reverse{<:EachLine}) = isdone(r.itr) + +# use reverse iteration to get end of EachLines (if possible) +last(itr::EachLine) = first(Iterators.reverse(itr)) + struct ReadEachIterator{T, IOT <: IO} stream::IOT end @@ -1028,7 +1187,7 @@ end Return an iterable object yielding [`read(io, T)`](@ref). -See also: [`skipchars`](@ref), [`eachline`](@ref), [`readuntil`](@ref) +See also [`skipchars`](@ref), [`eachline`](@ref), [`readuntil`](@ref). !!! compat "Julia 1.6" `readeach` requires Julia 1.6 or later. @@ -1053,13 +1212,15 @@ eltype(::Type{ReadEachIterator{T}}) where T = T IteratorSize(::Type{<:ReadEachIterator}) = SizeUnknown() +isdone(itr::ReadEachIterator, state...) = eof(itr.stream) + # IOStream Marking # Note that these functions expect that io.mark exists for # the concrete IO type. This may not be true for IO types # not in base. """ - mark(s) + mark(s::IO) Add a mark at the current position of stream `s`. Return the marked position. @@ -1070,7 +1231,7 @@ function mark(io::IO) end """ - unmark(s) + unmark(s::IO) Remove a mark from stream `s`. Return `true` if the stream was marked, `false` otherwise. @@ -1083,7 +1244,7 @@ function unmark(io::IO) end """ - reset(s) + reset(s::IO) Reset a stream `s` to a previously marked position, and remove the mark. Return the previously marked position. Throw an error if the stream is not marked. @@ -1099,7 +1260,7 @@ function reset(io::T) where T<:IO end """ - ismarked(s) + ismarked(s::IO) Return `true` if stream `s` is marked. @@ -1110,11 +1271,6 @@ ismarked(io::IO) = io.mark >= 0 # Make sure all IO streams support flush, even if only as a no-op, # to make it easier to write generic I/O code. -""" - flush(stream) - -Commit all currently buffered writes to the given stream. -""" flush(io::IO) = nothing """ @@ -1156,6 +1312,8 @@ pass the filename as the first argument. EOL markers other than `'\\n'` are supp passing them as the second argument. The last non-empty line of `io` is counted even if it does not end with the EOL, matching the length returned by [`eachline`](@ref) and [`readlines`](@ref). +To count lines of a `String`, `countlines(IOBuffer(str))` can be used. + # Examples ```jldoctest julia> io = IOBuffer("JuliaLang is a GitHub organization.\\n"); @@ -1168,8 +1326,13 @@ julia> io = IOBuffer("JuliaLang is a GitHub organization."); julia> countlines(io) 1 +julia> eof(io) # counting lines moves the file pointer +true + +julia> io = IOBuffer("JuliaLang is a GitHub organization."); + julia> countlines(io, eol = '.') -0 +1 ``` """ function countlines(io::IO; eol::AbstractChar='\n') diff --git a/base/iobuffer.jl b/base/iobuffer.jl index a1504b4bd4f638..e08a019d84a2ca 100644 --- a/base/iobuffer.jl +++ b/base/iobuffer.jl @@ -334,6 +334,12 @@ end eof(io::GenericIOBuffer) = (io.ptr-1 == io.size) +function closewrite(io::GenericIOBuffer) + io.writable = false + # OR throw(_UVError("closewrite", UV_ENOTSOCK)) + nothing +end + @noinline function close(io::GenericIOBuffer{T}) where T io.readable = false io.writable = false @@ -353,8 +359,7 @@ isopen(io::GenericIOBuffer) = io.readable || io.writable || io.seekable || bytes """ take!(b::IOBuffer) -Obtain the contents of an `IOBuffer` as an array, without copying. Afterwards, the -`IOBuffer` is reset to its initial state. +Obtain the contents of an `IOBuffer` as an array. Afterwards, the `IOBuffer` is reset to its initial state. # Examples ```jldoctest @@ -405,12 +410,12 @@ function take!(io::IOBuffer) return data end -function write(to::GenericIOBuffer, from::GenericIOBuffer) +function write(to::IO, from::GenericIOBuffer) if to === from from.ptr = from.size + 1 return 0 end - written::Int = write_sub(to, from.data, from.ptr, bytesavailable(from)) + written::Int = GC.@preserve from unsafe_write(to, pointer(from.data, from.ptr), UInt(bytesavailable(from))) from.ptr += written return written end @@ -434,14 +439,6 @@ function unsafe_write(to::GenericIOBuffer, p::Ptr{UInt8}, nb::UInt) return written end -function write_sub(to::GenericIOBuffer, a::AbstractArray{UInt8}, offs, nel) - require_one_based_indexing(a) - if offs+nel-1 > length(a) || offs < 1 || nel < 0 - throw(BoundsError()) - end - GC.@preserve a unsafe_write(to, pointer(a, offs), UInt(nel)) -end - @inline function write(to::GenericIOBuffer, a::UInt8) ensureroom(to, UInt(1)) ptr = (to.append ? to.size+1 : to.ptr) diff --git a/base/iostream.jl b/base/iostream.jl index 4a52a4f4ce5789..23dfb53256e826 100644 --- a/base/iostream.jl +++ b/base/iostream.jl @@ -13,7 +13,7 @@ Mostly used to represent files returned by [`open`](@ref). mutable struct IOStream <: IO handle::Ptr{Cvoid} ios::Array{UInt8,1} - name::AbstractString + name::String mark::Int64 lock::ReentrantLock _dolock::Bool @@ -272,7 +272,7 @@ safe multi-threaded access. !!! compat "Julia 1.5" The `lock` argument is available as of Julia 1.5. """ -function open(fname::AbstractString; lock = true, +function open(fname::String; lock = true, read :: Union{Bool,Nothing} = nothing, write :: Union{Bool,Nothing} = nothing, create :: Union{Bool,Nothing} = nothing, @@ -299,6 +299,7 @@ function open(fname::AbstractString; lock = true, end return s end +open(fname::AbstractString; kwargs...) = open(convert(String, fname)::String; kwargs...) """ open(filename::AbstractString, [mode::AbstractString]; lock = true) -> IOStream @@ -404,13 +405,15 @@ end if ENDIAN_BOM == 0x04030201 function read(s::IOStream, T::Union{Type{Int16},Type{UInt16},Type{Int32},Type{UInt32},Type{Int64},Type{UInt64}}) n = sizeof(T) - lock(s.lock) + l = s._dolock + _lock = s.lock + l && lock(_lock) if ccall(:jl_ios_buffer_n, Cint, (Ptr{Cvoid}, Csize_t), s.ios, n) != 0 - unlock(s.lock) + l && unlock(_lock) throw(EOFError()) end x = ccall(:jl_ios_get_nbyte_int, UInt64, (Ptr{Cvoid}, Csize_t), s.ios, n) % T - unlock(s.lock) + l && unlock(_lock) return x end @@ -450,17 +453,24 @@ function readbytes_all!(s::IOStream, nb::Integer) olb = lb = length(b) nr = 0 - @_lock_ios s begin - GC.@preserve b while nr < nb - if lb < nr+1 - lb = max(65536, (nr+1) * 2) - resize!(b, lb) + let l = s._dolock, slock = s.lock + l && lock(slock) + GC.@preserve b while nr < nb + if lb < nr+1 + try + lb = max(65536, (nr+1) * 2) + resize!(b, lb) + catch + l && unlock(slock) + rethrow() + end + end + thisr = Int(ccall(:ios_readall, Csize_t, (Ptr{Cvoid}, Ptr{Cvoid}, Csize_t), + s.ios, pointer(b, nr+1), min(lb-nr, nb-nr))) + nr += thisr + (nr == nb || thisr == 0 || _eof_nolock(s)) && break end - thisr = Int(ccall(:ios_readall, Csize_t, (Ptr{Cvoid}, Ptr{Cvoid}, Csize_t), - s.ios, pointer(b, nr+1), min(lb-nr, nb-nr))) - nr += thisr - (nr == nb || thisr == 0 || _eof_nolock(s)) && break - end + l && unlock(slock) end if lb > olb && lb > nr resize!(b, max(olb, nr)) # shrink to just contain input data if was resized diff --git a/base/irrationals.jl b/base/irrationals.jl index 1f7b1358dd70e3..ecc3aff6138c1a 100644 --- a/base/irrationals.jl +++ b/base/irrationals.jl @@ -22,7 +22,9 @@ abstract type AbstractIrrational <: Real end Irrational{sym} <: AbstractIrrational Number type representing an exact irrational value denoted by the -symbol `sym`. +symbol `sym`, such as [`π`](@ref pi), [`ℯ`](@ref) and [`γ`](@ref Base.MathConstants.eulergamma). + +See also [`@irrational`], [`AbstractIrrational`](@ref). """ struct Irrational{sym} <: AbstractIrrational end @@ -46,7 +48,8 @@ AbstractFloat(x::AbstractIrrational) = Float64(x)::Float64 Float16(x::AbstractIrrational) = Float16(Float32(x)::Float32) Complex{T}(x::AbstractIrrational) where {T<:Real} = Complex{T}(T(x)) -@pure function Rational{T}(x::AbstractIrrational) where T<:Integer +# XXX this may change `DEFAULT_PRECISION`, thus not effect free +@assume_effects :total function Rational{T}(x::AbstractIrrational) where T<:Integer o = precision(BigFloat) p = 256 while true @@ -62,7 +65,7 @@ Complex{T}(x::AbstractIrrational) where {T<:Real} = Complex{T}(T(x)) end Rational{BigInt}(x::AbstractIrrational) = throw(ArgumentError("Cannot convert an AbstractIrrational to a Rational{BigInt}: use rationalize(BigInt, x) instead")) -@pure function (t::Type{T})(x::AbstractIrrational, r::RoundingMode) where T<:Union{Float32,Float64} +@assume_effects :total function (t::Type{T})(x::AbstractIrrational, r::RoundingMode) where T<:Union{Float32,Float64} setprecision(BigFloat, 256) do T(BigFloat(x)::BigFloat, r) end @@ -104,11 +107,11 @@ end <=(x::AbstractFloat, y::AbstractIrrational) = x < y # Irrational vs Rational -@pure function rationalize(::Type{T}, x::AbstractIrrational; tol::Real=0) where T +@assume_effects :total function rationalize(::Type{T}, x::AbstractIrrational; tol::Real=0) where T return rationalize(T, big(x), tol=tol) end -@pure function lessrational(rx::Rational{<:Integer}, x::AbstractIrrational) - # an @pure version of `<` for determining if the rationalization of +@assume_effects :total function lessrational(rx::Rational{<:Integer}, x::AbstractIrrational) + # an @assume_effects :total version of `<` for determining if the rationalization of # an irrational number required rounding up or down return rx < big(x) end @@ -151,6 +154,8 @@ zero(::Type{<:AbstractIrrational}) = false one(::AbstractIrrational) = true one(::Type{<:AbstractIrrational}) = true +sign(x::AbstractIrrational) = ifelse(x < zero(x), -1.0, 1.0) + -(x::AbstractIrrational) = -Float64(x) for op in Symbol[:+, :-, :*, :/, :^] @eval $op(x::AbstractIrrational, y::AbstractIrrational) = $op(Float64(x),Float64(y)) @@ -160,8 +165,8 @@ end round(x::Irrational, r::RoundingMode) = round(float(x), r) """ - @irrational sym val def - @irrational(sym, val, def) + @irrational sym val def + @irrational(sym, val, def) Define a new `Irrational` value, `sym`, with pre-computed `Float64` value `val`, and arbitrary-precision definition in terms of `BigFloat`s given by the expression `def`. @@ -201,7 +206,7 @@ big(::Type{<:AbstractIrrational}) = BigFloat function alignment(io::IO, x::AbstractIrrational) m = match(r"^(.*?)(=.*)$", sprint(show, x, context=io, sizehint=0)) m === nothing ? (length(sprint(show, x, context=io, sizehint=0)), 0) : - (length(m.captures[1]), length(m.captures[2])) + (length(something(m.captures[1])), length(something(m.captures[2]))) end # inv diff --git a/base/iterators.jl b/base/iterators.jl index 6480b2b799e7fe..2702375d0f6304 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -9,7 +9,7 @@ module Iterators import ..@__MODULE__, ..parentmodule const Base = parentmodule(@__MODULE__) using .Base: - @inline, Pair, AbstractDict, IndexLinear, IndexCartesian, IndexStyle, AbstractVector, Vector, + @inline, Pair, Pairs, AbstractDict, IndexLinear, IndexCartesian, IndexStyle, AbstractVector, Vector, tail, SizeUnknown, HasLength, HasShape, IsInfinite, EltypeUnknown, HasEltype, OneTo, @propagate_inbounds, @isdefined, @boundscheck, @inbounds, Generator, AbstractRange, LinearIndices, (:), |, +, -, !==, !, <=, <, missing, any, _counttuple @@ -22,7 +22,7 @@ import .Base: getindex, setindex!, get, iterate, popfirst!, isdone, peek -export enumerate, zip, rest, countfrom, take, drop, takewhile, dropwhile, cycle, repeated, product, flatten, partition +export enumerate, zip, rest, countfrom, take, drop, takewhile, dropwhile, cycle, repeated, product, flatten, partition, flatmap """ Iterators.map(f, iterators...) @@ -103,7 +103,6 @@ size(r::Reverse) = size(r.itr) IteratorSize(::Type{Reverse{T}}) where {T} = IteratorSize(T) IteratorEltype(::Type{Reverse{T}}) where {T} = IteratorEltype(T) last(r::Reverse) = first(r.itr) # the first shall be last -first(r::Reverse) = last(r.itr) # and the last shall be first # reverse-order array iterators: assumes more-specialized Reverse for eachindex @propagate_inbounds function iterate(A::Reverse{<:AbstractArray}, state=(reverse(eachindex(A.itr)),)) @@ -113,13 +112,22 @@ first(r::Reverse) = last(r.itr) # and the last shall be first (A.itr[idx], (state[1], itrs)) end +# Fallback method of `iterate(::Reverse{T})` which assumes the collection has `getindex(::T) and `reverse(eachindex(::T))` +# don't propagate inbounds for this just in case +function iterate(A::Reverse, state=(reverse(eachindex(A.itr)),)) + y = iterate(state...) + y === nothing && return y + idx, itrs = y + (A.itr[idx], (state[1], itrs)) +end + reverse(R::AbstractRange) = Base.reverse(R) # copying ranges is cheap reverse(G::Generator) = Generator(G.f, reverse(G.iter)) reverse(r::Reverse) = r.itr reverse(x::Union{Number,AbstractChar}) = x reverse(p::Pair) = Base.reverse(p) # copying pairs is cheap -iterate(r::Reverse{<:Tuple}, i::Int = length(r.itr)) = i < 1 ? nothing : (r.itr[i], i-1) +iterate(r::Reverse{<:Union{Tuple, NamedTuple}}, i::Int = length(r.itr)) = i < 1 ? nothing : (r.itr[i], i-1) # enumerate @@ -160,6 +168,7 @@ size(e::Enumerate) = size(e.itr) n === nothing && return n (i, n[1]), (i+1, n[2]) end +last(e::Enumerate) = (length(e.itr), e.itr[end]) eltype(::Type{Enumerate{I}}) where {I} = Tuple{Int, eltype(I)} @@ -177,18 +186,6 @@ end (i, n[1]), (i-1, ri, n[2]) end -""" - Iterators.Pairs(values, keys) <: AbstractDict{eltype(keys), eltype(values)} - -Transforms an indexable container into an Dictionary-view of the same data. -Modifying the key-space of the underlying data may invalidate this object. -""" -struct Pairs{K, V, I, A} <: AbstractDict{K, V} - data::A - itr::I - Pairs(data::A, itr::I) where {A, I} = new{eltype(I), eltype(A), I, A}(data, itr) -end - """ pairs(IndexLinear(), A) pairs(IndexCartesian(), A) @@ -233,44 +230,59 @@ CartesianIndex(1, 2) d CartesianIndex(2, 2) e ``` -See also: [`IndexStyle`](@ref), [`axes`](@ref). +See also [`IndexStyle`](@ref), [`axes`](@ref). """ pairs(::IndexLinear, A::AbstractArray) = Pairs(A, LinearIndices(A)) pairs(::IndexCartesian, A::AbstractArray) = Pairs(A, CartesianIndices(axes(A))) # preserve indexing capabilities for known indexable types # faster than zip(keys(a), values(a)) for arrays +pairs(tuple::Tuple) = Pairs{Int}(tuple, keys(tuple)) +pairs(nt::NamedTuple) = Pairs{Symbol}(nt, keys(nt)) +pairs(v::Core.SimpleVector) = Pairs(v, LinearIndices(v)) pairs(A::AbstractArray) = pairs(IndexCartesian(), A) pairs(A::AbstractVector) = pairs(IndexLinear(), A) -pairs(tuple::Tuple) = Pairs(tuple, keys(tuple)) -pairs(nt::NamedTuple) = Pairs(nt, keys(nt)) -pairs(v::Core.SimpleVector) = Pairs(v, LinearIndices(v)) # pairs(v::Pairs) = v # listed for reference, but already defined from being an AbstractDict -length(v::Pairs) = length(v.itr) -axes(v::Pairs) = axes(v.itr) -size(v::Pairs) = size(v.itr) -@propagate_inbounds function iterate(v::Pairs{K, V}, state...) where {K, V} - x = iterate(v.itr, state...) +length(v::Pairs) = length(getfield(v, :itr)) +axes(v::Pairs) = axes(getfield(v, :itr)) +size(v::Pairs) = size(getfield(v, :itr)) + +@propagate_inbounds function _pairs_elt(p::Pairs{K, V}, idx) where {K, V} + return Pair{K, V}(idx, getfield(p, :data)[idx]) +end + +@propagate_inbounds function iterate(p::Pairs{K, V}, state...) where {K, V} + x = iterate(getfield(p, :itr), state...) + x === nothing && return x + idx, next = x + return (_pairs_elt(p, idx), next) +end + +@propagate_inbounds function iterate(r::Reverse{<:Pairs}, state=(reverse(getfield(r.itr, :itr)),)) + x = iterate(state...) x === nothing && return x - indx, n = x - item = v.data[indx] - return (Pair{K, V}(indx, item), n) + idx, next = x + return (_pairs_elt(r.itr, idx), (state[1], next)) end -@inline isdone(v::Pairs, state...) = isdone(v.itr, state...) + +@inline isdone(v::Pairs, state...) = isdone(getfield(v, :itr), state...) IteratorSize(::Type{<:Pairs{<:Any, <:Any, I}}) where {I} = IteratorSize(I) IteratorSize(::Type{<:Pairs{<:Any, <:Any, <:Base.AbstractUnitRange, <:Tuple}}) = HasLength() -reverse(v::Pairs) = Pairs(v.data, reverse(v.itr)) +function last(v::Pairs{K, V}) where {K, V} + idx = last(getfield(v, :itr)) + return Pair{K, V}(idx, v[idx]) +end -haskey(v::Pairs, key) = (key in v.itr) -keys(v::Pairs) = v.itr -values(v::Pairs) = v.data -getindex(v::Pairs, key) = v.data[key] -setindex!(v::Pairs, value, key) = (v.data[key] = value; v) -get(v::Pairs, key, default) = get(v.data, key, default) -get(f::Base.Callable, v::Pairs, key) = get(f, v.data, key) +haskey(v::Pairs, key) = (key in getfield(v, :itr)) +keys(v::Pairs) = getfield(v, :itr) +values(v::Pairs) = getfield(v, :data) # TODO: this should be a view of data subset by itr +getindex(v::Pairs, key) = getfield(v, :data)[key] +setindex!(v::Pairs, value, key) = (getfield(v, :data)[key] = value; v) +get(v::Pairs, key, default) = get(getfield(v, :data), key, default) +get(f::Base.Callable, v::Pairs, key) = get(f, getfield(v, :data), key) # zip @@ -288,6 +300,8 @@ the `zip` iterator is a tuple of values of its subiterators. `zip` orders the calls to its subiterators in such a way that stateful iterators will not advance when another iterator finishes in the current iteration. +See also: [`enumerate`](@ref), [`splat`](@ref Base.splat). + # Examples ```jldoctest julia> a = 1:5 @@ -408,7 +422,8 @@ zip_iteratoreltype() = HasEltype() zip_iteratoreltype(a) = a zip_iteratoreltype(a, tail...) = and_iteratoreltype(a, zip_iteratoreltype(tail...)) -reverse(z::Zip) = Zip(Base.map(reverse, z.is)) +reverse(z::Zip) = Zip(Base.map(reverse, z.is)) # n.b. we assume all iterators are the same length +last(z::Zip) = getindex.(z.is, minimum(Base.map(lastindex, z.is))) # filter @@ -441,6 +456,12 @@ julia> foreach(println, f) 1 3 5 + +julia> [x for x in [1, 2, 3, 4, 5] if isodd(x)] # collects a generator over Iterators.filter +3-element Vector{Int64}: + 1 + 3 + 5 ``` """ filter(flt, itr) = Filter(flt, itr) @@ -461,6 +482,7 @@ IteratorEltype(::Type{Filter{F,I}}) where {F,I} = IteratorEltype(I) IteratorSize(::Type{<:Filter}) = SizeUnknown() reverse(f::Filter) = Filter(f.flt, reverse(f.itr)) +last(f::Filter) = first(reverse(f)) # Accumulate -- partial reductions of a function over an iterator @@ -484,20 +506,22 @@ This is effectively a lazy version of [`Base.accumulate`](@ref). # Examples ```jldoctest -julia> f = Iterators.accumulate(+, [1,2,3,4]); +julia> a = Iterators.accumulate(+, [1,2,3,4]); -julia> foreach(println, f) +julia> foreach(println, a) 1 3 6 10 -julia> f = Iterators.accumulate(+, [1,2,3]; init = 100); +julia> b = Iterators.accumulate(/, (2, 5, 2, 5); init = 100); -julia> foreach(println, f) -101 -103 -106 +julia> collect(b) +4-element Vector{Float64}: + 50.0 + 10.0 + 5.0 + 1.0 ``` """ accumulate(f, itr; init = Base._InitialValue()) = Accumulate(f, itr, init) @@ -538,6 +562,8 @@ end An iterator that yields the same elements as `iter`, but starting at the given `state`. +See also: [`Iterators.drop`](@ref), [`Iterators.peel`](@ref), [`Base.rest`](@ref). + # Examples ```jldoctest julia> collect(Iterators.rest([1,2,3,4], 2)) @@ -556,6 +582,13 @@ rest(itr) = itr Returns the first element and an iterator over the remaining elements. +If the iterator is empty return `nothing` (like `iterate`). + +!!! compat "Julia 1.7" + Prior versions throw a BoundsError if the iterator is empty. + +See also: [`Iterators.drop`](@ref), [`Iterators.take`](@ref). + # Examples ```jldoctest julia> (a, rest) = Iterators.peel("abc"); @@ -571,7 +604,7 @@ julia> collect(rest) """ function peel(itr) y = iterate(itr) - y === nothing && throw(BoundsError()) + y === nothing && return y val, s = y val, rest(itr, s) end @@ -587,8 +620,8 @@ IteratorSize(::Type{<:Rest{I}}) where {I} = rest_iteratorsize(IteratorSize(I)) # Count -- infinite counting -struct Count{S<:Number} - start::S +struct Count{T,S} + start::T step::S end @@ -608,11 +641,13 @@ julia> for v in Iterators.countfrom(5, 2) 9 ``` """ -countfrom(start::Number, step::Number) = Count(promote(start, step)...) -countfrom(start::Number) = Count(start, oneunit(start)) -countfrom() = Count(1, 1) +countfrom(start::T, step::S) where {T,S} = Count{typeof(start+step),S}(start, step) +countfrom(start::Number, step::Number) = Count(promote(start, step)...) +countfrom(start) = Count(start, oneunit(start)) +countfrom() = Count(1, 1) + -eltype(::Type{Count{S}}) where {S} = S +eltype(::Type{<:Count{T}}) where {T} = T iterate(it::Count, state=it.start) = (state, state + it.step) @@ -634,6 +669,8 @@ end An iterator that generates at most the first `n` elements of `iter`. +See also: [`drop`](@ref Iterators.drop), [`peel`](@ref Iterators.peel), [`first`](@ref), [`take!`](@ref). + # Examples ```jldoctest julia> a = 1:2:11 @@ -779,7 +816,7 @@ end IteratorSize(::Type{<:TakeWhile}) = SizeUnknown() eltype(::Type{TakeWhile{I,P}} where P) where {I} = eltype(I) -IteratorEltype(::Type{TakeWhile{I}} where P) where {I} = IteratorEltype(I) +IteratorEltype(::Type{TakeWhile{I, P}} where P) where {I} = IteratorEltype(I) # dropwhile @@ -845,6 +882,8 @@ end An iterator that cycles through `iter` forever. If `iter` is empty, so is `cycle(iter)`. +See also: [`Iterators.repeated`](@ref), [`repeat`](@ref). + # Examples ```jldoctest julia> for (i, v) in enumerate(Iterators.cycle("hello")) @@ -870,6 +909,7 @@ function iterate(it::Cycle, state) end reverse(it::Cycle) = Cycle(reverse(it.xs)) +last(it::Cycle) = last(it.xs) # Repeated - repeat an object infinitely many times @@ -884,6 +924,8 @@ repeated(x) = Repeated(x) An iterator that generates the value `x` forever. If `n` is specified, generates `x` that many times (equivalent to `take(repeated(x), n)`). +See also: [`Iterators.cycle`](@ref), [`repeat`](@ref). + # Examples ```jldoctest julia> a = Iterators.repeated([1 2], 4); @@ -906,6 +948,7 @@ IteratorSize(::Type{<:Repeated}) = IsInfinite() IteratorEltype(::Type{<:Repeated}) = HasEltype() reverse(it::Union{Repeated,Take{<:Repeated}}) = it +last(it::Union{Repeated,Take{<:Repeated}}) = first(it) # Product -- cartesian product of iterators struct ProductIterator{T<:Tuple} @@ -919,12 +962,17 @@ Return an iterator over the product of several iterators. Each generated element a tuple whose `i`th element comes from the `i`th argument iterator. The first iterator changes the fastest. +See also: [`zip`](@ref), [`Iterators.flatten`](@ref). + # Examples ```jldoctest julia> collect(Iterators.product(1:2, 3:5)) 2×3 Matrix{Tuple{Int64, Int64}}: (1, 3) (1, 4) (1, 5) (2, 3) (2, 4) (2, 5) + +julia> ans == [(x,y) for x in 1:2, y in 3:5] # collects a generator involving Iterators.product +true ``` """ product(iters...) = ProductIterator(iters) @@ -1032,6 +1080,7 @@ end end reverse(p::ProductIterator) = ProductIterator(Base.map(reverse, p.iterators)) +last(p::ProductIterator) = Base.map(last, p.iterators) # flatten an iterator of iterators @@ -1054,6 +1103,15 @@ julia> collect(Iterators.flatten((1:2, 8:9))) 2 8 9 + +julia> [(x,y) for x in 0:1 for y in 'a':'c'] # collects generators involving Iterators.flatten +6-element Vector{Tuple{Int64, Char}}: + (0, 'a') + (0, 'b') + (0, 'c') + (1, 'a') + (1, 'b') + (1, 'c') ``` """ flatten(itr) = Flatten(itr) @@ -1102,6 +1160,34 @@ length(f::Flatten{Tuple{}}) = 0 end reverse(f::Flatten) = Flatten(reverse(itr) for itr in reverse(f.it)) +last(f::Flatten) = last(last(f.it)) + +""" + Iterators.flatmap(f, iterators...) + +Equivalent to `flatten(map(f, iterators...))`. + +See also [`Iterators.flatten`](@ref), [`Iterators.map`](@ref). + +!!! compat "Julia 1.9" + This function was added in Julia 1.9. + +# Examples +```jldoctest +julia> Iterators.flatmap(n->-n:2:n, 1:3) |> collect +9-element Vector{Int64}: + -1 + 1 + -2 + 0 + 2 + -3 + -1 + 1 + 3 +``` +""" +flatmap(f, c...) = flatten(map(f, c...)) """ partition(collection, n) @@ -1148,29 +1234,29 @@ end function length(itr::PartitionIterator) l = length(itr.c) - return div(l, itr.n) + ((mod(l, itr.n) > 0) ? 1 : 0) + return cld(l, itr.n) end -function iterate(itr::PartitionIterator{<:AbstractRange}, state=1) - state > length(itr.c) && return nothing - r = min(state + itr.n - 1, length(itr.c)) +function iterate(itr::PartitionIterator{<:AbstractRange}, state = firstindex(itr.c)) + state > lastindex(itr.c) && return nothing + r = min(state + itr.n - 1, lastindex(itr.c)) return @inbounds itr.c[state:r], r + 1 end -function iterate(itr::PartitionIterator{<:AbstractArray}, state=1) - state > length(itr.c) && return nothing - r = min(state + itr.n - 1, length(itr.c)) +function iterate(itr::PartitionIterator{<:AbstractArray}, state = firstindex(itr.c)) + state > lastindex(itr.c) && return nothing + r = min(state + itr.n - 1, lastindex(itr.c)) return @inbounds view(itr.c, state:r), r + 1 end struct IterationCutShort; end function iterate(itr::PartitionIterator, state...) - v = Vector{eltype(itr.c)}(undef, itr.n) # This is necessary to remember whether we cut the # last element short. In such cases, we do return that # element, but not the next one state === (IterationCutShort(),) && return nothing + v = Vector{eltype(itr.c)}(undef, itr.n) i = 0 y = iterate(itr.c, state...) while y !== nothing @@ -1223,6 +1309,12 @@ julia> collect(a) 2-element Vector{Char}: 'e': ASCII/Unicode U+0065 (category Ll: Letter, lowercase) 'f': ASCII/Unicode U+0066 (category Ll: Letter, lowercase) + +julia> Iterators.reset!(a); popfirst!(a) +'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase) + +julia> Iterators.reset!(a, "hello"); popfirst!(a) +'h': ASCII/Unicode U+0068 (category Ll: Letter, lowercase) ``` ```jldoctest @@ -1251,7 +1343,7 @@ mutable struct Stateful{T, VS} end end -function reset!(s::Stateful{T,VS}, itr::T) where {T,VS} +function reset!(s::Stateful{T,VS}, itr::T=s.itr) where {T,VS} s.itr = itr setfield!(s, :nextvalstate, iterate(itr)) s.taken = 0 @@ -1266,7 +1358,7 @@ else # fixpoint. approx_iter_type(itrT::Type) = _approx_iter_type(itrT, Base._return_type(iterate, Tuple{itrT})) # Not actually called, just passed to return type to avoid - # having to typesubtract + # having to typesplit on Nothing function doiterate(itr, valstate::Union{Nothing, Tuple{Any, Any}}) valstate === nothing && return nothing val, st = valstate @@ -1296,7 +1388,10 @@ convert(::Type{Stateful}, itr) = Stateful(itr) end end -@inline peek(s::Stateful, sentinel=nothing) = s.nextvalstate !== nothing ? s.nextvalstate[1] : sentinel +@inline function peek(s::Stateful, sentinel=nothing) + ns = s.nextvalstate + return ns !== nothing ? ns[1] : sentinel +end @inline iterate(s::Stateful, state=nothing) = s.nextvalstate === nothing ? nothing : (popfirst!(s), nothing) IteratorSize(::Type{Stateful{T,VS}}) where {T,VS} = IteratorSize(T) isa HasShape ? HasLength() : IteratorSize(T) eltype(::Type{Stateful{T, VS}} where VS) where {T} = eltype(T) @@ -1306,20 +1401,39 @@ length(s::Stateful) = length(s.itr) - s.taken """ only(x) -Returns the one and only element of collection `x`, and throws an `ArgumentError` if the +Return the one and only element of collection `x`, or throw an [`ArgumentError`](@ref) if the collection has zero or multiple elements. -See also: [`first`](@ref), [`last`](@ref). +See also [`first`](@ref), [`last`](@ref). !!! compat "Julia 1.4" This method requires at least Julia 1.4. + +# Examples +```jldoctest +julia> only(["a"]) +"a" + +julia> only("a") +'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase) + +julia> only(()) +ERROR: ArgumentError: Tuple contains 0 elements, must contain exactly 1 element +Stacktrace: +[...] + +julia> only(('a', 'b')) +ERROR: ArgumentError: Tuple contains 2 elements, must contain exactly 1 element +Stacktrace: +[...] +``` """ @propagate_inbounds function only(x) i = iterate(x) @boundscheck if i === nothing throw(ArgumentError("Collection is empty, must contain exactly 1 element")) end - (ret, state) = i + (ret, state) = i::NTuple{2,Any} @boundscheck if iterate(x, state) !== nothing throw(ArgumentError("Collection has multiple elements, must contain exactly 1 element")) end @@ -1340,4 +1454,7 @@ only(x::NamedTuple) = throw( ArgumentError("NamedTuple contains $(length(x)) elements, must contain exactly 1 element") ) + +Base.intersect(a::ProductIterator, b::ProductIterator) = ProductIterator(intersect.(a.iterators, b.iterators)) + end diff --git a/base/libc.jl b/base/libc.jl index 547561ac964bac..7d88e89bf605a4 100644 --- a/base/libc.jl +++ b/base/libc.jl @@ -131,7 +131,7 @@ Suspends execution for `s` seconds. This function does not yield to Julia's scheduler and therefore blocks the Julia thread that it is running on for the duration of the sleep time. -See also: [`sleep`](@ref) +See also [`sleep`](@ref). """ systemsleep @@ -255,7 +255,7 @@ time() = ccall(:jl_clock_now, Float64, ()) Get Julia's process ID. """ -getpid() = ccall(:jl_getpid, Int32, ()) +getpid() = ccall(:uv_os_getpid, Int32, ()) ## network functions ## @@ -376,31 +376,111 @@ free(p::Cwstring) = free(convert(Ptr{Cwchar_t}, p)) ## Random numbers ## +# Access to very high quality (kernel) randomness +function getrandom!(A::Union{Array,Base.RefValue}) + ret = ccall(:uv_random, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}, Csize_t, Cuint, Ptr{Cvoid}), + C_NULL, C_NULL, A, sizeof(A), 0, C_NULL) + Base.uv_error("getrandom", ret) + return A +end +_make_uint64_seed() = getrandom!(Base.RefValue{UInt64}())[] + # To limit dependency on rand functionality implemented in the Random module, -# Libc.rand is used in file.jl, and could be used in error.jl (but it breaks a test) +# Libc.rand is used in Base (it also is independent from Random.seed, so is +# only affected by `Libc.srand(seed)` calls) """ - rand([T::Type]) + rand([T::Type]=UInt32) -Interface to the C `rand()` function. If `T` is provided, generate a value of type `T` -by composing two calls to `rand()`. `T` can be `UInt32` or `Float64`. +Generate a random number of type `T`. `T` can be `UInt32` or `Float64`. """ -rand() = ccall(:rand, Cint, ()) -@static if Sys.iswindows() - # Windows RAND_MAX is 2^15-1 - rand(::Type{UInt32}) = ((rand() % UInt32) << 17) ⊻ ((rand() % UInt32) << 8) ⊻ (rand() % UInt32) -else - # RAND_MAX is at least 2^15-1 in theory, but we assume 2^16-1 - # on non-Windows systems (in practice, it's 2^31-1) - rand(::Type{UInt32}) = ((rand() % UInt32) << 16) ⊻ (rand() % UInt32) -end -rand(::Type{Float64}) = rand(UInt32) * 2.0^-32 +rand() = ccall(:jl_rand, UInt64, ()) % UInt32 +rand(::Type{UInt32}) = rand() +rand(::Type{Float64}) = rand() * 2.0^-32 """ srand([seed]) -Interface to the C `srand(seed)` function. +Set a value for the current global `seed`. """ -srand(seed=floor(Int, time()) % Cuint) = ccall(:srand, Cvoid, (Cuint,), seed) +function srand(seed::Integer=_make_uint64_seed()) + ccall(:jl_srand, Cvoid, (UInt64,), seed % UInt64) +end + +struct Cpasswd + username::Cstring + uid::Culong + gid::Culong + shell::Cstring + homedir::Cstring + gecos::Cstring + Cpasswd() = new(C_NULL, typemax(Culong), typemax(Culong), C_NULL, C_NULL, C_NULL) +end +mutable struct Cgroup + groupname::Cstring # group name + gid::Culong # group ID + mem::Ptr{Cstring} # group members + Cgroup() = new(C_NULL, typemax(Culong), C_NULL) +end +struct Passwd + username::String + uid::UInt + gid::UInt + shell::String + homedir::String + gecos::String +end +struct Group + groupname::String + gid::UInt + mem::Vector{String} +end + +function getpwuid(uid::Unsigned, throw_error::Bool=true) + ref_pd = Ref(Cpasswd()) + ret = ccall(:uv_os_get_passwd2, Cint, (Ref{Cpasswd}, Culong), ref_pd, uid) + if ret != 0 + throw_error && Base.uv_error("getpwuid", ret) + return + end + pd = ref_pd[] + pd = Passwd( + pd.username == C_NULL ? "" : unsafe_string(pd.username), + pd.uid, + pd.gid, + pd.shell == C_NULL ? "" : unsafe_string(pd.shell), + pd.homedir == C_NULL ? "" : unsafe_string(pd.homedir), + pd.gecos == C_NULL ? "" : unsafe_string(pd.gecos), + ) + ccall(:uv_os_free_passwd, Cvoid, (Ref{Cpasswd},), ref_pd) + return pd +end +function getgrgid(gid::Unsigned, throw_error::Bool=true) + ref_gp = Ref(Cgroup()) + ret = ccall(:uv_os_get_group, Cint, (Ref{Cgroup}, Culong), ref_gp, gid) + if ret != 0 + throw_error && Base.uv_error("getgrgid", ret) + return + end + gp = ref_gp[] + members = String[] + if gp.mem != C_NULL + while true + mem = unsafe_load(gp.mem, length(members) + 1) + mem == C_NULL && break + push!(members, unsafe_string(mem)) + end + end + gp = Group( + gp.groupname == C_NULL ? "" : unsafe_string(gp.groupname), + gp.gid, + members, + ) + ccall(:uv_os_free_group, Cvoid, (Ref{Cgroup},), ref_gp) + return gp +end + +getuid() = ccall(:jl_getuid, Culong, ()) +geteuid() = ccall(:jl_geteuid, Culong, ()) # Include dlopen()/dlpath() code include("libdl.jl") diff --git a/base/libdl.jl b/base/libdl.jl index c20b8168e2fec9..4f29260bb24f82 100644 --- a/base/libdl.jl +++ b/base/libdl.jl @@ -46,9 +46,12 @@ applicable. (RTLD_DEEPBIND, RTLD_FIRST, RTLD_GLOBAL, RTLD_LAZY, RTLD_LOCAL, RTLD_NODELETE, RTLD_NOLOAD, RTLD_NOW) """ - dlsym(handle, sym) + dlsym(handle, sym; throw_error::Bool = true) Look up a symbol from a shared library handle, return callable function pointer on success. + +If the symbol cannot be found, this method throws an error, unless the keyword argument +`throw_error` is set to `false`, in which case this method returns `nothing`. """ function dlsym(hnd::Ptr, s::Union{Symbol,AbstractString}; throw_error::Bool = true) hnd == C_NULL && throw(ArgumentError("NULL library handle")) diff --git a/base/libuv.jl b/base/libuv.jl index 82298516f4a1b9..64b228c6500e75 100644 --- a/base/libuv.jl +++ b/base/libuv.jl @@ -61,8 +61,11 @@ function preserve_handle(x) end function unpreserve_handle(x) lock(preserve_handle_lock) - v = uvhandles[x]::Int - if v == 1 + v = get(uvhandles, x, 0)::Int + if v == 0 + unlock(preserve_handle_lock) + error("unbalanced call to unpreserve_handle for $(typeof(x))") + elseif v == 1 pop!(uvhandles, x) else uvhandles[x] = v - 1 @@ -74,7 +77,7 @@ end ## Libuv error handling ## struct IOError <: Exception - msg::AbstractString + msg::String code::Int32 IOError(msg::AbstractString, code::Integer) = new(msg, code) end @@ -107,6 +110,7 @@ end function uv_alloc_buf end function uv_readcb end function uv_writecb_task end +function uv_shutdowncb_task end function uv_return_spawn end function uv_asynccb end function uv_timercb end @@ -129,21 +133,21 @@ function reinit_stdio() end """ - stdin + stdin::IO Global variable referring to the standard input stream. """ :stdin """ - stdout + stdout::IO Global variable referring to the standard out stream. """ :stdout """ - stderr + stderr::IO Global variable referring to the standard error stream. """ diff --git a/base/linked_list.jl b/base/linked_list.jl index beceb24a27f40e..c477dc56bdb2b6 100644 --- a/base/linked_list.jl +++ b/base/linked_list.jl @@ -1,23 +1,23 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -mutable struct InvasiveLinkedList{T} +mutable struct IntrusiveLinkedList{T} # Invasive list requires that T have a field `.next >: U{T, Nothing}` and `.queue >: U{ILL{T}, Nothing}` head::Union{T, Nothing} tail::Union{T, Nothing} - InvasiveLinkedList{T}() where {T} = new{T}(nothing, nothing) + IntrusiveLinkedList{T}() where {T} = new{T}(nothing, nothing) end #const list_append!! = append! #const list_deletefirst! = delete! -eltype(::Type{<:InvasiveLinkedList{T}}) where {T} = @isdefined(T) ? T : Any +eltype(::Type{<:IntrusiveLinkedList{T}}) where {T} = @isdefined(T) ? T : Any -iterate(q::InvasiveLinkedList) = (h = q.head; h === nothing ? nothing : (h, h)) -iterate(q::InvasiveLinkedList{T}, v::T) where {T} = (h = v.next; h === nothing ? nothing : (h, h)) +iterate(q::IntrusiveLinkedList) = (h = q.head; h === nothing ? nothing : (h, h)) +iterate(q::IntrusiveLinkedList{T}, v::T) where {T} = (h = v.next; h === nothing ? nothing : (h, h)) -isempty(q::InvasiveLinkedList) = (q.head === nothing) +isempty(q::IntrusiveLinkedList) = (q.head === nothing) -function length(q::InvasiveLinkedList) +function length(q::IntrusiveLinkedList) i = 0 head = q.head while head !== nothing @@ -27,7 +27,7 @@ function length(q::InvasiveLinkedList) return i end -function list_append!!(q::InvasiveLinkedList{T}, q2::InvasiveLinkedList{T}) where T +function list_append!!(q::IntrusiveLinkedList{T}, q2::IntrusiveLinkedList{T}) where T q === q2 && error("can't append list to itself") head2 = q2.head if head2 !== nothing @@ -49,7 +49,7 @@ function list_append!!(q::InvasiveLinkedList{T}, q2::InvasiveLinkedList{T}) wher return q end -function push!(q::InvasiveLinkedList{T}, val::T) where T +function push!(q::IntrusiveLinkedList{T}, val::T) where T val.queue === nothing || error("val already in a list") val.queue = q tail = q.tail @@ -62,7 +62,7 @@ function push!(q::InvasiveLinkedList{T}, val::T) where T return q end -function pushfirst!(q::InvasiveLinkedList{T}, val::T) where T +function pushfirst!(q::IntrusiveLinkedList{T}, val::T) where T val.queue === nothing || error("val already in a list") val.queue = q head = q.head @@ -75,19 +75,20 @@ function pushfirst!(q::InvasiveLinkedList{T}, val::T) where T return q end -function pop!(q::InvasiveLinkedList{T}) where {T} +function pop!(q::IntrusiveLinkedList{T}) where {T} val = q.tail::T list_deletefirst!(q, val) # expensive! return val end -function popfirst!(q::InvasiveLinkedList{T}) where {T} +function popfirst!(q::IntrusiveLinkedList{T}) where {T} val = q.head::T list_deletefirst!(q, val) # cheap return val end -function list_deletefirst!(q::InvasiveLinkedList{T}, val::T) where T +# this function assumes `val` is found in `q` +function list_deletefirst!(q::IntrusiveLinkedList{T}, val::T) where T val.queue === q || return head = q.head::T if head === val @@ -97,10 +98,10 @@ function list_deletefirst!(q::InvasiveLinkedList{T}, val::T) where T q.head = val.next::T end else - head_next = head.next + head_next = head.next::T while head_next !== val head = head_next - head_next = head.next::Union{T, Nothing} + head_next = head.next::T end if q.tail::T === val head.next = nothing @@ -124,20 +125,20 @@ end mutable struct LinkedListItem{T} # Adapter class to use any `T` in a LinkedList next::Union{LinkedListItem{T}, Nothing} - queue::Union{InvasiveLinkedList{LinkedListItem{T}}, Nothing} + queue::Union{IntrusiveLinkedList{LinkedListItem{T}}, Nothing} value::T LinkedListItem{T}(value::T) where {T} = new{T}(nothing, nothing, value) end -const LinkedList{T} = InvasiveLinkedList{LinkedListItem{T}} +const LinkedList{T} = IntrusiveLinkedList{LinkedListItem{T}} # delegate methods, as needed eltype(::Type{<:LinkedList{T}}) where {T} = @isdefined(T) ? T : Any iterate(q::LinkedList) = (h = q.head; h === nothing ? nothing : (h.value, h)) -iterate(q::InvasiveLinkedList{LLT}, v::LLT) where {LLT<:LinkedListItem} = (h = v.next; h === nothing ? nothing : (h.value, h)) +iterate(q::IntrusiveLinkedList{LLT}, v::LLT) where {LLT<:LinkedListItem} = (h = v.next; h === nothing ? nothing : (h.value, h)) push!(q::LinkedList{T}, val::T) where {T} = push!(q, LinkedListItem{T}(val)) pushfirst!(q::LinkedList{T}, val::T) where {T} = pushfirst!(q, LinkedListItem{T}(val)) -pop!(q::LinkedList) = invoke(pop!, Tuple{InvasiveLinkedList,}, q).value -popfirst!(q::LinkedList) = invoke(popfirst!, Tuple{InvasiveLinkedList,}, q).value +pop!(q::LinkedList) = invoke(pop!, Tuple{IntrusiveLinkedList,}, q).value +popfirst!(q::LinkedList) = invoke(popfirst!, Tuple{IntrusiveLinkedList,}, q).value function list_deletefirst!(q::LinkedList{T}, val::T) where T h = q.head while h !== nothing diff --git a/base/loading.jl b/base/loading.jl index 84150004e48106..7588aaa3cbc17e 100644 --- a/base/loading.jl +++ b/base/loading.jl @@ -1,16 +1,17 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license # Base.require is the implementation for the `import` statement +const require_lock = ReentrantLock() # Cross-platform case-sensitive path canonicalization if Sys.isunix() && !Sys.isapple() # assume case-sensitive filesystems, don't have to do anything - isfile_casesensitive(path) = isfile(path) + isfile_casesensitive(path) = isaccessiblefile(path) elseif Sys.iswindows() # GetLongPathName Win32 function returns the case-preserved filename on NTFS. function isfile_casesensitive(path) - isfile(path) || return false # Fail fast + isaccessiblefile(path) || return false # Fail fast basename(Filesystem.longpath(path)) == basename(path) end elseif Sys.isapple() @@ -43,7 +44,7 @@ elseif Sys.isapple() # Buffer buf; # getattrpath(path, &attr_list, &buf, sizeof(buf), FSOPT_NOFOLLOW); function isfile_casesensitive(path) - isfile(path) || return false + isaccessiblefile(path) || return false path_basename = String(basename(path)) local casepreserved_basename header_size = 12 @@ -74,21 +75,50 @@ elseif Sys.isapple() else # Generic fallback that performs a slow directory listing. function isfile_casesensitive(path) - isfile(path) || return false + isaccessiblefile(path) || return false dir, filename = splitdir(path) any(readdir(dir) .== filename) end end +# Check if the file is accessible. If stat fails return `false` + +function isaccessibledir(dir) + return try + isdir(dir) + catch err + err isa IOError || rethrow() + false + end +end + +function isaccessiblefile(file) + return try + isfile(file) + catch err + err isa IOError || rethrow() + false + end +end + +function isaccessiblepath(path) + return try + ispath(path) + catch err + err isa IOError || rethrow() + false + end +end + ## SHA1 ## struct SHA1 - bytes::Vector{UInt8} - function SHA1(bytes::Vector{UInt8}) - length(bytes) == 20 || - throw(ArgumentError("wrong number of bytes for SHA1 hash: $(length(bytes))")) - return new(bytes) - end + bytes::NTuple{20, UInt8} +end +function SHA1(bytes::Vector{UInt8}) + length(bytes) == 20 || + throw(ArgumentError("wrong number of bytes for SHA1 hash: $(length(bytes))")) + return SHA1(ntuple(i->bytes[i], Val(20))) end SHA1(s::AbstractString) = SHA1(hex2bytes(s)) parse(::Type{SHA1}, s::AbstractString) = SHA1(s) @@ -129,12 +159,23 @@ end const ns_dummy_uuid = UUID("fe0723d6-3a44-4c41-8065-ee0f42c8ceab") function dummy_uuid(project_file::String) + @lock require_lock begin + cache = LOADING_CACHE[] + if cache !== nothing + uuid = get(cache.dummy_uuid, project_file, nothing) + uuid === nothing || return uuid + end project_path = try realpath(project_file) catch project_file end - return uuid5(ns_dummy_uuid, project_path) + uuid = uuid5(ns_dummy_uuid, project_path) + if cache !== nothing + cache.dummy_uuid[project_file] = uuid + end + return uuid + end end ## package path slugs: turning UUID + SHA1 into a pair of 4-byte "slugs" ## @@ -163,16 +204,92 @@ function version_slug(uuid::UUID, sha1::SHA1, p::Int=5) return slug(crc, p) end -struct TOMLCache - p::TOML.Parser - d::Dict{String, Dict{String, Any}} +mutable struct CachedTOMLDict + path::String + inode::UInt64 + mtime::Float64 + size::Int64 + hash::UInt32 + d::Dict{String, Any} +end + +function CachedTOMLDict(p::TOML.Parser, path::String) + s = stat(path) + content = read(path) + crc32 = _crc32c(content) + TOML.reinit!(p, String(content); filepath=path) + d = TOML.parse(p) + return CachedTOMLDict( + path, + s.inode, + s.mtime, + s.size, + crc32, + d, + ) +end + +function get_updated_dict(p::TOML.Parser, f::CachedTOMLDict) + s = stat(f.path) + time_since_cached = time() - f.mtime + rough_mtime_granularity = 0.1 # seconds + # In case the file is being updated faster than the mtime granularity, + # and have the same size after the update we might miss that it changed. Therefore + # always check the hash in case we recently created the cache. + if time_since_cached < rough_mtime_granularity || s.inode != f.inode || s.mtime != f.mtime || f.size != s.size + content = read(f.path) + new_hash = _crc32c(content) + if new_hash != f.hash + f.inode = s.inode + f.mtime = s.mtime + f.size = s.size + f.hash = new_hash + TOML.reinit!(p, String(content); filepath=f.path) + return f.d = TOML.parse(p) + end + end + return f.d +end + +struct LoadingCache + load_path::Vector{String} + dummy_uuid::Dict{String, UUID} + env_project_file::Dict{String, Union{Bool, String}} + project_file_manifest_path::Dict{String, Union{Nothing, String}} + require_parsed::Set{String} end -TOMLCache() = TOMLCache(TOML.Parser(), Dict{String, Dict{String, Any}}()) +const LOADING_CACHE = Ref{Union{LoadingCache, Nothing}}(nothing) +LoadingCache() = LoadingCache(load_path(), Dict(), Dict(), Dict(), Set()) -function parsed_toml(cache::TOMLCache, project_file::String) - get!(cache.d, project_file) do - TOML.reinit!(cache.p, read(project_file, String); filepath=project_file) - TOML.parse(cache.p) + +struct TOMLCache + p::TOML.Parser + d::Dict{String, CachedTOMLDict} +end +const TOML_CACHE = TOMLCache(TOML.Parser(), Dict{String, Dict{String, Any}}()) + +parsed_toml(project_file::AbstractString) = parsed_toml(project_file, TOML_CACHE, require_lock) +function parsed_toml(project_file::AbstractString, toml_cache::TOMLCache, toml_lock::ReentrantLock) + lock(toml_lock) do + cache = LOADING_CACHE[] + dd = if !haskey(toml_cache.d, project_file) + d = CachedTOMLDict(toml_cache.p, project_file) + toml_cache.d[project_file] = d + d.d + else + d = toml_cache.d[project_file] + # We are in a require call and have already parsed this TOML file + # assume that it is unchanged to avoid hitting disk + if cache !== nothing && project_file in cache.require_parsed + d.d + else + get_updated_dict(toml_cache.p, d) + end + end + if cache !== nothing + push!(cache.require_parsed, project_file) + end + return dd end end @@ -180,22 +297,21 @@ end # Used by Pkg but not used in loading itself function find_package(arg) - cache = TOMLCache() - pkg = identify_package(arg, cache) + pkg = identify_package(arg) pkg === nothing && return nothing - return locate_package(pkg, cache) + return locate_package(pkg) end ## package identity: given a package name and a context, try to return its identity ## -identify_package(where::Module, name::String, cache::TOMLCache = TOMLCache()) = identify_package(PkgId(where), name, cache) +identify_package(where::Module, name::String) = identify_package(PkgId(where), name) # identify_package computes the PkgId for `name` from the context of `where` # or return `nothing` if no mapping exists for it -function identify_package(where::PkgId, name::String, cache::TOMLCache=TOMLCache())::Union{Nothing,PkgId} +function identify_package(where::PkgId, name::String)::Union{Nothing,PkgId} where.name === name && return where - where.uuid === nothing && return identify_package(name, cache) # ignore `where` + where.uuid === nothing && return identify_package(name) # ignore `where` for env in load_path() - uuid = manifest_deps_get(env, where, name, cache) + uuid = manifest_deps_get(env, where, name) uuid === nothing && continue # not found--keep looking uuid.uuid === nothing || return uuid # found in explicit environment--use it return nothing # found in implicit environment--return "not found" @@ -205,35 +321,39 @@ end # identify_package computes the PkgId for `name` from toplevel context # by looking through the Project.toml files and directories -function identify_package(name::String, cache::TOMLCache=TOMLCache())::Union{Nothing,PkgId} +function identify_package(name::String)::Union{Nothing,PkgId} for env in load_path() - uuid = project_deps_get(env, name, cache) + uuid = project_deps_get(env, name) uuid === nothing || return uuid # found--return it end return nothing end ## package location: given a package identity, find file to load ## -function locate_package(pkg::PkgId, cache::TOMLCache=TOMLCache())::Union{Nothing,String} +function locate_package(pkg::PkgId)::Union{Nothing,String} if pkg.uuid === nothing for env in load_path() # look for the toplevel pkg `pkg.name` in this entry - found = project_deps_get(env, pkg.name, cache) + found = project_deps_get(env, pkg.name) found === nothing && continue if pkg == found # pkg.name is present in this directory or project file, # return the path the entry point for the code, if it could be found # otherwise, signal failure - return implicit_manifest_uuid_path(env, pkg, cache) + return implicit_manifest_uuid_path(env, pkg) end @assert found.uuid !== nothing - return locate_package(found, cache) # restart search now that we know the uuid for pkg + return locate_package(found) # restart search now that we know the uuid for pkg end else for env in load_path() - path = manifest_uuid_path(env, pkg, cache) + path = manifest_uuid_path(env, pkg) path === nothing || return entry_path(path, pkg.name) end + # Allow loading of stdlibs if the name/uuid are given + # e.g. if they have been explicitly added to the project/manifest + path = manifest_uuid_path(Sys.STDLIB, pkg) + path === nothing || return entry_path(path, pkg.name) end return nothing end @@ -248,125 +368,179 @@ Use [`dirname`](@ref) to get the directory part and [`basename`](@ref) to get the file name part of the path. """ function pathof(m::Module) - pkgid = get(Base.module_keys, m, nothing) + @lock require_lock begin + pkgid = get(module_keys, m, nothing) pkgid === nothing && return nothing - origin = get(Base.pkgorigins, pkgid, nothing) + origin = get(pkgorigins, pkgid, nothing) origin === nothing && return nothing - return origin.path + path = origin.path + path === nothing && return nothing + return fixup_stdlib_path(path) + end end """ - pkgdir(m::Module) + pkgdir(m::Module[, paths::String...]) + +Return the root directory of the package that imported module `m`, +or `nothing` if `m` was not imported from a package. Optionally further +path component strings can be provided to construct a path within the +package root. - Return the root directory of the package that imported module `m`, - or `nothing` if `m` was not imported from a package. - """ -function pkgdir(m::Module) - rootmodule = Base.moduleroot(m) +```julia-repl +julia> pkgdir(Foo) +"/path/to/Foo.jl" + +julia> pkgdir(Foo, "src", "file.jl") +"/path/to/Foo.jl/src/file.jl" +``` + +!!! compat "Julia 1.7" + The optional argument `paths` requires at least Julia 1.7. +""" +function pkgdir(m::Module, paths::String...) + rootmodule = moduleroot(m) path = pathof(rootmodule) path === nothing && return nothing - return dirname(dirname(path)) + return joinpath(dirname(dirname(path)), paths...) end ## generic project & manifest API ## const project_names = ("JuliaProject.toml", "Project.toml") const manifest_names = ("JuliaManifest.toml", "Manifest.toml") +const preferences_names = ("JuliaLocalPreferences.toml", "LocalPreferences.toml") + +function locate_project_file(env::String) + for proj in project_names + project_file = joinpath(env, proj) + if isfile_casesensitive(project_file) + return project_file + end + end + return true +end # classify the LOAD_PATH entry to be one of: # - `false`: nonexistant / nothing to see here # - `true`: `env` is an implicit environment # - `path`: the path of an explicit project file function env_project_file(env::String)::Union{Bool,String} + @lock require_lock begin + cache = LOADING_CACHE[] + if cache !== nothing + project_file = get(cache.env_project_file, env, nothing) + project_file === nothing || return project_file + end if isdir(env) - for proj in project_names - project_file = joinpath(env, proj) - isfile_casesensitive(project_file) && return project_file - end - return true + project_file = locate_project_file(env) elseif basename(env) in project_names && isfile_casesensitive(env) - return env + project_file = env + else + project_file = false + end + if cache !== nothing + cache.env_project_file[env] = project_file + end + return project_file end - return false end -function project_deps_get(env::String, name::String, cache::TOMLCache)::Union{Nothing,PkgId} +function project_deps_get(env::String, name::String)::Union{Nothing,PkgId} project_file = env_project_file(env) if project_file isa String - pkg_uuid = explicit_project_deps_get(project_file, name, cache) + pkg_uuid = explicit_project_deps_get(project_file, name) pkg_uuid === nothing || return PkgId(pkg_uuid, name) elseif project_file - return implicit_project_deps_get(env, name, cache) + return implicit_project_deps_get(env, name) end return nothing end -function manifest_deps_get(env::String, where::PkgId, name::String, cache::TOMLCache)::Union{Nothing,PkgId} - @assert where.uuid !== nothing +function manifest_deps_get(env::String, where::PkgId, name::String)::Union{Nothing,PkgId} + uuid = where.uuid + @assert uuid !== nothing project_file = env_project_file(env) if project_file isa String # first check if `where` names the Project itself - proj = project_file_name_uuid(project_file, where.name, cache) + proj = project_file_name_uuid(project_file, where.name) if proj == where # if `where` matches the project, use [deps] section as manifest, and stop searching - pkg_uuid = explicit_project_deps_get(project_file, name, cache) + pkg_uuid = explicit_project_deps_get(project_file, name) return PkgId(pkg_uuid, name) end # look for manifest file and `where` stanza - return explicit_manifest_deps_get(project_file, where.uuid, name, cache) + return explicit_manifest_deps_get(project_file, uuid, name) elseif project_file # if env names a directory, search it - return implicit_manifest_deps_get(env, where, name, cache) + return implicit_manifest_deps_get(env, where, name) end return nothing end -function manifest_uuid_path(env::String, pkg::PkgId, cache::TOMLCache)::Union{Nothing,String} +function manifest_uuid_path(env::String, pkg::PkgId)::Union{Nothing,String} project_file = env_project_file(env) if project_file isa String - proj = project_file_name_uuid(project_file, pkg.name, cache) + proj = project_file_name_uuid(project_file, pkg.name) if proj == pkg # if `pkg` matches the project, return the project itself - return project_file_path(project_file, pkg.name, cache) + return project_file_path(project_file, pkg.name) end # look for manifest file and `where` stanza - return explicit_manifest_uuid_path(project_file, pkg, cache) + return explicit_manifest_uuid_path(project_file, pkg) elseif project_file # if env names a directory, search it - return implicit_manifest_uuid_path(env, pkg, cache) + return implicit_manifest_uuid_path(env, pkg) end return nothing end # find project file's top-level UUID entry (or nothing) -function project_file_name_uuid(project_file::String, name::String, cache::TOMLCache)::PkgId - uuid = dummy_uuid(project_file) - d = parsed_toml(cache, project_file) +function project_file_name_uuid(project_file::String, name::String)::PkgId + d = parsed_toml(project_file) uuid′ = get(d, "uuid", nothing)::Union{String, Nothing} - uuid′ === nothing || (uuid = UUID(uuid′)) + uuid = uuid′ === nothing ? dummy_uuid(project_file) : UUID(uuid′) name = get(d, "name", name)::String return PkgId(uuid, name) end -function project_file_path(project_file::String, name::String, cache) - d = parsed_toml(cache, project_file) +function project_file_path(project_file::String, name::String) + d = parsed_toml(project_file) joinpath(dirname(project_file), get(d, "path", "")::String) end # find project file's corresponding manifest file -function project_file_manifest_path(project_file::String, cache::TOMLCache)::Union{Nothing,String} +function project_file_manifest_path(project_file::String)::Union{Nothing,String} + @lock require_lock begin + cache = LOADING_CACHE[] + if cache !== nothing + manifest_path = get(cache.project_file_manifest_path, project_file, missing) + manifest_path === missing || return manifest_path + end dir = abspath(dirname(project_file)) - d = parsed_toml(cache, project_file) + d = parsed_toml(project_file) explicit_manifest = get(d, "manifest", nothing)::Union{String, Nothing} + manifest_path = nothing if explicit_manifest !== nothing manifest_file = normpath(joinpath(dir, explicit_manifest)) - isfile_casesensitive(manifest_file) && return manifest_file + if isfile_casesensitive(manifest_file) + manifest_path = manifest_file + end + end + if manifest_path === nothing + for mfst in manifest_names + manifest_file = joinpath(dir, mfst) + if isfile_casesensitive(manifest_file) + manifest_path = manifest_file + break + end + end end - for mfst in manifest_names - manifest_file = joinpath(dir, mfst) - isfile_casesensitive(manifest_file) && return manifest_file + if cache !== nothing + cache.project_file_manifest_path[project_file] = manifest_path + end + return manifest_path end - return nothing end # given a directory (implicit env from LOAD_PATH) and a name, @@ -408,8 +582,8 @@ end # find project file root or deps `name => uuid` mapping # return `nothing` if `name` is not found -function explicit_project_deps_get(project_file::String, name::String, cache::TOMLCache)::Union{Nothing,UUID} - d = parsed_toml(cache, project_file) +function explicit_project_deps_get(project_file::String, name::String)::Union{Nothing,UUID} + d = parsed_toml(project_file) root_uuid = dummy_uuid(project_file) if get(d, "name", nothing)::Union{String, Nothing} === name uuid = get(d, "uuid", nothing)::Union{String, Nothing} @@ -423,18 +597,41 @@ function explicit_project_deps_get(project_file::String, name::String, cache::TO return nothing end +function is_v1_format_manifest(raw_manifest::Dict) + if haskey(raw_manifest, "manifest_format") + mf = raw_manifest["manifest_format"] + if mf isa Dict && haskey(mf, "uuid") + # the off-chance where an old format manifest has a dep called "manifest_format" + return true + end + return false + else + return true + end +end + +# returns a deps list for both old and new manifest formats +function get_deps(raw_manifest::Dict) + if is_v1_format_manifest(raw_manifest) + return raw_manifest + else + # if the manifest has no deps, there won't be a `deps` field + return get(Dict{String, Any}, raw_manifest, "deps")::Dict{String, Any} + end +end + # find `where` stanza and return the PkgId for `name` # return `nothing` if it did not find `where` (indicating caller should continue searching) -function explicit_manifest_deps_get(project_file::String, where::UUID, name::String, cache::TOMLCache)::Union{Nothing,PkgId} - manifest_file = project_file_manifest_path(project_file, cache) +function explicit_manifest_deps_get(project_file::String, where::UUID, name::String)::Union{Nothing,PkgId} + manifest_file = project_file_manifest_path(project_file) manifest_file === nothing && return nothing # manifest not found--keep searching LOAD_PATH - d = parsed_toml(cache, manifest_file) + d = get_deps(parsed_toml(manifest_file)) found_where = false found_name = false for (dep_name, entries) in d entries::Vector{Any} for entry in entries - entry::Dict{String, Any} + entry = entry::Dict{String, Any} uuid = get(entry, "uuid", nothing)::Union{String, Nothing} uuid === nothing && continue if UUID(uuid) === where @@ -447,7 +644,7 @@ function explicit_manifest_deps_get(project_file::String, where::UUID, name::Str found_name = name in deps break else - deps::Dict{String, Any} + deps = deps::Dict{String, Any} for (dep, uuid) in deps uuid::String if dep === name @@ -472,15 +669,15 @@ function explicit_manifest_deps_get(project_file::String, where::UUID, name::Str end # find `uuid` stanza, return the corresponding path -function explicit_manifest_uuid_path(project_file::String, pkg::PkgId, cache::TOMLCache)::Union{Nothing,String} - manifest_file = project_file_manifest_path(project_file, cache) +function explicit_manifest_uuid_path(project_file::String, pkg::PkgId)::Union{Nothing,String} + manifest_file = project_file_manifest_path(project_file) manifest_file === nothing && return nothing # no manifest, skip env - d = parsed_toml(cache, manifest_file) + d = get_deps(parsed_toml(manifest_file)) entries = get(d, pkg.name, nothing)::Union{Nothing, Vector{Any}} entries === nothing && return nothing # TODO: allow name to mismatch? for entry in entries - entry::Dict{String, Any} + entry = entry::Dict{String, Any} uuid = get(entry, "uuid", nothing)::Union{Nothing, String} uuid === nothing && continue if UUID(uuid) === pkg.uuid @@ -500,10 +697,11 @@ function explicit_manifest_entry_path(manifest_file::String, pkg::PkgId, entry:: hash === nothing && return nothing hash = SHA1(hash) # Keep the 4 since it used to be the default - for slug in (version_slug(pkg.uuid, hash, 4), version_slug(pkg.uuid, hash)) + uuid = pkg.uuid::UUID # checked within `explicit_manifest_uuid_path` + for slug in (version_slug(uuid, hash), version_slug(uuid, hash, 4)) for depot in DEPOT_PATH - path = abspath(depot, "packages", pkg.name, slug) - ispath(path) && return path + path = joinpath(depot, "packages", pkg.name, slug) + ispath(path) && return abspath(path) end end return nothing @@ -513,13 +711,13 @@ end # look for an entry point for `name` from a top-level package (no environment) # otherwise return `nothing` to indicate the caller should keep searching -function implicit_project_deps_get(dir::String, name::String, cache::TOMLCache)::Union{Nothing,PkgId} +function implicit_project_deps_get(dir::String, name::String)::Union{Nothing,PkgId} path, project_file = entry_point_and_project_file(dir, name) if project_file === nothing path === nothing && return nothing return PkgId(name) end - proj = project_file_name_uuid(project_file, name, cache) + proj = project_file_name_uuid(project_file, name) proj.name == name || return nothing return proj end @@ -527,25 +725,25 @@ end # look for an entry-point for `name`, check that UUID matches # if there's a project file, look up `name` in its deps and return that # otherwise return `nothing` to indicate the caller should keep searching -function implicit_manifest_deps_get(dir::String, where::PkgId, name::String, cache::TOMLCache)::Union{Nothing,PkgId} +function implicit_manifest_deps_get(dir::String, where::PkgId, name::String)::Union{Nothing,PkgId} @assert where.uuid !== nothing project_file = entry_point_and_project_file(dir, where.name)[2] project_file === nothing && return nothing # a project file is mandatory for a package with a uuid - proj = project_file_name_uuid(project_file, where.name, cache) + proj = project_file_name_uuid(project_file, where.name) proj == where || return nothing # verify that this is the correct project file # this is the correct project, so stop searching here - pkg_uuid = explicit_project_deps_get(project_file, name, cache) + pkg_uuid = explicit_project_deps_get(project_file, name) return PkgId(pkg_uuid, name) end # look for an entry-point for `pkg` and return its path if UUID matches -function implicit_manifest_uuid_path(dir::String, pkg::PkgId, cache::TOMLCache)::Union{Nothing,String} +function implicit_manifest_uuid_path(dir::String, pkg::PkgId)::Union{Nothing,String} path, project_file = entry_point_and_project_file(dir, pkg.name) if project_file === nothing pkg.uuid === nothing || return nothing return path end - proj = project_file_name_uuid(project_file, pkg.name, cache) + proj = project_file_name_uuid(project_file, pkg.name) proj == pkg || return nothing return path end @@ -554,7 +752,7 @@ end function find_source_file(path::AbstractString) (isabspath(path) || isfile(path)) && return path - base_path = joinpath(Sys.BINDIR::String, DATAROOTDIR, "julia", "base", path) + base_path = joinpath(Sys.BINDIR, DATAROOTDIR, "julia", "base", path) return isfile(base_path) ? normpath(base_path) : nothing end @@ -569,7 +767,7 @@ function find_all_in_cache_path(pkg::PkgId) entrypath, entryfile = cache_file_entry(pkg) for path in joinpath.(DEPOT_PATH, entrypath) isdir(path) || continue - for file in readdir(path) + for file in readdir(path, sort = false) # no sort given we sort later if !((pkg.uuid === nothing && file == entryfile * ".ji") || (pkg.uuid !== nothing && startswith(file, entryfile * "_"))) continue @@ -578,34 +776,69 @@ function find_all_in_cache_path(pkg::PkgId) isfile_casesensitive(filepath) && push!(paths, filepath) end end - return paths + if length(paths) > 1 + # allocating the sort vector is less expensive than using sort!(.. by=mtime), which would + # call the relatively slow mtime multiple times per path + p = sortperm(mtime.(paths), rev = true) + return paths[p] + else + return paths + end end # these return either the array of modules loaded from the path / content given # or an Exception that describes why it couldn't be loaded # and it reconnects the Base.Docs.META -function _include_from_serialized(path::String, depmods::Vector{Any}) +function _include_from_serialized(pkg::PkgId, path::String, depmods::Vector{Any}) sv = ccall(:jl_restore_incremental, Any, (Cstring, Any), path, depmods) if isa(sv, Exception) return sv end - restored = sv[1] - if !isa(restored, Exception) - for M in restored::Vector{Any} - M = M::Module - if isdefined(M, Base.Docs.META) - push!(Base.Docs.modules, M) - end - if parentmodule(M) === M - register_root_module(M) - end + sv = sv::SimpleVector + restored = sv[1]::Vector{Any} + for M in restored + M = M::Module + if isdefined(M, Base.Docs.META) && getfield(M, Base.Docs.META) !== nothing + push!(Base.Docs.modules, M) + end + if parentmodule(M) === M + register_root_module(M) + end + end + + # Register this cache path now - If Requires.jl is loaded, Revise may end + # up looking at the cache path during the init callback. + get!(PkgOrigin, pkgorigins, pkg).cachepath = path + + inits = sv[2]::Vector{Any} + if !isempty(inits) + unlock(require_lock) # temporarily _unlock_ during these callbacks + try + ccall(:jl_init_restored_modules, Cvoid, (Any,), inits) + finally + lock(require_lock) end end - isassigned(sv, 2) && ccall(:jl_init_restored_modules, Cvoid, (Any,), sv[2]) return restored end -function _tryrequire_from_serialized(modkey::PkgId, build_id::UInt64, modpath::Union{Nothing, String}, cache::TOMLCache) +function run_package_callbacks(modkey::PkgId) + unlock(require_lock) + try + for callback in package_callbacks + invokelatest(callback, modkey) + end + catch + # Try to continue loading if a callback errors + errs = current_exceptions() + @error "Error during package callback" exception=errs + finally + lock(require_lock) + end + nothing +end + +function _tryrequire_from_serialized(modkey::PkgId, build_id::UInt64, modpath::Union{Nothing, String}, depth::Int = 0) if root_module_exists(modkey) M = root_module(modkey) if PkgId(M) == modkey && module_build_id(M) === build_id @@ -613,14 +846,13 @@ function _tryrequire_from_serialized(modkey::PkgId, build_id::UInt64, modpath::U end else if modpath === nothing - modpath = locate_package(modkey, cache) + modpath = locate_package(modkey) modpath === nothing && return nothing end - mod = _require_search_from_serialized(modkey, String(modpath), cache) + mod = _require_search_from_serialized(modkey, String(modpath), depth) + get!(PkgOrigin, pkgorigins, modkey).path = modpath if !isa(mod, Bool) - for callback in package_callbacks - invokelatest(callback, modkey) - end + run_package_callbacks(modkey) for M in mod::Vector{Any} M = M::Module if PkgId(M) == modkey && module_build_id(M) === build_id @@ -632,7 +864,7 @@ function _tryrequire_from_serialized(modkey::PkgId, build_id::UInt64, modpath::U return nothing end -function _require_from_serialized(path::String, cache::TOMLCache) +function _require_from_serialized(pkg::PkgId, path::String) # loads a precompile cache file, ignoring stale_cachfile tests # load all of the dependent modules first local depmodnames @@ -648,24 +880,35 @@ function _require_from_serialized(path::String, cache::TOMLCache) depmods = Vector{Any}(undef, ndeps) for i in 1:ndeps modkey, build_id = depmodnames[i] - dep = _tryrequire_from_serialized(modkey, build_id, nothing, cache) + dep = _tryrequire_from_serialized(modkey, build_id, nothing) dep === nothing && return ErrorException("Required dependency $modkey failed to load from a cache file.") depmods[i] = dep::Module end # then load the file - return _include_from_serialized(path, depmods) + return _include_from_serialized(pkg, path, depmods) end +# use an Int counter so that nested @time_imports calls all remain open +const TIMING_IMPORTS = Threads.Atomic{Int}(0) + # returns `true` if require found a precompile cache for this sourcepath, but couldn't load it # returns `false` if the module isn't known to be precompilable # returns the set of modules restored if the cache load succeeded -function _require_search_from_serialized(pkg::PkgId, sourcepath::String, cache::TOMLCache) +@constprop :none function _require_search_from_serialized(pkg::PkgId, sourcepath::String, depth::Int = 0) + timing_imports = TIMING_IMPORTS[] > 0 + try + if timing_imports + t_before = time_ns() + cumulative_compile_timing(true) + t_comp_before = cumulative_compile_time_ns() + end paths = find_all_in_cache_path(pkg) for path_to_try in paths::Vector{String} - staledeps = stale_cachefile(sourcepath, path_to_try, cache) + staledeps = stale_cachefile(sourcepath, path_to_try) if staledeps === true continue end + staledeps = staledeps::Vector{Any} try touch(path_to_try) # update timestamp of precompilation file catch # file might be read-only and then we fail to update timestamp, which is fine @@ -675,7 +918,7 @@ function _require_search_from_serialized(pkg::PkgId, sourcepath::String, cache:: dep = staledeps[i] dep isa Module && continue modpath, modkey, build_id = dep::Tuple{String, PkgId, UInt64} - dep = _tryrequire_from_serialized(modkey, build_id, modpath, cache) + dep = _tryrequire_from_serialized(modkey, build_id, modpath, depth + 1) if dep === nothing @debug "Required dependency $modkey failed to load from cache file for $modpath." staledeps = true @@ -686,18 +929,37 @@ function _require_search_from_serialized(pkg::PkgId, sourcepath::String, cache:: if staledeps === true continue end - restored = _include_from_serialized(path_to_try, staledeps) + restored = _include_from_serialized(pkg, path_to_try, staledeps) if isa(restored, Exception) @debug "Deserialization checks failed while attempting to load cache from $path_to_try" exception=restored else + if timing_imports + elapsed = round((time_ns() - t_before) / 1e6, digits = 1) + comp_time, recomp_time = cumulative_compile_time_ns() .- t_comp_before + tree_prefix = depth == 0 ? "" : " "^(depth-1)*"┌ " + print(lpad(elapsed, 9), " ms ") + printstyled(tree_prefix, color = :light_black) + print(pkg.name) + if comp_time > 0 + printstyled(" ", Ryu.writefixed(Float64(100 * comp_time / (elapsed * 1e6)), 2), "% compilation time", color = Base.info_color()) + end + if recomp_time > 0 + perc = Float64(100 * recomp_time / comp_time) + printstyled(" (", perc < 1 ? "<1" : Ryu.writefixed(perc, 0), "% recompilation)", color = Base.warn_color()) + end + println() + end return restored end end return !isempty(paths) + finally + timing_imports && cumulative_compile_timing(false) + end end # to synchronize multiple tasks trying to import/using something -const package_locks = Dict{PkgId,Condition}() +const package_locks = Dict{PkgId,Threads.Condition}() # to notify downstream consumers that a module was successfully loaded # Callbacks take the form (mod::Base.PkgId) -> nothing. @@ -720,7 +982,9 @@ function _include_dependency(mod::Module, _path::AbstractString) path = normpath(joinpath(dirname(prev), _path)) end if _track_dependencies[] + @lock require_lock begin push!(_require_dependencies, (mod, path, mtime(path))) + end end return path, prev end @@ -767,9 +1031,6 @@ end # require always works in Main scope and loads files from node 1 const toplevel_load = Ref(true) -const full_warning_showed = Ref(false) -const modules_warned_for = Set{PkgId}() - """ require(into::Module, module::Symbol) @@ -792,63 +1053,70 @@ For more details regarding code loading, see the manual sections on [modules](@r [parallel computing](@ref code-availability). """ function require(into::Module, mod::Symbol) - cache = TOMLCache() - uuidkey = identify_package(into, String(mod), cache) - # Core.println("require($(PkgId(into)), $mod) -> $uuidkey") - if uuidkey === nothing - where = PkgId(into) - if where.uuid === nothing - throw(ArgumentError(""" - Package $mod not found in current path: - - Run `import Pkg; Pkg.add($(repr(String(mod))))` to install the $mod package. - """)) - else - s = """ - Package $(where.name) does not have $mod in its dependencies: - - If you have $(where.name) checked out for development and have - added $mod as a dependency but haven't updated your primary - environment's manifest file, try `Pkg.resolve()`. - - Otherwise you may need to report an issue with $(where.name)""" - - uuidkey = identify_package(PkgId(string(into)), String(mod)) - uuidkey === nothing && throw(ArgumentError(s)) - - # fall back to toplevel loading with a warning - if !(where in modules_warned_for) - @warn string( - full_warning_showed[] ? "" : s, "\n", - string("Loading $(mod) into $(where.name) from project dependency, ", - "future warnings for $(where.name) are suppressed.") - ) _module = nothing _file = nothing _group = nothing - push!(modules_warned_for, where) + @lock require_lock begin + LOADING_CACHE[] = LoadingCache() + try + uuidkey = identify_package(into, String(mod)) + # Core.println("require($(PkgId(into)), $mod) -> $uuidkey") + if uuidkey === nothing + where = PkgId(into) + if where.uuid === nothing + hint, dots = begin + if isdefined(into, mod) && getfield(into, mod) isa Module + true, "." + elseif isdefined(parentmodule(into), mod) && getfield(parentmodule(into), mod) isa Module + true, ".." + else + false, "" + end + end + hint_message = hint ? ", maybe you meant `import/using $(dots)$(mod)`" : "" + start_sentence = hint ? "Otherwise, run" : "Run" + throw(ArgumentError(""" + Package $mod not found in current path$hint_message. + - $start_sentence `import Pkg; Pkg.add($(repr(String(mod))))` to install the $mod package.""")) + else + throw(ArgumentError(""" + Package $(where.name) does not have $mod in its dependencies: + - You may have a partially installed environment. Try `Pkg.instantiate()` + to ensure all packages in the environment are installed. + - Or, if you have $(where.name) checked out for development and have + added $mod as a dependency but haven't updated your primary + environment's manifest file, try `Pkg.resolve()`. + - Otherwise you may need to report an issue with $(where.name)""")) end - full_warning_showed[] = true end + if _track_dependencies[] + push!(_require_dependencies, (into, binpack(uuidkey), 0.0)) + end + return _require_prelocked(uuidkey) + finally + LOADING_CACHE[] = nothing end - if _track_dependencies[] - push!(_require_dependencies, (into, binpack(uuidkey), 0.0)) end - return require(uuidkey, cache) end mutable struct PkgOrigin - # version::VersionNumber path::Union{String,Nothing} cachepath::Union{String,Nothing} + version::Union{VersionNumber,Nothing} end -PkgOrigin() = PkgOrigin(nothing, nothing) +PkgOrigin() = PkgOrigin(nothing, nothing, nothing) const pkgorigins = Dict{PkgId,PkgOrigin}() -function require(uuidkey::PkgId, cache::TOMLCache=TOMLCache()) +require(uuidkey::PkgId) = @lock require_lock _require_prelocked(uuidkey) + +function _require_prelocked(uuidkey::PkgId) + just_loaded_pkg = false if !root_module_exists(uuidkey) - cachefile = _require(uuidkey, cache) - if cachefile !== nothing - get!(PkgOrigin, pkgorigins, uuidkey).cachepath = cachefile - end + _require(uuidkey) # After successfully loading, notify downstream consumers - for callback in package_callbacks - invokelatest(callback, uuidkey) - end + run_package_callbacks(uuidkey) + just_loaded_pkg = true + end + if just_loaded_pkg && !root_module_exists(uuidkey) + error("package `$(uuidkey.name)` did not define the expected \ + module `$(uuidkey.name)`, check for typos in package module name") end return root_module(uuidkey) end @@ -856,10 +1124,13 @@ end const loaded_modules = Dict{PkgId,Module}() const module_keys = IdDict{Module,PkgId}() # the reverse -is_root_module(m::Module) = haskey(module_keys, m) -root_module_key(m::Module) = module_keys[m] +is_root_module(m::Module) = @lock require_lock haskey(module_keys, m) +root_module_key(m::Module) = @lock require_lock module_keys[m] -function register_root_module(m::Module) +@constprop :none function register_root_module(m::Module) + # n.b. This is called from C after creating a new module in `Base.__toplevel__`, + # instead of adding them to the binding table there. + @lock require_lock begin key = PkgId(m, String(nameof(m))) if haskey(loaded_modules, key) oldm = loaded_modules[key] @@ -869,6 +1140,7 @@ function register_root_module(m::Module) end loaded_modules[key] = m module_keys[m] = key + end nothing end @@ -884,12 +1156,13 @@ using Base end # get a top-level Module from the given key -root_module(key::PkgId) = loaded_modules[key] +root_module(key::PkgId) = @lock require_lock loaded_modules[key] root_module(where::Module, name::Symbol) = root_module(identify_package(where, String(name))) +maybe_root_module(key::PkgId) = @lock require_lock get(loaded_modules, key, nothing) -root_module_exists(key::PkgId) = haskey(loaded_modules, key) -loaded_modules_array() = collect(values(loaded_modules)) +root_module_exists(key::PkgId) = @lock require_lock haskey(loaded_modules, key) +loaded_modules_array() = @lock require_lock collect(values(loaded_modules)) function unreference_module(key::PkgId) if haskey(loaded_modules, key) @@ -899,8 +1172,23 @@ function unreference_module(key::PkgId) end end +function set_pkgorigin_version_path(pkg, path) + pkgorigin = get!(PkgOrigin, pkgorigins, pkg) + if path !== nothing + project_file = locate_project_file(joinpath(dirname(path), "..")) + if project_file isa String + d = parsed_toml(project_file) + v = get(d, "version", nothing) + if v !== nothing + pkgorigin.version = VersionNumber(v::AbstractString) + end + end + end + pkgorigin.path = path +end + # Returns `nothing` or the name of the newly-created cachefile -function _require(pkg::PkgId, cache::TOMLCache) +function _require(pkg::PkgId) # handle recursive calls to require loading = get(package_locks, pkg, false) if loading !== false @@ -908,14 +1196,14 @@ function _require(pkg::PkgId, cache::TOMLCache) wait(loading) return end - package_locks[pkg] = Condition() + package_locks[pkg] = Threads.Condition(require_lock) last = toplevel_load[] try toplevel_load[] = false # perform the search operation to select the module file require intends to load - path = locate_package(pkg, cache) - get!(PkgOrigin, pkgorigins, pkg).path = path + path = locate_package(pkg) + set_pkgorigin_version_path(pkg, path) if path === nothing throw(ArgumentError(""" Package $pkg is required but does not seem to be installed: @@ -925,7 +1213,7 @@ function _require(pkg::PkgId, cache::TOMLCache) # attempt to load the module file via the precompile cache locations if JLOptions().use_compiled_modules != 0 - m = _require_search_from_serialized(pkg, path, cache) + m = _require_search_from_serialized(pkg, path) if !isa(m, Bool) return end @@ -958,11 +1246,11 @@ function _require(pkg::PkgId, cache::TOMLCache) end # fall-through to loading the file locally else - m = _require_from_serialized(cachefile, cache) + m = _require_from_serialized(pkg, cachefile) if isa(m, Exception) @warn "The call to compilecache failed to create a usable precompiled cache file for $pkg" exception=m else - return cachefile + return end end end @@ -976,10 +1264,12 @@ function _require(pkg::PkgId, cache::TOMLCache) if uuid !== old_uuid ccall(:jl_set_module_uuid, Cvoid, (Any, NTuple{2, UInt64}), __toplevel__, uuid) end + unlock(require_lock) try include(__toplevel__, path) return finally + lock(require_lock) if uuid !== old_uuid ccall(:jl_set_module_uuid, Cvoid, (Any, NTuple{2, UInt64}), __toplevel__, old_uuid) end @@ -1002,6 +1292,9 @@ Like [`include`](@ref), except reads code from the given string rather than from The optional first argument `mapexpr` can be used to transform the included code before it is evaluated: for each parsed expression `expr` in `code`, the `include_string` function actually evaluates `mapexpr(expr)`. If it is omitted, `mapexpr` defaults to [`identity`](@ref). + +!!! compat "Julia 1.5" + Julia 1.5 is required for passing the `mapexpr` argument. """ function include_string(mapexpr::Function, mod::Module, code::AbstractString, filename::AbstractString="string") @@ -1036,8 +1329,11 @@ include_string(m::Module, txt::AbstractString, fname::AbstractString="string") = function source_path(default::Union{AbstractString,Nothing}="") s = current_task().storage - if s !== nothing && haskey(s::IdDict{Any,Any}, :SOURCE_PATH) - return s[:SOURCE_PATH]::Union{Nothing,String} + if s !== nothing + s = s::IdDict{Any,Any} + if haskey(s, :SOURCE_PATH) + return s[:SOURCE_PATH]::Union{Nothing,String} + end end return default end @@ -1061,12 +1357,15 @@ interactively, or to combine files in packages that are broken into multiple sou The optional first argument `mapexpr` can be used to transform the included code before it is evaluated: for each parsed expression `expr` in `path`, the `include` function actually evaluates `mapexpr(expr)`. If it is omitted, `mapexpr` defaults to [`identity`](@ref). + +!!! compat "Julia 1.5" + Julia 1.5 is required for passing the `mapexpr` argument. """ Base.include # defined in Base.jl # Full include() implementation which is used after bootstrap function _include(mapexpr::Function, mod::Module, _path::AbstractString) - @_noinline_meta # Workaround for module availability in _simplify_include_frames + @noinline # Workaround for module availability in _simplify_include_frames path, prev = _include_dependency(mod, _path) for callback in include_callbacks # to preserve order, must come before eval in include_string invokelatest(callback, mod, path) @@ -1115,40 +1414,46 @@ function load_path_setup_code(load_path::Bool=true) code *= """ append!(empty!(Base.LOAD_PATH), $(repr(load_path))) ENV["JULIA_LOAD_PATH"] = $(repr(join(load_path, Sys.iswindows() ? ';' : ':'))) - Base.ACTIVE_PROJECT[] = nothing + Base.set_active_project(nothing) """ end return code end # this is called in the external process that generates precompiled package files -function include_package_for_output(input::String, depot_path::Vector{String}, dl_load_path::Vector{String}, load_path::Vector{String}, concrete_deps::typeof(_concrete_dependencies), uuid_tuple::NTuple{2,UInt64}, source::Union{Nothing,String}) +function include_package_for_output(pkg::PkgId, input::String, depot_path::Vector{String}, dl_load_path::Vector{String}, load_path::Vector{String}, + concrete_deps::typeof(_concrete_dependencies), source::Union{Nothing,String}) append!(empty!(Base.DEPOT_PATH), depot_path) append!(empty!(Base.DL_LOAD_PATH), dl_load_path) append!(empty!(Base.LOAD_PATH), load_path) ENV["JULIA_LOAD_PATH"] = join(load_path, Sys.iswindows() ? ';' : ':') - Base.ACTIVE_PROJECT[] = nothing + set_active_project(nothing) Base._track_dependencies[] = true + get!(Base.PkgOrigin, Base.pkgorigins, pkg).path = input append!(empty!(Base._concrete_dependencies), concrete_deps) + uuid_tuple = pkg.uuid === nothing ? (UInt64(0), UInt64(0)) : convert(NTuple{2, UInt64}, pkg.uuid) ccall(:jl_set_module_uuid, Cvoid, (Any, NTuple{2, UInt64}), Base.__toplevel__, uuid_tuple) if source !== nothing task_local_storage()[:SOURCE_PATH] = source end + Core.Compiler.track_newly_inferred.x = true try Base.include(Base.__toplevel__, input) catch ex precompilableerror(ex) || rethrow() @debug "Aborting `create_expr_cache'" exception=(ErrorException("Declaration of __precompile__(false) not allowed"), catch_backtrace()) exit(125) # we define status = 125 means PrecompileableError + finally + Core.Compiler.track_newly_inferred.x = false end + ccall(:jl_set_newly_inferred, Cvoid, (Any,), Core.Compiler.newly_inferred) end -@assert precompile(include_package_for_output, (String,Vector{String},Vector{String},Vector{String},typeof(_concrete_dependencies),NTuple{2,UInt64},Nothing)) -@assert precompile(include_package_for_output, (String,Vector{String},Vector{String},Vector{String},typeof(_concrete_dependencies),NTuple{2,UInt64},String)) - -function create_expr_cache(input::String, output::String, concrete_deps::typeof(_concrete_dependencies), uuid::Union{Nothing,UUID}) +const PRECOMPILE_TRACE_COMPILE = Ref{String}() +function create_expr_cache(pkg::PkgId, input::String, output::String, concrete_deps::typeof(_concrete_dependencies), internal_stderr::IO = stderr, internal_stdout::IO = stdout) + @nospecialize internal_stderr internal_stdout rm(output, force=true) # Remove file if it exists depot_path = map(abspath, DEPOT_PATH) dl_load_path = map(abspath, DL_LOAD_PATH) @@ -1158,37 +1463,41 @@ function create_expr_cache(input::String, output::String, concrete_deps::typeof( error("LOAD_PATH entries cannot contain $(repr(path_sep))") deps_strs = String[] - for (pkg, build_id) in concrete_deps - pkg_str = if pkg.uuid === nothing - "Base.PkgId($(repr(pkg.name)))" + function pkg_str(_pkg::PkgId) + if _pkg.uuid === nothing + "Base.PkgId($(repr(_pkg.name)))" else - "Base.PkgId(Base.UUID(\"$(pkg.uuid)\"), $(repr(pkg.name)))" + "Base.PkgId(Base.UUID(\"$(_pkg.uuid)\"), $(repr(_pkg.name)))" end - push!(deps_strs, "$pkg_str => $(repr(build_id))") end - deps = repr(eltype(concrete_deps)) * "[" * join(deps_strs, ",") * "]" - - uuid_tuple = uuid === nothing ? (UInt64(0), UInt64(0)) : convert(NTuple{2, UInt64}, uuid) - + for (pkg, build_id) in concrete_deps + push!(deps_strs, "$(pkg_str(pkg)) => $(repr(build_id))") + end + deps_eltype = sprint(show, eltype(concrete_deps); context = :module=>nothing) + deps = deps_eltype * "[" * join(deps_strs, ",") * "]" + trace = isassigned(PRECOMPILE_TRACE_COMPILE) ? `--trace-compile=$(PRECOMPILE_TRACE_COMPILE[])` : `` io = open(pipeline(`$(julia_cmd()::Cmd) -O0 --output-ji $output --output-incremental=yes --startup-file=no --history-file=no --warn-overwrite=yes --color=$(have_color === nothing ? "auto" : have_color ? "yes" : "no") - --eval 'eval(Meta.parse(read(stdin,String)))'`, stderr=stderr), + $trace + -`, stderr = internal_stderr, stdout = internal_stdout), "w", stdout) # write data over stdin to avoid the (unlikely) case of exceeding max command line size write(io.in, """ - Base.include_package_for_output($(repr(abspath(input))), $(repr(depot_path)), $(repr(dl_load_path)), - $(repr(load_path)), $deps, $(repr(uuid_tuple)), $(repr(source_path(nothing)))) + Base.include_package_for_output($(pkg_str(pkg)), $(repr(abspath(input))), $(repr(depot_path)), $(repr(dl_load_path)), + $(repr(load_path)), $deps, $(repr(source_path(nothing)))) """) close(io.in) return io end -@assert precompile(create_expr_cache, (String, String, typeof(_concrete_dependencies), Nothing)) -@assert precompile(create_expr_cache, (String, String, typeof(_concrete_dependencies), UUID)) +function compilecache_dir(pkg::PkgId) + entrypath, entryfile = cache_file_entry(pkg) + return joinpath(DEPOT_PATH[1], entrypath) +end -function compilecache_path(pkg::PkgId)::String +function compilecache_path(pkg::PkgId, prefs_hash::UInt64)::String entrypath, entryfile = cache_file_entry(pkg) cachepath = joinpath(DEPOT_PATH[1], entrypath) isdir(cachepath) || mkpath(cachepath) @@ -1198,6 +1507,7 @@ function compilecache_path(pkg::PkgId)::String crc = _crc32c(something(Base.active_project(), "")) crc = _crc32c(unsafe_string(JLOptions().image_file), crc) crc = _crc32c(unsafe_string(JLOptions().julia_bin), crc) + crc = _crc32c(prefs_hash, crc) project_precompile_slug = slug(crc, 5) abspath(cachepath, string(entryfile, "_", project_precompile_slug, ".ji")) end @@ -1211,32 +1521,29 @@ This can be used to reduce package load times. Cache files are stored in `DEPOT_PATH[1]/compiled`. See [Module initialization and precompilation](@ref) for important notes. """ -function compilecache(pkg::PkgId, cache::TOMLCache = TOMLCache()) - path = locate_package(pkg, cache) +function compilecache(pkg::PkgId, internal_stderr::IO = stderr, internal_stdout::IO = stdout) + @nospecialize internal_stderr internal_stdout + path = locate_package(pkg) path === nothing && throw(ArgumentError("$pkg not found during precompilation")) - return compilecache(pkg, path) + return compilecache(pkg, path, internal_stderr, internal_stdout) end -const MAX_NUM_PRECOMPILE_FILES = 10 +const MAX_NUM_PRECOMPILE_FILES = Ref(10) + +function compilecache(pkg::PkgId, path::String, internal_stderr::IO = stderr, internal_stdout::IO = stdout, + ignore_loaded_modules::Bool = true) -function compilecache(pkg::PkgId, path::String) + @nospecialize internal_stderr internal_stdout # decide where to put the resulting cache file - cachefile = compilecache_path(pkg) - cachepath = dirname(cachefile) - # prune the directory with cache files - if pkg.uuid !== nothing - entrypath, entryfile = cache_file_entry(pkg) - cachefiles = filter!(x -> startswith(x, entryfile * "_"), readdir(cachepath)) - if length(cachefiles) >= MAX_NUM_PRECOMPILE_FILES - idx = findmin(mtime.(joinpath.(cachepath, cachefiles)))[2] - rm(joinpath(cachepath, cachefiles[idx])) - end - end + cachepath = compilecache_dir(pkg) + # build up the list of modules that we want the precompile process to preserve concrete_deps = copy(_concrete_dependencies) - for (key, mod) in loaded_modules - if !(mod === Main || mod === Core || mod === Base) - push!(concrete_deps, key => module_build_id(mod)) + if ignore_loaded_modules + for (key, mod) in loaded_modules + if !(mod === Main || mod === Core || mod === Base) + push!(concrete_deps, key => module_build_id(mod)) + end end end # run the expression and cache the result @@ -1245,18 +1552,34 @@ function compilecache(pkg::PkgId, path::String) # create a temporary file in `cachepath` directory, write the cache in it, # write the checksum, _and then_ atomically move the file to `cachefile`. + mkpath(cachepath) tmppath, tmpio = mktemp(cachepath) local p try close(tmpio) - p = create_expr_cache(path, tmppath, concrete_deps, pkg.uuid) + p = create_expr_cache(pkg, path, tmppath, concrete_deps, internal_stderr, internal_stdout) if success(p) # append checksum to the end of the .ji file: open(tmppath, "a+") do f write(f, _crc32c(seekstart(f))) end - # inherit permission from the source file - chmod(tmppath, filemode(path) & 0o777) + # inherit permission from the source file (and make them writable) + chmod(tmppath, filemode(path) & 0o777 | 0o200) + + # Read preferences hash back from .ji file (we can't precompute because + # we don't actually know what the list of compile-time preferences are without compiling) + prefs_hash = preferences_hash(tmppath) + cachefile = compilecache_path(pkg, prefs_hash) + + # prune the directory with cache files + if pkg.uuid !== nothing + entrypath, entryfile = cache_file_entry(pkg) + cachefiles = filter!(x -> startswith(x, entryfile * "_"), readdir(cachepath)) + if length(cachefiles) >= MAX_NUM_PRECOMPILE_FILES[] + idx = findmin(mtime.(joinpath.(cachepath, cachefiles)))[2] + rm(joinpath(cachepath, cachefiles[idx]); force=true) + end + end # this is atomic according to POSIX: rename(tmppath, cachefile; force=true) @@ -1268,7 +1591,7 @@ function compilecache(pkg::PkgId, path::String) if p.exitcode == 125 return PrecompilableError() else - error("Failed to precompile $pkg to $cachefile.") + error("Failed to precompile $pkg to $tmppath.") end end @@ -1294,17 +1617,23 @@ function parse_cache_header(f::IO) build_id = read(f, UInt64) # build UUID (mostly just a timestamp) push!(modules, PkgId(uuid, sym) => build_id) end - totbytes = read(f, Int64) # total bytes for file dependencies + totbytes = read(f, Int64) # total bytes for file dependencies + preferences # read the list of requirements # and split the list into include and requires statements includes = CacheHeaderIncludes[] requires = Pair{PkgId, PkgId}[] while true n2 = read(f, Int32) - n2 == 0 && break + totbytes -= 4 + if n2 == 0 + break + end depname = String(read(f, n2)) + totbytes -= n2 mtime = read(f, Float64) + totbytes -= 8 n1 = read(f, Int32) + totbytes -= 4 # map ids to keys modkey = (n1 == 0) ? PkgId("") : modules[n1].first modpath = String[] @@ -1313,7 +1642,9 @@ function parse_cache_header(f::IO) while true n1 = read(f, Int32) totbytes -= 4 - n1 == 0 && break + if n1 == 0 + break + end push!(modpath, String(read(f, n1))) totbytes -= n1 end @@ -1323,10 +1654,22 @@ function parse_cache_header(f::IO) else push!(includes, CacheHeaderIncludes(modkey, depname, mtime, modpath)) end - totbytes -= 4 + 4 + n2 + 8 end - @assert totbytes == 12 "header of cache file appears to be corrupt" + prefs = String[] + while true + n2 = read(f, Int32) + totbytes -= 4 + if n2 == 0 + break + end + push!(prefs, String(read(f, n2))) + totbytes -= n2 + end + prefs_hash = read(f, UInt64) + totbytes -= 8 srctextpos = read(f, Int64) + totbytes -= 8 + @assert totbytes == 0 "header of cache file appears to be corrupt (totbytes == $(totbytes))" # read the list of modules that are required to be present during loading required_modules = Vector{Pair{PkgId, UInt64}}() while true @@ -1337,7 +1680,7 @@ function parse_cache_header(f::IO) build_id = read(f, UInt64) # build id push!(required_modules, PkgId(uuid, sym) => build_id) end - return modules, (includes, requires), required_modules, srctextpos + return modules, (includes, requires), required_modules, srctextpos, prefs, prefs_hash end function parse_cache_header(cachefile::String; srcfiles_only::Bool=false) @@ -1346,21 +1689,37 @@ function parse_cache_header(cachefile::String; srcfiles_only::Bool=false) !isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile.")) ret = parse_cache_header(io) srcfiles_only || return ret - modules, (includes, requires), required_modules, srctextpos = ret + modules, (includes, requires), required_modules, srctextpos, prefs, prefs_hash = ret srcfiles = srctext_files(io, srctextpos) delidx = Int[] for (i, chi) in enumerate(includes) chi.filename ∈ srcfiles || push!(delidx, i) end deleteat!(includes, delidx) - return modules, (includes, requires), required_modules, srctextpos + return modules, (includes, requires), required_modules, srctextpos, prefs, prefs_hash finally close(io) end end + + +preferences_hash(f::IO) = parse_cache_header(f)[end] +function preferences_hash(cachefile::String) + io = open(cachefile, "r") + try + if !isvalid_cache_header(io) + throw(ArgumentError("Invalid header in cache file $cachefile.")) + end + return preferences_hash(io) + finally + close(io) + end +end + + function cache_dependencies(f::IO) - defs, (includes, requires), modules = parse_cache_header(f) + defs, (includes, requires), modules, srctextpos, prefs, prefs_hash = parse_cache_header(f) return modules, map(chi -> (chi.filename, chi.mtime), includes) # return just filename and mtime end @@ -1375,7 +1734,7 @@ function cache_dependencies(cachefile::String) end function read_dependency_src(io::IO, filename::AbstractString) - modules, (includes, requires), required_modules, srctextpos = parse_cache_header(io) + modules, (includes, requires), required_modules, srctextpos, prefs, prefs_hash = parse_cache_header(io) srctextpos == 0 && error("no source-text stored in cache file") seek(io, srctextpos) return _read_dependency_src(io, filename) @@ -1420,17 +1779,181 @@ function srctext_files(f::IO, srctextpos::Int64) return files end +# Test to see if this UUID is mentioned in this `Project.toml`; either as +# the top-level UUID (e.g. that of the project itself), as a dependency, +# or as an extra for Preferences. +function get_uuid_name(project::Dict{String, Any}, uuid::UUID) + uuid_p = get(project, "uuid", nothing)::Union{Nothing, String} + name = get(project, "name", nothing)::Union{Nothing, String} + if name !== nothing && uuid_p !== nothing && UUID(uuid_p) == uuid + return name + end + deps = get(project, "deps", nothing)::Union{Nothing, Dict{String, Any}} + if deps !== nothing + for (k, v) in deps + if uuid == UUID(v::String) + return k + end + end + end + for subkey in ("deps", "extras") + subsection = get(project, subkey, nothing)::Union{Nothing, Dict{String, Any}} + if subsection !== nothing + for (k, v) in subsection + if uuid == UUID(v::String) + return k + end + end + end + end + return nothing +end + +function get_uuid_name(project_toml::String, uuid::UUID) + project = parsed_toml(project_toml) + return get_uuid_name(project, uuid) +end + +# If we've asked for a specific UUID, this function will extract the prefs +# for that particular UUID. Otherwise, it returns all preferences. +function filter_preferences(prefs::Dict{String, Any}, pkg_name) + if pkg_name === nothing + return prefs + else + return get(Dict{String, Any}, prefs, pkg_name)::Dict{String, Any} + end +end + +function collect_preferences(project_toml::String, uuid::Union{UUID,Nothing}) + # We'll return a list of dicts to be merged + dicts = Dict{String, Any}[] + + project = parsed_toml(project_toml) + pkg_name = nothing + if uuid !== nothing + # If we've been given a UUID, map that to the name of the package as + # recorded in the preferences section. If we can't find that mapping, + # exit out, as it means there's no way preferences can be set for that + # UUID, as we only allow actual dependencies to have preferences set. + pkg_name = get_uuid_name(project, uuid) + if pkg_name === nothing + return dicts + end + end + + # Look first inside of `Project.toml` to see we have preferences embedded within there + proj_preferences = get(Dict{String, Any}, project, "preferences")::Dict{String, Any} + push!(dicts, filter_preferences(proj_preferences, pkg_name)) + + # Next, look for `(Julia)LocalPreferences.toml` files next to this `Project.toml` + project_dir = dirname(project_toml) + for name in preferences_names + toml_path = joinpath(project_dir, name) + if isfile(toml_path) + prefs = parsed_toml(toml_path) + push!(dicts, filter_preferences(prefs, pkg_name)) + + # If we find `JuliaLocalPreferences.toml`, don't look for `LocalPreferences.toml` + break + end + end + + return dicts +end + +""" + recursive_prefs_merge(base::Dict, overrides::Dict...) + +Helper function to merge preference dicts recursively, honoring overrides in nested +dictionaries properly. +""" +function recursive_prefs_merge(base::Dict{String, Any}, overrides::Dict{String, Any}...) + new_base = Base._typeddict(base, overrides...) + + for override in overrides + # Clear entries are keys that should be deleted from any previous setting. + override_clear = get(override, "__clear__", nothing) + if override_clear isa Vector{String} + for k in override_clear + delete!(new_base, k) + end + end + + for (k, override_k) in override + # Note that if `base` has a mapping that is _not_ a `Dict`, and `override` + new_base_k = get(new_base, k, nothing) + if new_base_k isa Dict{String, Any} && override_k isa Dict{String, Any} + new_base[k] = recursive_prefs_merge(new_base_k, override_k) + else + new_base[k] = override_k + end + end + end + return new_base +end + +function get_preferences(uuid::Union{UUID,Nothing} = nothing) + merged_prefs = Dict{String,Any}() + for env in reverse(load_path()) + project_toml = env_project_file(env) + if !isa(project_toml, String) + continue + end + + # Collect all dictionaries from the current point in the load path, then merge them in + dicts = collect_preferences(project_toml, uuid) + merged_prefs = recursive_prefs_merge(merged_prefs, dicts...) + end + return merged_prefs +end + +function get_preferences_hash(uuid::Union{UUID, Nothing}, prefs_list::Vector{String}) + # Start from a predictable hash point to ensure that the same preferences always + # hash to the same value, modulo changes in how Dictionaries are hashed. + h = UInt(0) + uuid === nothing && return UInt64(h) + + # Load the preferences + prefs = get_preferences(uuid) + + # Walk through each name that's called out as a compile-time preference + for name in prefs_list + prefs_value = get(prefs, name, nothing) + if prefs_value !== nothing + h = hash(prefs_value, h)::UInt + end + end + # We always return a `UInt64` so that our serialization format is stable + return UInt64(h) +end + +get_preferences_hash(m::Module, prefs_list::Vector{String}) = get_preferences_hash(PkgId(m).uuid, prefs_list) + +# This is how we keep track of who is using what preferences at compile-time +const COMPILETIME_PREFERENCES = Dict{UUID,Set{String}}() + +# In `Preferences.jl`, if someone calls `load_preference(@__MODULE__, key)` while we're precompiling, +# we mark that usage as a usage at compile-time and call this method, so that at the end of `.ji` generation, +# we can record the list of compile-time preferences and embed that into the `.ji` header +function record_compiletime_preference(uuid::UUID, key::String) + pref = get!(Set{String}, COMPILETIME_PREFERENCES, uuid) + push!(pref, key) + return nothing +end +get_compiletime_preferences(uuid::UUID) = collect(get(Vector{String}, COMPILETIME_PREFERENCES, uuid)) +get_compiletime_preferences(m::Module) = get_compiletime_preferences(PkgId(m).uuid) +get_compiletime_preferences(::Nothing) = String[] + # returns true if it "cachefile.ji" is stale relative to "modpath.jl" # otherwise returns the list of dependencies to also check -stale_cachefile(modpath::String, cachefile::String) = stale_cachefile(modpath, cachefile, TOMLCache()) -function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache) +@constprop :none function stale_cachefile(modpath::String, cachefile::String; ignore_loaded::Bool = false) io = open(cachefile, "r") try if !isvalid_cache_header(io) @debug "Rejecting cache file $cachefile due to it containing an invalid cache header" return true # invalid cache file end - (modules, (includes, requires), required_modules) = parse_cache_header(io) + modules, (includes, requires), required_modules, srctextpos, prefs, prefs_hash = parse_cache_header(io) id = isempty(modules) ? nothing : first(modules).first modules = Dict{PkgId, UInt64}(modules) @@ -1444,17 +1967,21 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache) M = root_module(req_key) if PkgId(M) == req_key && module_build_id(M) === req_build_id depmods[i] = M + elseif ignore_loaded + # Used by Pkg.precompile given that there it's ok to precompile different versions of loaded packages + @goto locate_branch else @debug "Rejecting cache file $cachefile because module $req_key is already loaded and incompatible." return true # Won't be able to fulfill dependency end else - path = locate_package(req_key, cache) - get!(PkgOrigin, pkgorigins, req_key).path = path + @label locate_branch + path = locate_package(req_key) if path === nothing @debug "Rejecting cache file $cachefile because dependency $req_key not found." return true # Won't be able to fulfill dependency end + set_pkgorigin_version_path(req_key, path) depmods[i] = (path, req_key, req_build_id) end end @@ -1478,12 +2005,12 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache) # now check if this file is fresh relative to its source files if !skip_timecheck if !samefile(includes[1].filename, modpath) - @debug "Rejecting cache file $cachefile because it is for file $(includes[1].filename)) not file $modpath" + @debug "Rejecting cache file $cachefile because it is for file $(includes[1].filename) not file $modpath" return true # cache file was compiled from a different path end for (modkey, req_modkey) in requires # verify that `require(modkey, name(req_modkey))` ==> `req_modkey` - if identify_package(modkey, req_modkey.name, cache) != req_modkey + if identify_package(modkey, req_modkey.name) != req_modkey @debug "Rejecting cache file $cachefile because uuid mapping for $modkey => $req_modkey has changed" return true end @@ -1492,8 +2019,9 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache) f, ftime_req = chi.filename, chi.mtime # Issue #13606: compensate for Docker images rounding mtimes # Issue #20837: compensate for GlusterFS truncating mtimes to microseconds + # The `ftime != 1.0` condition below provides compatibility with Nix mtime. ftime = mtime(f) - if ftime != ftime_req && ftime != floor(ftime_req) && ftime != trunc(ftime_req, digits=6) + if ftime != ftime_req && ftime != floor(ftime_req) && ftime != trunc(ftime_req, digits=6) && ftime != 1.0 @debug "Rejecting stale cache file $cachefile (mtime $ftime_req) because file $f (mtime $ftime) has changed" return true end @@ -1506,7 +2034,11 @@ function stale_cachefile(modpath::String, cachefile::String, cache::TOMLCache) end if isa(id, PkgId) - get!(PkgOrigin, pkgorigins, id).cachepath = cachefile + curr_prefs_hash = get_preferences_hash(id.uuid, prefs) + if prefs_hash != curr_prefs_hash + @debug "Rejecting cache file $cachefile because preferences hash does not match 0x$(string(prefs_hash, base=16)) != 0x$(string(curr_prefs_hash, base=16))" + return true + end end return depmods # fresh cachefile @@ -1540,3 +2072,25 @@ macro __DIR__() _dirname = dirname(String(__source__.file::Symbol)) return isempty(_dirname) ? pwd() : abspath(_dirname) end + +""" + precompile(f, args::Tuple{Vararg{Any}}) + +Compile the given function `f` for the argument tuple (of types) `args`, but do not execute it. +""" +function precompile(@nospecialize(f), @nospecialize(args::Tuple)) + precompile(Tuple{Core.Typeof(f), args...}) +end + +const ENABLE_PRECOMPILE_WARNINGS = Ref(false) +function precompile(@nospecialize(argt::Type)) + ret = ccall(:jl_compile_hint, Int32, (Any,), argt) != 0 + if !ret && ENABLE_PRECOMPILE_WARNINGS[] + @warn "Inactive precompile statement" maxlog=100 form=argt _module=nothing _file=nothing _line=0 + end + return ret +end + +precompile(include_package_for_output, (PkgId, String, Vector{String}, Vector{String}, Vector{String}, typeof(_concrete_dependencies), Nothing)) +precompile(include_package_for_output, (PkgId, String, Vector{String}, Vector{String}, Vector{String}, typeof(_concrete_dependencies), String)) +precompile(create_expr_cache, (PkgId, String, String, typeof(_concrete_dependencies), IO, IO)) diff --git a/base/lock.jl b/base/lock.jl index 7033fdd80cff9f..8a15d3f95b2391 100644 --- a/base/lock.jl +++ b/base/lock.jl @@ -6,16 +6,47 @@ const ThreadSynchronizer = GenericCondition{Threads.SpinLock} """ ReentrantLock() -Creates a re-entrant lock for synchronizing [`Task`](@ref)s. -The same task can acquire the lock as many times as required. -Each [`lock`](@ref) must be matched with an [`unlock`](@ref). +Creates a re-entrant lock for synchronizing [`Task`](@ref)s. The same task can +acquire the lock as many times as required. Each [`lock`](@ref) must be matched +with an [`unlock`](@ref). + +Calling 'lock' will also inhibit running of finalizers on that thread until the +corresponding 'unlock'. Use of the standard lock pattern illustrated below +should naturally be supported, but beware of inverting the try/lock order or +missing the try block entirely (e.g. attempting to return with the lock still +held): + +This provides a acquire/release memory ordering on lock/unlock calls. + +``` +lock(l) +try + +finally + unlock(l) +end +``` + +If [`!islocked(lck::ReentrantLock)`](@ref islocked) holds, [`trylock(lck)`](@ref trylock) +succeeds unless there are other tasks attempting to hold the lock "at the same time." """ mutable struct ReentrantLock <: AbstractLock - locked_by::Union{Task, Nothing} - cond_wait::ThreadSynchronizer - reentrancy_cnt::Int - - ReentrantLock() = new(nothing, ThreadSynchronizer(), 0) + # offset = 16 + @atomic locked_by::Union{Task, Nothing} + # offset32 = 20, offset64 = 24 + reentrancy_cnt::UInt32 + # offset32 = 24, offset64 = 28 + @atomic havelock::UInt8 # 0x0 = none, 0x1 = lock, 0x2 = conflict + # offset32 = 28, offset64 = 32 + cond_wait::ThreadSynchronizer # 2 words + # offset32 = 36, offset64 = 48 + # sizeof32 = 20, sizeof64 = 32 + # now add padding to make this a full cache line to minimize false sharing between objects + _::NTuple{Int === Int32 ? 2 : 3, Int} + # offset32 = 44, offset64 = 72 == sizeof+offset + # sizeof32 = 28, sizeof64 = 56 + + ReentrantLock() = new(nothing, 0x0000_0000, 0x00, ThreadSynchronizer()) end assert_havelock(l::ReentrantLock) = assert_havelock(l, l.locked_by) @@ -24,10 +55,43 @@ assert_havelock(l::ReentrantLock) = assert_havelock(l, l.locked_by) islocked(lock) -> Status (Boolean) Check whether the `lock` is held by any task/thread. -This should not be used for synchronization (see instead [`trylock`](@ref)). +This function alone should not be used for synchronization. However, `islocked` combined +with [`trylock`](@ref) can be used for writing the test-and-test-and-set or exponential +backoff algorithms *if it is supported by the `typeof(lock)`* (read its documentation). + +# Extended help + +For example, an exponential backoff can be implemented as follows if the `lock` +implementation satisfied the properties documented below. + +```julia +nspins = 0 +while true + while islocked(lock) + GC.safepoint() + nspins += 1 + nspins > LIMIT && error("timeout") + end + trylock(lock) && break + backoff() +end +``` + +## Implementation + +A lock implementation is advised to define `islocked` with the following properties and note +it in its docstring. + +* `islocked(lock)` is data-race-free. +* If `islocked(lock)` returns `false`, an immediate invocation of `trylock(lock)` must + succeed (returns `true`) if there is no interference from other tasks. """ +function islocked end +# Above docstring is a documentation for the abstract interface and not the one specific to +# `ReentrantLock`. + function islocked(rl::ReentrantLock) - return rl.reentrancy_cnt != 0 + return (@atomic :monotonic rl.havelock) != 0 end """ @@ -39,23 +103,35 @@ If the lock is already locked by a different task/thread, return `false`. Each successful `trylock` must be matched by an [`unlock`](@ref). + +Function `trylock` combined with [`islocked`](@ref) can be used for writing the +test-and-test-and-set or exponential backoff algorithms *if it is supported by the +`typeof(lock)`* (read its documentation). """ -function trylock(rl::ReentrantLock) - t = current_task() - if t === rl.locked_by - rl.reentrancy_cnt += 1 +function trylock end +# Above docstring is a documentation for the abstract interface and not the one specific to +# `ReentrantLock`. + +@inline function trylock(rl::ReentrantLock) + ct = current_task() + if rl.locked_by === ct + #@assert rl.havelock !== 0x00 + rl.reentrancy_cnt += 0x0000_0001 return true end - lock(rl.cond_wait) - if rl.reentrancy_cnt == 0 - rl.locked_by = t - rl.reentrancy_cnt = 1 - got = true - else - got = false + return _trylock(rl, ct) +end +@noinline function _trylock(rl::ReentrantLock, ct::Task) + GC.disable_finalizers() + if (@atomicreplace :acquire rl.havelock 0x00 => 0x01).success + #@assert rl.locked_by === nothing + #@assert rl.reentrancy_cnt === 0 + rl.reentrancy_cnt = 0x0000_0001 + @atomic :release rl.locked_by = ct + return true end - unlock(rl.cond_wait) - return got + GC.enable_finalizers() + return false end """ @@ -67,27 +143,23 @@ wait for it to become available. Each `lock` must be matched by an [`unlock`](@ref). """ -function lock(rl::ReentrantLock) - t = current_task() - if t === rl.locked_by - rl.reentrancy_cnt += 1 - else - lock(rl.cond_wait) - while true - if rl.reentrancy_cnt == 0 - rl.locked_by = t - rl.reentrancy_cnt = 1 - break - end - try - wait(rl.cond_wait) - catch - unlock(rl.cond_wait) - rethrow() +@inline function lock(rl::ReentrantLock) + trylock(rl) || (@noinline function slowlock(rl::ReentrantLock) + c = rl.cond_wait + lock(c.lock) + try + while true + if (@atomicreplace rl.havelock 0x01 => 0x02).old == 0x00 # :sequentially_consistent ? # now either 0x00 or 0x02 + # it was unlocked, so try to lock it ourself + _trylock(rl, current_task()) && break + else # it was locked, so now wait for the release to notify us + wait(c) + end end + finally + unlock(c.lock) end - unlock(rl.cond_wait) - end + end)(rl) return end @@ -99,56 +171,42 @@ Releases ownership of the `lock`. If this is a recursive lock which has been acquired before, decrement an internal counter and return immediately. """ -function unlock(rl::ReentrantLock) - t = current_task() - n = rl.reentrancy_cnt - n == 0 && error("unlock count must match lock count") - rl.locked_by === t || error("unlock from wrong thread") - if n > 1 - rl.reentrancy_cnt = n - 1 - else - lock(rl.cond_wait) - rl.reentrancy_cnt = 0 - rl.locked_by = nothing - if !isempty(rl.cond_wait.waitq) - try - notify(rl.cond_wait) - catch - unlock(rl.cond_wait) - rethrow() +@inline function unlock(rl::ReentrantLock) + rl.locked_by === current_task() || + error(rl.reentrancy_cnt == 0x0000_0000 ? "unlock count must match lock count" : "unlock from wrong thread") + (@noinline function _unlock(rl::ReentrantLock) + n = rl.reentrancy_cnt - 0x0000_0001 + rl.reentrancy_cnt = n + if n == 0x0000_00000 + @atomic :monotonic rl.locked_by = nothing + if (@atomicswap :release rl.havelock = 0x00) == 0x02 + (@noinline function notifywaiters(rl) + cond_wait = rl.cond_wait + lock(cond_wait) + try + notify(cond_wait) + finally + unlock(cond_wait) + end + end)(rl) end + return true end - unlock(rl.cond_wait) - end - return + return false + end)(rl) && GC.enable_finalizers() + nothing end function unlockall(rl::ReentrantLock) - t = current_task() - n = rl.reentrancy_cnt - rl.locked_by === t || error("unlock from wrong thread") - n == 0 && error("unlock count must match lock count") - lock(rl.cond_wait) - rl.reentrancy_cnt = 0 - rl.locked_by = nothing - if !isempty(rl.cond_wait.waitq) - try - notify(rl.cond_wait) - catch - unlock(rl.cond_wait) - rethrow() - end - end - unlock(rl.cond_wait) + n = @atomicswap :not_atomic rl.reentrancy_cnt = 0x0000_0001 + unlock(rl) return n end -function relockall(rl::ReentrantLock, n::Int) - t = current_task() +function relockall(rl::ReentrantLock, n::UInt32) lock(rl) - n1 = rl.reentrancy_cnt - rl.reentrancy_cnt = n - n1 == 1 || concurrency_violation() + old = @atomicswap :not_atomic rl.reentrancy_cnt = n + old == 0x0000_0001 || concurrency_violation() return end @@ -161,6 +219,9 @@ available. When this function returns, the `lock` has been released, so the caller should not attempt to `unlock` it. + +!!! compat "Julia 1.7" + Using a [`Channel`](@ref) as the second argument requires Julia 1.7 or later. """ function lock(f, l::AbstractLock) lock(l) @@ -182,6 +243,22 @@ function trylock(f, l::AbstractLock) return false end +""" + @lock l expr + +Macro version of `lock(f, l::AbstractLock)` but with `expr` instead of `f` function. +Expands to: +```julia +lock(l) +try + expr +finally + unlock(l) +end +``` +This is similar to using [`lock`](@ref) with a `do` block, but avoids creating a closure +and thus can improve the performance. +""" macro lock(l, expr) quote temp = $(esc(l)) @@ -194,6 +271,13 @@ macro lock(l, expr) end end +""" + @lock_nofail l expr + +Equivalent to `@lock l expr` for cases in which we can guarantee that the function +will not throw any error. In this case, avoiding try-catch can improve the performance. +See [`@lock`](@ref). +""" macro lock_nofail(l, expr) quote temp = $(esc(l)) @@ -249,6 +333,8 @@ end Create a counting semaphore that allows at most `sem_size` acquires to be in use at any time. Each acquire must be matched with a release. + +This provides a acquire & release memory ordering on acquire/release calls. """ mutable struct Semaphore sem_size::Int @@ -276,6 +362,39 @@ function acquire(s::Semaphore) return end +""" + acquire(f, s::Semaphore) + +Execute `f` after acquiring from Semaphore `s`, +and `release` on completion or error. + +For example, a do-block form that ensures only 2 +calls of `foo` will be active at the same time: + +```julia +s = Base.Semaphore(2) +@sync for _ in 1:100 + Threads.@spawn begin + Base.acquire(s) do + foo() + end + end +end +``` + +!!! compat "Julia 1.8" + This method requires at least Julia 1.8. + +""" +function acquire(f, s::Semaphore) + acquire(s) + try + return f() + finally + release(s) + end +end + """ release(s::Semaphore) @@ -297,40 +416,60 @@ end """ - Event() + Event([autoreset=false]) Create a level-triggered event source. Tasks that call [`wait`](@ref) on an -`Event` are suspended and queued until `notify` is called on the `Event`. +`Event` are suspended and queued until [`notify`](@ref) is called on the `Event`. After `notify` is called, the `Event` remains in a signaled state and -tasks will no longer block when waiting for it. +tasks will no longer block when waiting for it, until `reset` is called. + +If `autoreset` is true, at most one task will be released from `wait` for +each call to `notify`. + +This provides an acquire & release memory ordering on notify/wait. !!! compat "Julia 1.1" This functionality requires at least Julia 1.1. + +!!! compat "Julia 1.8" + The `autoreset` functionality and memory ordering guarantee requires at least Julia 1.8. """ mutable struct Event notify::Threads.Condition - set::Bool - Event() = new(Threads.Condition(), false) + autoreset::Bool + @atomic set::Bool + Event(autoreset::Bool=false) = new(Threads.Condition(), autoreset, false) end function wait(e::Event) - e.set && return - lock(e.notify) + if e.autoreset + (@atomicswap :acquire_release e.set = false) && return + else + (@atomic e.set) && return # full barrier also + end + lock(e.notify) # acquire barrier try - while !e.set - wait(e.notify) + if e.autoreset + (@atomicswap :acquire_release e.set = false) && return + else + e.set && return end + wait(e.notify) finally - unlock(e.notify) + unlock(e.notify) # release barrier end nothing end function notify(e::Event) - lock(e.notify) + lock(e.notify) # acquire barrier try - if !e.set - e.set = true + if e.autoreset + if notify(e.notify, all=false) == 0 + @atomic :release e.set = true + end + elseif !e.set + @atomic :release e.set = true notify(e.notify) end finally @@ -339,6 +478,17 @@ function notify(e::Event) nothing end +""" + reset(::Event) + +Reset an Event back into an un-set state. Then any future calls to `wait` will +block until `notify` is called again. +""" +function reset(e::Event) + @atomic e.set = false # full barrier + nothing +end + @eval Threads begin import .Base: Event export Event diff --git a/base/locks-mt.jl b/base/locks-mt.jl index 49c7d2c0f90118..bfa3ac1b8352e4 100644 --- a/base/locks-mt.jl +++ b/base/locks-mt.jl @@ -21,50 +21,27 @@ to execute and does not block (e.g. perform I/O). In general, [`ReentrantLock`](@ref) should be used instead. Each [`lock`](@ref) must be matched with an [`unlock`](@ref). +If [`!islocked(lck::SpinLock)`](@ref islocked) holds, [`trylock(lck)`](@ref trylock) +succeeds unless there are other tasks attempting to hold the lock "at the same time." Test-and-test-and-set spin locks are quickest up to about 30ish contending threads. If you have more contention than that, different synchronization approaches should be considered. """ mutable struct SpinLock <: AbstractLock - owned::Int + # we make this much larger than necessary to minimize false-sharing + @atomic owned::Int SpinLock() = new(0) end -import Base.Sys.WORD_SIZE - -@eval _xchg!(x::SpinLock, v::Int) = - llvmcall($""" - %ptr = inttoptr i$WORD_SIZE %0 to i$WORD_SIZE* - %rv = atomicrmw xchg i$WORD_SIZE* %ptr, i$WORD_SIZE %1 acq_rel - ret i$WORD_SIZE %rv - """, Int, Tuple{Ptr{Int}, Int}, unsafe_convert(Ptr{Int}, pointer_from_objref(x)), v) - -@eval _get(x::SpinLock) = - llvmcall($""" - %ptr = inttoptr i$WORD_SIZE %0 to i$WORD_SIZE* - %rv = load atomic i$WORD_SIZE, i$WORD_SIZE* %ptr monotonic, align $(gc_alignment(Int)) - ret i$WORD_SIZE %rv - """, Int, Tuple{Ptr{Int}}, unsafe_convert(Ptr{Int}, pointer_from_objref(x))) - -@eval _set!(x::SpinLock, v::Int) = - llvmcall($""" - %ptr = inttoptr i$WORD_SIZE %0 to i$WORD_SIZE* - store atomic i$WORD_SIZE %1, i$WORD_SIZE* %ptr release, align $(gc_alignment(Int)) - ret void - """, Cvoid, Tuple{Ptr{Int}, Int}, unsafe_convert(Ptr{Int}, pointer_from_objref(x)), v) - # Note: this cannot assert that the lock is held by the correct thread, because we do not # track which thread locked it. Users beware. Base.assert_havelock(l::SpinLock) = islocked(l) ? nothing : Base.concurrency_violation() function lock(l::SpinLock) while true - if _get(l) == 0 - p = _xchg!(l, 1) - if p == 0 - return - end + if @inline trylock(l) + return end ccall(:jl_cpu_pause, Cvoid, ()) # Temporary solution before we have gc transition support in codegen. @@ -73,18 +50,26 @@ function lock(l::SpinLock) end function trylock(l::SpinLock) - if _get(l) == 0 - return _xchg!(l, 1) == 0 + if l.owned == 0 + GC.disable_finalizers() + p = @atomicswap :acquire l.owned = 1 + if p == 0 + return true + end + GC.enable_finalizers() end return false end function unlock(l::SpinLock) - _set!(l, 0) + if (@atomicswap :release l.owned = 0) == 0 + error("unlock count must match lock count") + end + GC.enable_finalizers() ccall(:jl_cpu_wake, Cvoid, ()) return end function islocked(l::SpinLock) - return _get(l) != 0 + return (@atomic :monotonic l.owned) != 0 end diff --git a/base/logging.jl b/base/logging.jl index 653ded32e443d2..d2b6fa637c1bc9 100644 --- a/base/logging.jl +++ b/base/logging.jl @@ -73,21 +73,21 @@ catch_exceptions(logger) = true # Prevent invalidation when packages define custom loggers # Using invoke in combination with @nospecialize eliminates backedges to these methods -function _invoked_shouldlog(logger, level, _module, group, id) +Base.@constprop :none function _invoked_shouldlog(logger, level, _module, group, id) @nospecialize return invoke( shouldlog, Tuple{typeof(logger), typeof(level), typeof(_module), typeof(group), typeof(id)}, logger, level, _module, group, id - ) + )::Bool end function _invoked_min_enabled_level(@nospecialize(logger)) - return invoke(min_enabled_level, Tuple{typeof(logger)}, logger) + return invoke(min_enabled_level, Tuple{typeof(logger)}, logger)::LogLevel end function _invoked_catch_exceptions(@nospecialize(logger)) - return invoke(catch_exceptions, Tuple{typeof(logger)}, logger) + return invoke(catch_exceptions, Tuple{typeof(logger)}, logger)::Bool end """ @@ -101,7 +101,7 @@ struct NullLogger <: AbstractLogger; end min_enabled_level(::NullLogger) = AboveMaxLevel shouldlog(::NullLogger, args...) = false handle_message(::NullLogger, args...; kwargs...) = - error("Null logger handle_message() should not be called") + (@nospecialize; error("Null logger handle_message() should not be called")) #------------------------------------------------------------------------------- @@ -116,7 +116,7 @@ filtered, before any other work is done to construct the log record data structure itself. # Examples -``` +```julia-repl julia> Logging.LogLevel(0) == Logging.Info true ``` @@ -133,9 +133,29 @@ isless(a::LogLevel, b::LogLevel) = isless(a.level, b.level) convert(::Type{LogLevel}, level::Integer) = LogLevel(level) const BelowMinLevel = LogLevel(-1000001) +""" + Debug + +Alias for [`LogLevel(-1000)`](@ref LogLevel). +""" const Debug = LogLevel( -1000) +""" + Info + +Alias for [`LogLevel(0)`](@ref LogLevel). +""" const Info = LogLevel( 0) +""" + Warn + +Alias for [`LogLevel(1000)`](@ref LogLevel). +""" const Warn = LogLevel( 1000) +""" + Error + +Alias for [`LogLevel(2000)`](@ref LogLevel). +""" const Error = LogLevel( 2000) const AboveMaxLevel = LogLevel( 1000001) @@ -202,7 +222,7 @@ There's also some key value pairs which have conventional meaning: # Examples -``` +```julia @debug "Verbose debugging information. Invisible by default" @info "An informational message" @warn "Something was odd. You should pay attention" @@ -254,10 +274,11 @@ _log_record_ids = Set{Symbol}() # across versions of the originating module, provided the log generating # statement itself doesn't change. function log_record_id(_module, level, message, log_kws) + @nospecialize modname = _module === nothing ? "" : join(fullname(_module), "_") # Use an arbitrarily chosen eight hex digits here. TODO: Figure out how to # make the id exactly the same on 32 and 64 bit systems. - h = UInt32(hash(string(modname, level, message, log_kws)) & 0xFFFFFFFF) + h = UInt32(hash(string(modname, level, message, log_kws)::String) & 0xFFFFFFFF) while true id = Symbol(modname, '_', string(h, base = 16, pad = 8)) # _log_record_ids is a registry of log record ids for use during @@ -274,48 +295,115 @@ end default_group(file) = Symbol(splitext(basename(file))[1]) +function issimple(@nospecialize val) + val isa String && return true + val isa Symbol && return true + val isa QuoteNode && return true + val isa Number && return true + val isa Char && return true + if val isa Expr + val.head === :quote && issimple(val.args[1]) && return true + val.head === :inert && return true + end + return false +end +function issimplekw(@nospecialize val) + if val isa Expr + if val.head === :kw + val = val.args[2] + if val isa Expr && val.head === :escape + issimple(val.args[1]) && return true + end + end + end + return false +end + # Generate code for logging macros function logmsg_code(_module, file, line, level, message, exs...) + @nospecialize log_data = process_logmsg_exs(_module, file, line, level, message, exs...) - quote - let - level = $level - std_level = convert(LogLevel, level) - if std_level >= getindex(_min_enabled_level) - group = $(log_data._group) - _module = $(log_data._module) - logger = current_logger_for_env(std_level, group, _module) - if !(logger === nothing) - id = $(log_data._id) - # Second chance at an early bail-out (before computing the message), - # based on arbitrary logger-specific logic. - if _invoked_shouldlog(logger, level, _module, group, id) - file = $(log_data._file) - line = $(log_data._line) - try - msg = $(esc(message)) - handle_message( + if !isa(message, Symbol) && issimple(message) && isempty(log_data.kwargs) + logrecord = quote + msg = $(message) + kwargs = (;) + true + end + elseif issimple(message) && all(issimplekw, log_data.kwargs) + # if message and kwargs are just values and variables, we can avoid try/catch + # complexity by adding the code for testing the UndefVarError by hand + checkerrors = nothing + for kwarg in reverse(log_data.kwargs) + if isa(kwarg.args[2].args[1], Symbol) + checkerrors = Expr(:if, Expr(:isdefined, kwarg.args[2]), checkerrors, Expr(:call, Expr(:core, :UndefVarError), QuoteNode(kwarg.args[2].args[1]))) + end + end + if isa(message, Symbol) + message = esc(message) + checkerrors = Expr(:if, Expr(:isdefined, message), checkerrors, Expr(:call, Expr(:core, :UndefVarError), QuoteNode(message.args[1]))) + end + logrecord = quote + let err = $checkerrors + if err === nothing + msg = $(message) + kwargs = (;$(log_data.kwargs...)) + true + else + logging_error(logger, level, _module, group, id, file, line, err, false) + false + end + end + end + else + logrecord = quote + try + msg = $(esc(message)) + kwargs = (;$(log_data.kwargs...)) + true + catch err + logging_error(logger, level, _module, group, id, file, line, err, true) + false + end + end + end + return quote + let + level = $level + std_level = convert(LogLevel, level) + if std_level >= _min_enabled_level[] + group = $(log_data._group) + _module = $(log_data._module) + logger = current_logger_for_env(std_level, group, _module) + if !(logger === nothing) + id = $(log_data._id) + # Second chance at an early bail-out (before computing the message), + # based on arbitrary logger-specific logic. + if _invoked_shouldlog(logger, level, _module, group, id) + file = $(log_data._file) + if file isa String + file = Base.fixup_stdlib_path(file) + end + line = $(log_data._line) + local msg, kwargs + $(logrecord) && handle_message( logger, level, msg, _module, group, id, file, line; - $(log_data.kwargs...) - ) - catch err - logging_error(logger, level, _module, group, id, file, line, err) + kwargs...) end end end + nothing end - nothing - end end end function process_logmsg_exs(_orig_module, _file, _line, level, message, exs...) + @nospecialize local _group, _id _module = _orig_module kwargs = Any[] for ex in exs if ex isa Expr && ex.head === :(=) - k,v = ex.args + k, v = ex.args if !(k isa Symbol) k = Symbol(k) end @@ -352,6 +440,7 @@ function process_logmsg_exs(_orig_module, _file, _line, level, message, exs...) end function default_group_code(file) + @nospecialize if file isa String && isdefined(Base, :basename) QuoteNode(default_group(file)) # precompute if we can else @@ -361,43 +450,38 @@ function default_group_code(file) end -# Report an error in log message creation (or in the logger itself). +# Report an error in log message creation @noinline function logging_error(logger, level, _module, group, id, - filepath, line, @nospecialize(err)) + filepath, line, @nospecialize(err), real::Bool) + @nospecialize if !_invoked_catch_exceptions(logger) - rethrow(err) - end - try - msg = "Exception while generating log record in module $_module at $filepath:$line" - handle_message( - logger, Error, msg, _module, :logevent_error, id, filepath, line; - exception=(err,catch_backtrace()) - ) - catch err2 - try - # Give up and write to stderr, in three independent calls to - # increase the odds of it getting through. - print(stderr, "Exception handling log message: ") - println(stderr, err) - println(stderr, " module=$_module file=$filepath line=$line") - println(stderr, " Second exception: ", err2) - catch - end + real ? rethrow(err) : throw(err) end + msg = try + "Exception while generating log record in module $_module at $filepath:$line" + catch ex + LazyString("Exception handling log message: ", ex) + end + bt = real ? catch_backtrace() : backtrace() + handle_message( + logger, Error, msg, _module, :logevent_error, id, filepath, line; + exception=(err,bt)) nothing end # Log a message. Called from the julia C code; kwargs is in the format # Any[key1,val1, ...] for simplicity in construction on the C side. function logmsg_shim(level, message, _module, group, id, file, line, kwargs) - real_kws = Any[(kwargs[i],kwargs[i+1]) for i in 1:2:length(kwargs)] + @nospecialize + real_kws = Any[(kwargs[i], kwargs[i+1]) for i in 1:2:length(kwargs)] @logmsg(convert(LogLevel, level), message, _module=_module, _id=id, _group=group, _file=String(file), _line=line, real_kws...) + nothing end # Global log limiting mechanism for super fast but inflexible global log limiting. -const _min_enabled_level = Ref(Debug) +const _min_enabled_level = Ref{LogLevel}(Debug) # LogState - a cache of data extracted from the logger, plus the logger itself. struct LogState @@ -413,7 +497,7 @@ function current_logstate() end # helper function to get the current logger, if enabled for the specified message type -@noinline function current_logger_for_env(std_level::LogLevel, group, _module) +@noinline Base.@constprop :none function current_logger_for_env(std_level::LogLevel, group, _module) logstate = current_logstate() if std_level >= logstate.min_enabled_level || env_override_minlevel(group, _module) return logstate.logger @@ -444,7 +528,7 @@ a *global* setting, intended to make debug logging extremely cheap when disabled. # Examples -``` +```julia Logging.disable_logging(Logging.Info) # Disable debug and info ``` """ @@ -455,7 +539,7 @@ end let _debug_groups_include::Vector{Symbol} = Symbol[], _debug_groups_exclude::Vector{Symbol} = Symbol[], _debug_str::String = "" -global function env_override_minlevel(group, _module) +global Base.@constprop :none function env_override_minlevel(group, _module) debug = get(ENV, "JULIA_DEBUG", "") if !(debug === _debug_str) _debug_str = debug @@ -550,21 +634,25 @@ attached to the task. """ current_logger() = current_logstate().logger +const closed_stream = IOBuffer(UInt8[]) +close(closed_stream) #------------------------------------------------------------------------------- # SimpleLogger """ - SimpleLogger(stream=stderr, min_level=Info) + SimpleLogger([stream,] min_level=Info) Simplistic logger for logging all messages with level greater than or equal to -`min_level` to `stream`. +`min_level` to `stream`. If stream is closed then messages with log level +greater or equal to `Warn` will be logged to `stderr` and below to `stdout`. """ struct SimpleLogger <: AbstractLogger stream::IO min_level::LogLevel message_limits::Dict{Any,Int} end -SimpleLogger(stream::IO=stderr, level=Info) = SimpleLogger(stream, level, Dict{Any,Int}()) +SimpleLogger(stream::IO, level=Info) = SimpleLogger(stream, level, Dict{Any,Int}()) +SimpleLogger(level=Info) = SimpleLogger(closed_stream, level) shouldlog(logger::SimpleLogger, level, _module, group, id) = get(logger.message_limits, id, 1) > 0 @@ -573,30 +661,37 @@ min_enabled_level(logger::SimpleLogger) = logger.min_level catch_exceptions(logger::SimpleLogger) = false -function handle_message(logger::SimpleLogger, level, message, _module, group, id, - filepath, line; maxlog=nothing, kwargs...) - if maxlog !== nothing && maxlog isa Integer - remaining = get!(logger.message_limits, id, maxlog) +function handle_message(logger::SimpleLogger, level::LogLevel, message, _module, group, id, + filepath, line; kwargs...) + @nospecialize + maxlog = get(kwargs, :maxlog, nothing) + if maxlog isa Core.BuiltinInts + remaining = get!(logger.message_limits, id, Int(maxlog)::Int) logger.message_limits[id] = remaining - 1 remaining > 0 || return end buf = IOBuffer() - iob = IOContext(buf, logger.stream) + stream = logger.stream + if !isopen(stream) + stream = stderr + end + iob = IOContext(buf, stream) levelstr = level == Warn ? "Warning" : string(level) - msglines = split(chomp(string(message)), '\n') - println(iob, "┌ ", levelstr, ": ", msglines[1]) - for i in 2:length(msglines) - println(iob, "│ ", msglines[i]) + msglines = eachsplit(chomp(convert(String, string(message))::String), '\n') + msg1, rest = Iterators.peel(msglines) + println(iob, "┌ ", levelstr, ": ", msg1) + for msg in rest + println(iob, "│ ", msg) end for (key, val) in kwargs + key === :maxlog && continue println(iob, "│ ", key, " = ", val) end - println(iob, "└ @ ", something(_module, "nothing"), " ", - something(filepath, "nothing"), ":", something(line, "nothing")) - write(logger.stream, take!(buf)) + println(iob, "└ @ ", _module, " ", filepath, ":", line) + write(stream, take!(buf)) nothing end -_global_logstate = LogState(SimpleLogger(Core.stderr, CoreLogging.Info)) +_global_logstate = LogState(SimpleLogger()) end # CoreLogging diff --git a/base/math.jl b/base/math.jl index ab77d95b97df6d..9550a0a54b4963 100644 --- a/base/math.jl +++ b/base/math.jl @@ -18,7 +18,7 @@ export sin, cos, sincos, tan, sinh, cosh, tanh, asin, acos, atan, import .Base: log, exp, sin, cos, tan, sinh, cosh, tanh, asin, acos, atan, asinh, acosh, atanh, sqrt, log2, log10, max, min, minmax, ^, exp2, muladd, rem, - exp10, expm1, log1p + exp10, expm1, log1p, @constprop, @assume_effects using .Base: sign_mask, exponent_mask, exponent_one, exponent_half, uinttype, significand_mask, @@ -30,32 +30,56 @@ using Core.Intrinsics: sqrt_llvm using .Base: IEEEFloat @noinline function throw_complex_domainerror(f::Symbol, x) - throw(DomainError(x, string("$f will only return a complex result if called with a ", - "complex argument. Try $f(Complex(x))."))) + throw(DomainError(x, + LazyString(f," will only return a complex result if called with a complex argument. Try ", f,"(Complex(x))."))) end @noinline function throw_exp_domainerror(x) - throw(DomainError(x, string("Exponentiation yielding a complex result requires a ", - "complex argument.\nReplace x^y with (x+0im)^y, ", - "Complex(x)^y, or similar."))) + throw(DomainError(x, LazyString( + "Exponentiation yielding a complex result requires a ", + "complex argument.\nReplace x^y with (x+0im)^y, ", + "Complex(x)^y, or similar."))) end # non-type specific math functions +@inline function two_mul(x::Float64, y::Float64) + if Core.Intrinsics.have_fma(Float64) + xy = x*y + return xy, fma(x, y, -xy) + end + return Base.twomul(x,y) +end + +@inline function two_mul(x::T, y::T) where T<: Union{Float16, Float32} + if Core.Intrinsics.have_fma(T) + xy = x*y + return xy, fma(x, y, -xy) + end + xy = widen(x)*y + Txy = T(xy) + return Txy, T(xy-Txy) +end + """ clamp(x, lo, hi) Return `x` if `lo <= x <= hi`. If `x > hi`, return `hi`. If `x < lo`, return `lo`. Arguments are promoted to a common type. +See also [`clamp!`](@ref), [`min`](@ref), [`max`](@ref). + +!!! compat "Julia 1.3" + `missing` as the first argument requires at least Julia 1.3. + # Examples ```jldoctest -julia> clamp.([pi, 1.0, big(10.)], 2., 9.) +julia> clamp.([pi, 1.0, big(10)], 2.0, 9.0) 3-element Vector{BigFloat}: 3.141592653589793238462643383279502884197169399375105820974944592307816406286198 2.0 9.0 -julia> clamp.([11,8,5],10,6) # an example where lo > hi +julia> clamp.([11, 8, 5], 10, 6) # an example where lo > hi 3-element Vector{Int64}: 6 6 @@ -73,12 +97,18 @@ clamp(x::X, lo::L, hi::H) where {X,L,H} = Clamp `x` between `typemin(T)` and `typemax(T)` and convert the result to type `T`. +See also [`trunc`](@ref). + # Examples ```jldoctest julia> clamp(200, Int8) 127 + julia> clamp(-200, Int8) -128 + +julia> trunc(Int, 4pi^2) +39 ``` """ clamp(x, ::Type{T}) where {T<:Integer} = clamp(x, typemin(T), typemax(T)) % T @@ -89,6 +119,22 @@ clamp(x, ::Type{T}) where {T<:Integer} = clamp(x, typemin(T), typemax(T)) % T Restrict values in `array` to the specified range, in-place. See also [`clamp`](@ref). + +!!! compat "Julia 1.3" + `missing` entries in `array` require at least Julia 1.3. + +# Examples +```jldoctest +julia> row = collect(-4:4)'; + +julia> clamp!(row, 0, Inf) +1×9 adjoint(::Vector{Int64}) with eltype Int64: + 0 0 0 0 0 1 2 3 4 + +julia> clamp.((-4:4)', 0, Inf) +1×9 Matrix{Float64}: + 0.0 0.0 0.0 0.0 0.0 1.0 2.0 3.0 4.0 +``` """ function clamp!(x::AbstractArray, lo, hi) @inbounds for i in eachindex(x) @@ -97,6 +143,15 @@ function clamp!(x::AbstractArray, lo, hi) x end +""" + clamp(x::Integer, r::AbstractUnitRange) + +Clamp `x` to lie within range `r`. + +!!! compat "Julia 1.6" + This method requires at least Julia 1.6. +""" +clamp(x::Integer, r::AbstractUnitRange{<:Integer}) = clamp(x, first(r), last(r)) """ evalpoly(x, p) @@ -196,6 +251,8 @@ end @horner(x, p...) Evaluate `p[1] + x * (p[2] + x * (....))`, i.e. a polynomial via Horner's rule. + +See also [`@evalpoly`](@ref), [`evalpoly`](@ref). """ macro horner(x, p...) xesc, pesc = esc(x), esc.(p) @@ -215,6 +272,8 @@ that is, the coefficients are given in ascending order by power of `z`. This ma to efficient inline code that uses either Horner's method or, for complex `z`, a more efficient Goertzel-like algorithm. +See also [`evalpoly`](@ref). + # Examples ```jldoctest julia> @evalpoly(3, 1, 0, 1) @@ -232,6 +291,19 @@ macro evalpoly(z, p...) :(evalpoly($zesc, ($(pesc...),))) end +# polynomial evaluation using compensated summation. +# much more accurate, especially when lo can be combined with other rounding errors +@inline function exthorner(x, p::Tuple) + hi, lo = p[end], zero(x) + for i in length(p)-1:-1:1 + pi = p[i] + prod, err = two_mul(hi,x) + hi = pi+prod + lo = fma(lo, x, prod - (hi - pi) + err) + end + return hi, lo +end + """ rad2deg(x) @@ -250,6 +322,8 @@ rad2deg(z::AbstractFloat) = z * (180 / oftype(z, pi)) Convert `x` from degrees to radians. +See also: [`rad2deg`](@ref), [`sind`](@ref). + # Examples ```jldoctest julia> deg2rad(90) @@ -356,64 +430,6 @@ Compute the inverse hyperbolic sine of `x`. """ asinh(x::Number) -""" - expm1(x) - -Accurately compute ``e^x-1``. -""" -expm1(x) -for f in (:exp2, :expm1) - @eval begin - ($f)(x::Float64) = ccall(($(string(f)),libm), Float64, (Float64,), x) - ($f)(x::Float32) = ccall(($(string(f,"f")),libm), Float32, (Float32,), x) - ($f)(x::Real) = ($f)(float(x)) - end -end - -""" - exp2(x) - -Compute the base 2 exponential of `x`, in other words ``2^x``. - -# Examples -```jldoctest -julia> exp2(5) -32.0 -``` -""" -exp2(x::AbstractFloat) = 2^x - -""" - exp10(x) - -Compute the base 10 exponential of `x`, in other words ``10^x``. - -# Examples -```jldoctest -julia> exp10(2) -100.0 -``` -""" -exp10(x::AbstractFloat) = 10^x - -for f in (:sinh, :cosh, :tanh, :atan, :asinh, :exp, :expm1) - @eval ($f)(x::AbstractFloat) = error("not implemented for ", typeof(x)) -end - -# functions with special cases for integer arguments -@inline function exp2(x::Base.BitInteger) - if x > 1023 - Inf64 - elseif x <= -1023 - # if -1073 < x <= -1023 then Result will be a subnormal number - # Hex literal with padding must be used to work on 32bit machine - reinterpret(Float64, 0x0000_0000_0000_0001 << ((x + 1074) % UInt)) - else - # We will cast everything to Int64 to avoid errors in case of Int128 - # If x is a Int128, and is outside the range of Int64, then it is not -1023 log(2) @@ -497,6 +519,8 @@ log(x::Number) Compute the logarithm of `x` to base 2. Throws [`DomainError`](@ref) for negative [`Real`](@ref) arguments. +See also: [`exp2`](@ref), [`ldexp`](@ref), [`ispow2`](@ref). + # Examples ```jldoctest; filter = r"Stacktrace:(\\n \\[[0-9]+\\].*)*" julia> log2(4) @@ -507,9 +531,9 @@ julia> log2(10) julia> log2(-2) ERROR: DomainError with -2.0: -NaN result for non-NaN input. +log2 will only return a complex result if called with a complex argument. Try log2(Complex(x)). Stacktrace: - [1] nan_dom_err at ./math.jl:325 [inlined] + [1] throw_complex_domainerror(f::Symbol, x::Float64) at ./math.jl:31 [...] ``` """ @@ -531,9 +555,9 @@ julia> log10(2) julia> log10(-2) ERROR: DomainError with -2.0: -NaN result for non-NaN input. +log10 will only return a complex result if called with a complex argument. Try log10(Complex(x)). Stacktrace: - [1] nan_dom_err at ./math.jl:325 [inlined] + [1] throw_complex_domainerror(f::Symbol, x::Float64) at ./math.jl:31 [...] ``` """ @@ -562,13 +586,6 @@ Stacktrace: ``` """ log1p(x) -for f in (:log2, :log10) - @eval begin - @inline ($f)(x::Float64) = nan_dom_err(ccall(($(string(f)), libm), Float64, (Float64,), x), x) - @inline ($f)(x::Float32) = nan_dom_err(ccall(($(string(f, "f")), libm), Float32, (Float32,), x), x) - @inline ($f)(x::Real) = ($f)(float(x)) - end -end @inline function sqrt(x::Union{Float32,Float64}) x < zero(x) && throw_complex_domainerror(:sqrt, x) @@ -581,6 +598,8 @@ end Return ``\\sqrt{x}``. Throws [`DomainError`](@ref) for negative [`Real`](@ref) arguments. Use complex negative arguments instead. The prefix operator `√` is equivalent to `sqrt`. +See also: [`hypot`](@ref). + # Examples ```jldoctest; filter = r"Stacktrace:(\\n \\[[0-9]+\\].*)*" julia> sqrt(big(81)) @@ -595,9 +614,16 @@ Stacktrace: julia> sqrt(big(complex(-81))) 0.0 + 9.0im + +julia> .√(1:4) +4-element Vector{Float64}: + 1.0 + 1.4142135623730951 + 1.7320508075688772 + 2.0 ``` """ -sqrt(x::Real) = sqrt(float(x)) +sqrt(x) """ hypot(x, y) @@ -607,9 +633,15 @@ Compute the hypotenuse ``\\sqrt{|x|^2+|y|^2}`` avoiding overflow and underflow. This code is an implementation of the algorithm described in: An Improved Algorithm for `hypot(a,b)` by Carlos F. Borges -The article is available online at ArXiv at the link +The article is available online at arXiv at the link https://arxiv.org/abs/1904.09481 + hypot(x...) + +Compute the hypotenuse ``\\sqrt{\\sum |x_i|^2}`` avoiding overflow and underflow. + +See also `norm` in the [`LinearAlgebra`](@ref man-linalg) standard library. + # Examples ```jldoctest; filter = r"Stacktrace:(\\n \\[[0-9]+\\].*)*" julia> a = Int64(10)^10; @@ -625,85 +657,103 @@ Stacktrace: julia> hypot(3, 4im) 5.0 + +julia> hypot(-5.7) +5.7 + +julia> hypot(3, 4im, 12.0) +13.0 + +julia> using LinearAlgebra + +julia> norm([a, a, a, a]) == hypot(a, a, a, a) +true ``` """ -hypot(x::Number, y::Number) = hypot(promote(x, y)...) -hypot(x::Complex, y::Complex) = hypot(abs(x), abs(y)) -hypot(x::T, y::T) where {T<:Real} = hypot(float(x), float(y)) -function hypot(x::T, y::T) where {T<:Number} - if !iszero(x) - z = y/x - z2 = z*z +hypot(x::Number) = abs(float(x)) +hypot(x::Number, y::Number) = _hypot(promote(float(x), y)...) +hypot(x::Number, y::Number, xs::Number...) = _hypot(promote(float(x), y, xs...)) +function _hypot(x, y) + # preserves unit + axu = abs(x) + ayu = abs(y) - abs(x) * sqrt(oneunit(z2) + z2) - else - abs(y) - end -end + # unitless + ax = axu / oneunit(axu) + ay = ayu / oneunit(ayu) -function hypot(x::T, y::T) where T<:AbstractFloat # Return Inf if either or both inputs is Inf (Compliance with IEEE754) - if isinf(x) || isinf(y) - return T(Inf) + if isinf(ax) || isinf(ay) + return typeof(axu)(Inf) end # Order the operands - ax,ay = abs(x), abs(y) if ay > ax - ax,ay = ay,ax + axu, ayu = ayu, axu + ax, ay = ay, ax end # Widely varying operands - if ay <= ax*sqrt(eps(T)/2) #Note: This also gets ay == 0 - return ax + if ay <= ax*sqrt(eps(typeof(ax))/2) #Note: This also gets ay == 0 + return axu end # Operands do not vary widely - scale = eps(T)*sqrt(floatmin(T)) #Rescaling constant - if ax > sqrt(floatmax(T)/2) + scale = eps(typeof(ax))*sqrt(floatmin(ax)) #Rescaling constant + if ax > sqrt(floatmax(ax)/2) ax = ax*scale ay = ay*scale scale = inv(scale) - elseif ay < sqrt(floatmin(T)) + elseif ay < sqrt(floatmin(ax)) ax = ax/scale ay = ay/scale else - scale = one(scale) + scale = oneunit(scale) end - h = sqrt(muladd(ax,ax,ay*ay)) + h = sqrt(muladd(ax, ax, ay*ay)) # This branch is correctly rounded but requires a native hardware fma. - if Base.Math.FMA_NATIVE + if Core.Intrinsics.have_fma(typeof(h)) hsquared = h*h axsquared = ax*ax - h -= (fma(-ay,ay,hsquared-axsquared) + fma(h,h,-hsquared) - fma(ax,ax,-axsquared))/(2*h) + h -= (fma(-ay, ay, hsquared-axsquared) + fma(h, h,-hsquared) - fma(ax, ax, -axsquared))/(2*h) # This branch is within one ulp of correctly rounded. else if h <= 2*ay delta = h-ay - h -= muladd(delta,delta-2*(ax-ay),ax*(2*delta - ax))/(2*h) + h -= muladd(delta, delta-2*(ax-ay), ax*(2*delta - ax))/(2*h) else delta = h-ax - h -= muladd(delta,delta,muladd(ay,(4*delta-ay),2*delta*(ax-2*ay)))/(2*h) + h -= muladd(delta, delta, muladd(ay, (4*delta - ay), 2*delta*(ax - 2*ay)))/(2*h) end end - return h*scale + return h*scale*oneunit(axu) +end +@inline function _hypot(x::Float32, y::Float32) + if isinf(x) || isinf(y) + return Inf32 + end + _x, _y = Float64(x), Float64(y) + return Float32(sqrt(muladd(_x, _x, _y*_y))) +end +@inline function _hypot(x::Float16, y::Float16) + if isinf(x) || isinf(y) + return Inf16 + end + _x, _y = Float32(x), Float32(y) + return Float16(sqrt(muladd(_x, _x, _y*_y))) +end +_hypot(x::ComplexF16, y::ComplexF16) = Float16(_hypot(ComplexF32(x), ComplexF32(y))) + +function _hypot(x::NTuple{N,<:Number}) where {N} + maxabs = maximum(abs, x) + if isnan(maxabs) && any(isinf, x) + return typeof(maxabs)(Inf) + elseif (iszero(maxabs) || isinf(maxabs)) + return maxabs + else + return maxabs * sqrt(sum(y -> abs2(y / maxabs), x)) + end end - -""" - hypot(x...) - -Compute the hypotenuse ``\\sqrt{\\sum |x_i|^2}`` avoiding overflow and underflow. - -# Examples -```jldoctest -julia> hypot(-5.7) -5.7 - -julia> hypot(3, 4im, 12.0) -13.0 -``` -""" -hypot(x::Number...) = sqrt(sum(abs2(y) for y in x)) atan(y::Real, x::Real) = atan(promote(float(y),float(x))...) atan(y::T, x::T) where {T<:AbstractFloat} = Base.no_op_err("atan", T) @@ -735,7 +785,7 @@ function ldexp(x::T, e::Integer) where T<:IEEEFloat xu = reinterpret(Unsigned, x) xs = xu & ~sign_mask(T) xs >= exponent_mask(T) && return x # NaN or Inf - k = Int(xs >> significand_bits(T)) + k = (xs >> significand_bits(T)) % Int if k == 0 # x is subnormal xs == 0 && return x # +-0 m = leading_zeros(xs) - exponent_bits(T) @@ -768,7 +818,8 @@ function ldexp(x::T, e::Integer) where T<:IEEEFloat return flipsign(T(0.0), x) end k += significand_bits(T) - z = T(2.0)^-significand_bits(T) + # z = T(2.0) ^ (-significand_bits(T)) + z = reinterpret(T, rem(exponent_bias(T)-significand_bits(T), uinttype(T)) << significand_bits(T)) xu = (xu & ~exponent_mask(T)) | (rem(k, uinttype(T)) << significand_bits(T)) return z*reinterpret(T, xu) end @@ -780,10 +831,19 @@ ldexp(x::Float16, q::Integer) = Float16(ldexp(Float32(x), q)) Get the exponent of a normalized floating-point number. Returns the largest integer `y` such that `2^y ≤ abs(x)`. + +# Examples +```jldoctest +julia> exponent(6.5) +2 + +julia> exponent(16.0) +4 +``` """ function exponent(x::T) where T<:IEEEFloat @noinline throw1(x) = throw(DomainError(x, "Cannot be NaN or Inf.")) - @noinline throw2(x) = throw(DomainError(x, "Cannot be subnormal converted to 0.")) + @noinline throw2(x) = throw(DomainError(x, "Cannot be ±0.0.")) xs = reinterpret(Unsigned, x) & ~sign_mask(T) xs >= exponent_mask(T) && throw1(x) k = Int(xs >> significand_bits(T)) @@ -795,20 +855,42 @@ function exponent(x::T) where T<:IEEEFloat return k - exponent_bias(T) end +# Like exponent, but assumes the nothrow precondition. For +# internal use only. Could be written as +# @assume_effects :nothrow exponent() +# but currently this form is easier on the compiler. +function _exponent_finite_nonzero(x::T) where T<:IEEEFloat + # @precond :nothrow !isnan(x) && !isinf(x) && !iszero(x) + xs = reinterpret(Unsigned, x) & ~sign_mask(T) + k = rem(xs >> significand_bits(T), Int) + if k == 0 # x is subnormal + m = leading_zeros(xs) - exponent_bits(T) + k = 1 - m + end + return k - exponent_bias(T) +end + """ significand(x) -Extract the `significand(s)` (a.k.a. mantissa), in binary representation, of a -floating-point number. If `x` is a non-zero finite number, then the result will be -a number of the same type on the interval ``[1,2)``. Otherwise `x` is returned. +Extract the significand (a.k.a. mantissa) of a floating-point number. If `x` is +a non-zero finite number, then the result will be a number of the same type and +sign as `x`, and whose absolute value is on the interval ``[1,2)``. Otherwise +`x` is returned. # Examples ```jldoctest -julia> significand(15.2)/15.2 -0.125 +julia> significand(15.2) +1.9 + +julia> significand(-15.2) +-1.9 + +julia> significand(-15.2) * 2^3 +-15.2 -julia> significand(15.2)*8 -15.2 +julia> significand(-Inf), significand(Inf), significand(NaN) +(-Inf, Inf, NaN) ``` """ function significand(x::T) where T<:IEEEFloat @@ -830,6 +912,11 @@ end Return `(x,exp)` such that `x` has a magnitude in the interval ``[1/2, 1)`` or 0, and `val` is equal to ``x \\times 2^{exp}``. +# Examples +```jldoctest +julia> frexp(12.8) +(0.8, 4) +``` """ function frexp(x::T) where T<:IEEEFloat xu = reinterpret(Unsigned, x) @@ -848,11 +935,39 @@ function frexp(x::T) where T<:IEEEFloat return reinterpret(T, xu), k end -rem(x::Float64, y::Float64, ::RoundingMode{:Nearest}) = - ccall((:remainder, libm),Float64,(Float64,Float64),x,y) -rem(x::Float32, y::Float32, ::RoundingMode{:Nearest}) = - ccall((:remainderf, libm),Float32,(Float32,Float32),x,y) -rem(x::Float16, y::Float16, r::RoundingMode{:Nearest}) = Float16(rem(Float32(x), Float32(y), r)) +# NOTE: This `rem` method is adapted from the msun `remainder` and `remainderf` +# functions, which are under the following license: +# +# Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. +# +# Developed at SunSoft, a Sun Microsystems, Inc. business. +# Permission to use, copy, modify, and distribute this +# software is freely granted, provided that this notice +# is preserved. +function rem(x::T, p::T, ::RoundingMode{:Nearest}) where T<:IEEEFloat + (iszero(p) || !isfinite(x) || isnan(p)) && return T(NaN) + x == p && return copysign(zero(T), x) + oldx = x + x = abs(rem(x, 2p)) # 2p may overflow but that's okay + p = abs(p) + if p < 2 * floatmin(T) # Check whether dividing p by 2 will underflow + if 2x > p + x -= p + if 2x >= p + x -= p + end + end + else + p_half = p / 2 + if x > p_half + x -= p + if x >= p_half + x -= p + end + end + end + return flipsign(x, oldx) +end """ @@ -872,47 +987,94 @@ julia> modf(-3.5) """ modf(x) = isinf(x) ? (flipsign(zero(x), x), x) : (rem(x, one(x)), trunc(x)) -function modf(x::Float32) - temp = Ref{Float32}() - f = ccall((:modff, libm), Float32, (Float32, Ptr{Float32}), x, temp) - f, temp[] +function modf(x::T) where T<:IEEEFloat + isinf(x) && return (copysign(zero(T), x), x) + ix = trunc(x) + rx = copysign(x - ix, x) + return (rx, ix) end -function modf(x::Float64) - temp = Ref{Float64}() - f = ccall((:modf, libm), Float64, (Float64, Ptr{Float64}), x, temp) - f, temp[] +# @constprop aggressive to help the compiler see the switch between the integer and float +# variants for callers with constant `y` +@constprop :aggressive function ^(x::Float64, y::Float64) + yint = unsafe_trunc(Int, y) # Note, this is actually safe since julia freezes the result + y == yint && return x^yint + #numbers greater than 2*inv(eps(T)) must be even, and the pow will overflow + y >= 2*inv(eps()) && return x^(typemax(Int64)-1) + xu = reinterpret(UInt64, x) + x<0 && y > -4e18 && throw_exp_domainerror(x) # |y| is small enough that y isn't an integer + x === 1.0 && return 1.0 + x==0 && return abs(y)*Inf*(!(y>0)) + !isfinite(x) && return x*(y>0 || isnan(x)) # x is inf or NaN + if xu < (UInt64(1)<<52) # x is subnormal + xu = reinterpret(UInt64, x * 0x1p52) # normalize x + xu &= ~sign_mask(Float64) + xu -= UInt64(52) << 52 # mess with the exponent + end + return pow_body(xu, y) end -@inline function ^(x::Float64, y::Float64) - z = ccall("llvm.pow.f64", llvmcall, Float64, (Float64, Float64), x, y) - if isnan(z) & !isnan(x+y) - throw_exp_domainerror(x) - end - z +@inline function pow_body(xu::UInt64, y::Float64) + logxhi,logxlo = Base.Math._log_ext(xu) + xyhi, xylo = two_mul(logxhi,y) + xylo = muladd(logxlo, y, xylo) + hi = xyhi+xylo + return Base.Math.exp_impl(hi, xylo-(hi-xyhi), Val(:ℯ)) end -@inline function ^(x::Float32, y::Float32) - z = ccall("llvm.pow.f32", llvmcall, Float32, (Float32, Float32), x, y) - if isnan(z) & !isnan(x+y) - throw_exp_domainerror(x) - end - z + +@constprop :aggressive function ^(x::T, y::T) where T <: Union{Float16, Float32} + yint = unsafe_trunc(Int64, y) # Note, this is actually safe since julia freezes the result + y == yint && return x^yint + #numbers greater than 2*inv(eps(T)) must be even, and the pow will overflow + y >= 2*inv(eps(T)) && return x^(typemax(Int64)-1) + x < 0 && y > -4e18 && throw_exp_domainerror(x) # |y| is small enough that y isn't an integer + return pow_body(x, y) +end + +@inline function pow_body(x::T, y::T) where T <: Union{Float16, Float32} + x == 1 && return one(T) + !isfinite(x) && return x*(y>0 || isnan(x)) + x==0 && return abs(y)*T(Inf)*(!(y>0)) + return T(exp2(log2(abs(widen(x))) * y)) +end + +# compensated power by squaring +@constprop :aggressive @inline function ^(x::Float64, n::Integer) + n == 0 && return one(x) + return pow_body(x, n) end -@inline function ^(x::Float64, y::Integer) - y == -1 && return inv(x) - y == 0 && return one(x) - y == 1 && return x - y == 2 && return x*x - y == 3 && return x*x*x - ccall("llvm.pow.f64", llvmcall, Float64, (Float64, Float64), x, Float64(y)) + +@assume_effects :terminates_locally @noinline function pow_body(x::Float64, n::Integer) + y = 1.0 + xnlo = ynlo = 0.0 + n == 3 && return x*x*x # keep compatibility with literal_pow + if n < 0 + rx = inv(x) + n==-2 && return rx*rx #keep compatability with literal_pow + isfinite(x) && (xnlo = -fma(x, rx, -1.) * rx) + x = rx + n = -n + end + while n > 1 + if n&1 > 0 + err = muladd(y, xnlo, x*ynlo) + y, ynlo = two_mul(x,y) + ynlo += err + end + err = x*2*xnlo + x, xnlo = two_mul(x, x) + xnlo += err + n >>>= 1 + end + !isfinite(x) && return x*y + return muladd(x, y, muladd(y, xnlo, x*ynlo)) end -@inline function ^(x::Float32, y::Integer) - y == -1 && return inv(x) - y == 0 && return one(x) - y == 1 && return x - y == 2 && return x*x - y == 3 && return x*x*x - ccall("llvm.pow.f32", llvmcall, Float32, (Float32, Float32), x, Float32(y)) + +function ^(x::Float32, n::Integer) + n == -2 && return (i=inv(x); i*i) + n == 3 && return x*x*x #keep compatibility with literal_pow + n < 0 && return Float32(Base.power_by_squaring(inv(Float64(x)),-n)) + Float32(Base.power_by_squaring(Float64(x),n)) end @inline ^(x::Float16, y::Integer) = Float16(Float32(x) ^ y) @inline literal_pow(::typeof(^), x::Float16, ::Val{p}) where {p} = Float16(literal_pow(^,Float32(x),Val(p))) @@ -1138,24 +1300,6 @@ julia> 3 * 2 + 1 """ muladd(x,y,z) = x*y+z -# Float16 definitions - -for func in (:sin,:cos,:tan,:asin,:acos,:atan,:sinh,:cosh,:tanh,:asinh,:acosh, - :atanh,:exp,:exp2,:exp10,:log,:log2,:log10,:sqrt,:lgamma,:log1p) - @eval begin - $func(a::Float16) = Float16($func(Float32(a))) - $func(a::ComplexF16) = ComplexF16($func(ComplexF32(a))) - end -end - -for func in (:atan,:hypot) - @eval begin - $func(a::Float16,b::Float16) = Float16($func(Float32(a),Float32(b))) - end -end - -cbrt(a::Float16) = Float16(cbrt(Float32(a))) -sincos(a::Float16) = Float16.(sincos(Float32(a))) # helper functions for Libm functionality @@ -1184,20 +1328,50 @@ Return positive part of the high word of `x` as a `UInt32`. # More special functions include("special/cbrt.jl") include("special/exp.jl") -include("special/exp10.jl") -include("special/ldexp_exp.jl") include("special/hyperbolic.jl") include("special/trig.jl") include("special/rem_pio2.jl") include("special/log.jl") -# `missing` definitions for functions in this module -for f in (:(acos), :(acosh), :(asin), :(asinh), :(atan), :(atanh), - :(sin), :(sinh), :(cos), :(cosh), :(tan), :(tanh), - :(exp), :(exp2), :(expm1), :(log), :(log10), :(log1p), - :(log2), :(exponent), :(sqrt)) + +# Float16 definitions + +for func in (:sin,:cos,:tan,:asin,:acos,:atan,:cosh,:tanh,:asinh,:acosh, + :atanh,:log,:log2,:log10,:sqrt,:lgamma,:log1p) + @eval begin + $func(a::Float16) = Float16($func(Float32(a))) + $func(a::ComplexF16) = ComplexF16($func(ComplexF32(a))) + end +end + +for func in (:exp,:exp2,:exp10,:sinh) + @eval $func(a::ComplexF16) = ComplexF16($func(ComplexF32(a))) +end + + +atan(a::Float16,b::Float16) = Float16(atan(Float32(a),Float32(b))) +sincos(a::Float16) = Float16.(sincos(Float32(a))) + +for f in (:sin, :cos, :tan, :asin, :atan, :acos, + :sinh, :cosh, :tanh, :asinh, :acosh, :atanh, + :exp, :exp2, :exp10, :expm1, :log, :log2, :log10, :log1p, + :exponent, :sqrt, :cbrt) + @eval function ($f)(x::Real) + xf = float(x) + x === xf && throw(MethodError($f, (x,))) + return ($f)(xf) + end @eval $(f)(::Missing) = missing end + +for f in (:atan, :hypot, :log) + @eval $(f)(::Missing, ::Missing) = missing + @eval $(f)(::Number, ::Missing) = missing + @eval $(f)(::Missing, ::Number) = missing +end + +exp2(x::AbstractFloat) = 2^x +exp10(x::AbstractFloat) = 10^x clamp(::Missing, lo, hi) = missing end # module diff --git a/base/mathconstants.jl b/base/mathconstants.jl index a3d1be99becbb5..3bb4bb52ad07f7 100644 --- a/base/mathconstants.jl +++ b/base/mathconstants.jl @@ -23,10 +23,17 @@ Base.@irrational catalan 0.91596559417721901505 catalan The constant pi. +Unicode `π` can be typed by writing `\\pi` then pressing tab in the Julia REPL, and in many editors. + +See also: [`sinpi`](@ref), [`sincospi`](@ref), [`deg2rad`](@ref). + # Examples ```jldoctest julia> pi π = 3.1415926535897... + +julia> 1/2pi +0.15915494309189535 ``` """ π, const pi = π @@ -37,10 +44,20 @@ julia> pi The constant ℯ. +Unicode `ℯ` can be typed by writing `\\euler` and pressing tab in the Julia REPL, and in many editors. + +See also: [`exp`](@ref), [`cis`](@ref), [`cispi`](@ref). + # Examples ```jldoctest julia> ℯ ℯ = 2.7182818284590... + +julia> log(ℯ) +1 + +julia> ℯ^(im)π ≈ -1 +true ``` """ ℯ, const e = ℯ @@ -55,6 +72,11 @@ Euler's constant. ```jldoctest julia> Base.MathConstants.eulergamma γ = 0.5772156649015... + +julia> dx = 10^-6; + +julia> sum(-exp(-x) * log(x) for x in dx:dx:100) * dx +0.5772078382499133 ``` """ γ, const eulergamma = γ @@ -69,6 +91,9 @@ The golden ratio. ```jldoctest julia> Base.MathConstants.golden φ = 1.6180339887498... + +julia> (2ans - 1)^2 ≈ 5 +true ``` """ φ, const golden = φ @@ -82,6 +107,9 @@ Catalan's constant. ```jldoctest julia> Base.MathConstants.catalan catalan = 0.9159655941772... + +julia> sum(log(x)/(1+x^2) for x in 1:0.01:10^6) * 0.01 +0.9159466120554123 ``` """ catalan @@ -95,4 +123,10 @@ Base.literal_pow(::typeof(^), ::Irrational{:ℯ}, ::Val{p}) where {p} = exp(p) Base.log(::Irrational{:ℯ}) = 1 # use 1 to correctly promote expressions like log(x)/log(ℯ) Base.log(::Irrational{:ℯ}, x::Number) = log(x) +Base.sin(::Irrational{:π}) = 0.0 +Base.cos(::Irrational{:π}) = -1.0 +Base.sincos(::Irrational{:π}) = (0.0, -1.0) +Base.tan(::Irrational{:π}) = 0.0 +Base.cot(::Irrational{:π}) = -1/0 + end # module diff --git a/base/meta.jl b/base/meta.jl index ff2ea563cb3234..cf59d3fa3274e1 100644 --- a/base/meta.jl +++ b/base/meta.jl @@ -9,14 +9,24 @@ using ..CoreLogging export quot, isexpr, + isidentifier, + isoperator, + isunaryoperator, + isbinaryoperator, + ispostfixoperator, + replace_sourceloc!, show_sexpr, @dump +using Base: isidentifier, isoperator, isunaryoperator, isbinaryoperator, ispostfixoperator +import Base: isexpr + """ Meta.quot(ex)::Expr -Quote expression `ex` to produce an expression with head `quote`. This can for instance be used to represent objects of type `Expr` in the AST. -See also the manual section about [QuoteNode](@ref man-quote-node). +Quote expression `ex` to produce an expression with head `quote`. This can for +instance be used to represent objects of type `Expr` in the AST. See also the +manual section about [QuoteNode](@ref man-quote-node). # Examples ```jldoctest @@ -38,7 +48,10 @@ quot(ex) = Expr(:quote, ex) """ Meta.isexpr(ex, head[, n])::Bool -Check if `ex` is an expression with head `head` and `n` arguments. +Return true if `ex` is an `Expr` with the given type `head` and optionally that +the argument list is of length `n`. `head` may be a `Symbol` or collection of +`Symbol`s. For example, to check that a macro was passed a function call +expression, you might use `isexpr(ex, :call)`. # Examples ```jldoctest @@ -61,10 +74,36 @@ julia> Meta.isexpr(ex, :call, 2) true ``` """ -isexpr(@nospecialize(ex), head::Symbol) = isa(ex, Expr) && ex.head === head -isexpr(@nospecialize(ex), heads) = isa(ex, Expr) && in(ex.head, heads) -isexpr(@nospecialize(ex), head::Symbol, n::Int) = isa(ex, Expr) && ex.head === head && length(ex.args) == n -isexpr(@nospecialize(ex), heads, n::Int) = isa(ex, Expr) && in(ex.head, heads) && length(ex.args) == n +isexpr + +""" + replace_sourceloc!(location, expr) + +Overwrite the caller source location for each macro call in `expr`, returning +the resulting AST. This is useful when you need to wrap a macro inside a +macro, and want the inner macro to see the `__source__` location of the outer +macro. For example: + +``` +macro test_is_one(ex) + replace_sourceloc!(__source__, :(@test \$(esc(ex)) == 1)) +end +@test_is_one 2 +``` + +`@test` now reports the location of the call `@test_is_one 2` to the user, +rather than line 2 where `@test` is used as an implementation detail. +""" +function replace_sourceloc!(sourceloc, @nospecialize(ex)) + if ex isa Expr + if ex.head == :macrocall + ex.args[2] = sourceloc + end + map!(e -> replace_sourceloc!(sourceloc, e), ex.args, ex.args) + end + return ex +end + """ Meta.show_sexpr([io::IO,], ex) @@ -147,41 +186,52 @@ The expression passed to the [`parse`](@ref) function could not be interpreted a expression. """ struct ParseError <: Exception - msg::AbstractString + msg::String end function _parse_string(text::AbstractString, filename::AbstractString, - index::Integer, options) + lineno::Integer, index::Integer, options) if index < 1 || index > ncodeunits(text) + 1 throw(BoundsError(text, index)) end - ex, offset::Int = Core._parse(text, filename, index-1, options) + ex, offset::Int = Core._parse(text, filename, lineno, index-1, options) ex, offset+1 end """ parse(str, start; greedy=true, raise=true, depwarn=true) -Parse the expression string and return an expression (which could later be passed to eval -for execution). `start` is the index of the first character to start parsing. If `greedy` is -`true` (default), `parse` will try to consume as much input as it can; otherwise, it will -stop as soon as it has parsed a valid expression. Incomplete but otherwise syntactically -valid expressions will return `Expr(:incomplete, "(error message)")`. If `raise` is `true` -(default), syntax errors other than incomplete expressions will raise an error. If `raise` -is `false`, `parse` will return an expression that will raise an error upon evaluation. If -`depwarn` is `false`, deprecation warnings will be suppressed. +Parse the expression string and return an expression (which could later be +passed to eval for execution). `start` is the code unit index into `str` of the +first character to start parsing at (as with all string indexing, these are not +character indices). If `greedy` is `true` (default), `parse` will try to consume +as much input as it can; otherwise, it will stop as soon as it has parsed a +valid expression. Incomplete but otherwise syntactically valid expressions will +return `Expr(:incomplete, "(error message)")`. If `raise` is `true` (default), +syntax errors other than incomplete expressions will raise an error. If `raise` +is `false`, `parse` will return an expression that will raise an error upon +evaluation. If `depwarn` is `false`, deprecation warnings will be suppressed. ```jldoctest -julia> Meta.parse("x = 3, y = 5", 7) -(:(y = 5), 13) +julia> Meta.parse("(α, β) = 3, 5", 1) # start of string +(:((α, β) = (3, 5)), 16) + +julia> Meta.parse("(α, β) = 3, 5", 1, greedy=false) +(:((α, β)), 9) + +julia> Meta.parse("(α, β) = 3, 5", 16) # end of string +(nothing, 16) + +julia> Meta.parse("(α, β) = 3, 5", 11) # index of 3 +(:((3, 5)), 16) -julia> Meta.parse("x = 3, y = 5", 5) -(:((3, y) = 5), 13) +julia> Meta.parse("(α, β) = 3, 5", 11, greedy=false) +(3, 13) ``` """ function parse(str::AbstractString, pos::Integer; greedy::Bool=true, raise::Bool=true, depwarn::Bool=true) - ex, pos = _parse_string(str, "none", pos, greedy ? :statement : :atom) + ex, pos = _parse_string(str, "none", 1, pos, greedy ? :statement : :atom) if raise && isa(ex,Expr) && ex.head === :error throw(ParseError(ex.args[1])) end @@ -225,12 +275,12 @@ function parse(str::AbstractString; raise::Bool=true, depwarn::Bool=true) return ex end -function parseatom(text::AbstractString, pos::Integer; filename="none") - return _parse_string(text, filename, pos, :atom) +function parseatom(text::AbstractString, pos::Integer; filename="none", lineno=1) + return _parse_string(text, String(filename), lineno, pos, :atom) end -function parseall(text::AbstractString; filename="none") - ex,_ = _parse_string(text, filename, 1, :all) +function parseall(text::AbstractString; filename="none", lineno=1) + ex,_ = _parse_string(text, String(filename), lineno, 1, :all) return ex end @@ -303,10 +353,26 @@ function _partially_inline!(@nospecialize(x), slot_replacements::Vector{Any}, x.edges .+= slot_offset return x end + if isa(x, Core.ReturnNode) + return Core.ReturnNode( + _partially_inline!(x.val, slot_replacements, type_signature, static_param_values, + slot_offset, statement_offset, boundscheck), + ) + end + if isa(x, Core.GotoIfNot) + return Core.GotoIfNot( + _partially_inline!(x.cond, slot_replacements, type_signature, static_param_values, + slot_offset, statement_offset, boundscheck), + x.dest + statement_offset, + ) + end if isa(x, Expr) head = x.head if head === :static_parameter - return QuoteNode(static_param_values[x.args[1]]) + if isassigned(static_param_values, x.args[1]) + return QuoteNode(static_param_values[x.args[1]]) + end + return x elseif head === :cfunction @assert !isa(type_signature, UnionAll) || !isempty(spvals) if !isa(x.args[2], QuoteNode) # very common no-op @@ -326,7 +392,7 @@ function _partially_inline!(@nospecialize(x), slot_replacements::Vector{Any}, elseif i == 4 @assert isa(x.args[4], Int) elseif i == 5 - @assert isa((x.args[5]::QuoteNode).value, Symbol) + @assert isa((x.args[5]::QuoteNode).value, Union{Symbol, Tuple{Symbol, UInt8}}) else x.args[i] = _partially_inline!(x.args[i], slot_replacements, type_signature, static_param_values, @@ -349,7 +415,31 @@ function _partially_inline!(@nospecialize(x), slot_replacements::Vector{Any}, x.args[2] += statement_offset elseif head === :enter x.args[1] += statement_offset - elseif !is_meta_expr_head(head) + elseif head === :isdefined + arg = x.args[1] + # inlining a QuoteNode or literal into `Expr(:isdefined, x)` is invalid, replace with true + if isa(arg, Core.SlotNumber) + id = arg.id + if 1 <= id <= length(slot_replacements) + replacement = slot_replacements[id] + if isa(replacement, Union{Core.SlotNumber, GlobalRef, Symbol}) + return Expr(:isdefined, replacement) + else + @assert !isa(replacement, Expr) + return true + end + end + return Expr(:isdefined, Core.SlotNumber(id + slot_offset)) + elseif isexpr(arg, :static_parameter) + if isassigned(static_param_values, arg.args[1]) + return true + end + return x + else + @assert isa(arg, Union{GlobalRef, Symbol}) + return x + end + elseif !Core.Compiler.is_meta_expr_head(head) partially_inline!(x.args, slot_replacements, type_signature, static_param_values, slot_offset, statement_offset, boundscheck) end @@ -359,6 +449,4 @@ end _instantiate_type_in_env(x, spsig, spvals) = ccall(:jl_instantiate_type_in_env, Any, (Any, Any, Ptr{Any}), x, spsig, spvals) -is_meta_expr_head(head::Symbol) = (head === :inbounds || head === :boundscheck || head === :meta || head === :loopinfo) - end # module diff --git a/base/methodshow.jl b/base/methodshow.jl index 1894e62b4a3765..1fe12d718457d0 100644 --- a/base/methodshow.jl +++ b/base/methodshow.jl @@ -2,48 +2,52 @@ # Method and method table pretty-printing +const empty_sym = Symbol("") +function strip_gensym(sym) + if sym === :var"#self#" || sym === :var"#unused#" + return empty_sym + end + return Symbol(replace(String(sym), r"^(.*)#(.*#)?\d+$" => s"\1")) +end + function argtype_decl(env, n, @nospecialize(sig::DataType), i::Int, nargs, isva::Bool) # -> (argname, argtype) - t = sig.parameters[i] - if i == nargs && isva && !isvarargtype(t) - t = Vararg{t,length(sig.parameters)-nargs+1} + t = sig.parameters[unwrapva(min(i, end))] + if i == nargs && isva + va = sig.parameters[end] + if isvarargtype(va) && (!isdefined(va, :N) || !isa(va.N, Int)) + t = va + else + ntotal = length(sig.parameters) + isvarargtype(va) && (ntotal += va.N - 1) + t = Vararg{t,ntotal-nargs+1} + end end if isa(n,Expr) n = n.args[1] # handle n::T in arg list end - s = string(n)::String - i = findfirst(isequal('#'), s) - if i !== nothing - s = s[1:prevind(s, i)::Int] - end - if t === Any && !isempty(s) - return s, "" + n = strip_gensym(n) + local s + if n === empty_sym + s = "" + else + s = sprint(show_sym, n) + t === Any && return s, "" end if isvarargtype(t) - v1, v2 = nothing, nothing - if isa(t, UnionAll) - v1 = t.var - t = t.body - if isa(t, UnionAll) - v2 = t.var - t = t.body - end - end - ut = unwrap_unionall(t) - tt, tn = ut.parameters[1], ut.parameters[2] - if isa(tn, TypeVar) && (tn === v1 || tn === v2) - if tt === Any || (isa(tt, TypeVar) && (tt === v1 || tt === v2)) + if !isdefined(t, :N) + if unwrapva(t) === Any return string(s, "..."), "" else - return s, string_with_env(env, tt) * "..." + return s, string_with_env(env, unwrapva(t)) * "..." end end - return s, string_with_env(env, "Vararg{", tt, ", ", tn, "}") + return s, string_with_env(env, "Vararg{", t.T, ", ", t.N, "}") end return s, string_with_env(env, t) end function method_argnames(m::Method) - argnames = ccall(:jl_uncompress_argnames, Vector{Any}, (Any,), m.slot_syms) + argnames = ccall(:jl_uncompress_argnames, Vector{Symbol}, (Any,), m.slot_syms) isempty(argnames) && return argnames return argnames[1:m.nargs] end @@ -65,7 +69,7 @@ function arg_decl_parts(m::Method, html=false) end decls = Tuple{String,String}[argtype_decl(show_env, argnames[i], sig, i, m.nargs, m.isva) for i = 1:m.nargs] - decls[1] = ("", sprint(show_signature_function, sig.parameters[1], false, decls[1][1], html, + decls[1] = ("", sprint(show_signature_function, unwrapva(sig.parameters[1]), false, decls[1][1], html, context = show_env)) else decls = Tuple{String,String}[("", "") for i = 1:length(sig.parameters::SimpleVector)] @@ -73,10 +77,11 @@ function arg_decl_parts(m::Method, html=false) return tv, decls, file, line end -const empty_sym = Symbol("") - # NOTE: second argument is deprecated and is no longer used function kwarg_decl(m::Method, kwtype = nothing) + if m.sig === Tuple # OpaqueClosure + return Symbol[] + end mt = get_methodtable(m) if isdefined(mt, :kwsorter) kwtype = typeof(mt.kwsorter) @@ -84,7 +89,7 @@ function kwarg_decl(m::Method, kwtype = nothing) kwli = ccall(:jl_methtable_lookup, Any, (Any, Any, UInt), kwtype.name.mt, sig, get_world_counter()) if kwli !== nothing kwli = kwli::Method - slotnames = ccall(:jl_uncompress_argnames, Vector{Any}, (Any,), kwli.slot_syms) + slotnames = ccall(:jl_uncompress_argnames, Vector{Symbol}, (Any,), kwli.slot_syms) kws = filter(x -> !(x === empty_sym || '#' in string(x)), slotnames[(kwli.nargs + 1):end]) # ensure the kwarg... is always printed last. The order of the arguments are not # necessarily the same as defined in the function @@ -96,7 +101,7 @@ function kwarg_decl(m::Method, kwtype = nothing) return kws end end - return Any[] + return Symbol[] end function show_method_params(io::IO, tv) @@ -122,24 +127,37 @@ end # In case the line numbers in the source code have changed since the code was compiled, # allow packages to set a callback function that corrects them. # (Used by Revise and perhaps other packages.) +# Any function `f` stored here must be consistent with the signature +# f(m::Method)::Tuple{Union{Symbol,String}, Union{Int32,Int64}} const methodloc_callback = Ref{Union{Function, Nothing}}(nothing) +function fixup_stdlib_path(path::String) + # The file defining Base.Sys gets included after this file is included so make sure + # this function is valid even in this intermediary state + if isdefined(@__MODULE__, :Sys) + BUILD_STDLIB_PATH = Sys.BUILD_STDLIB_PATH::String + STDLIB = Sys.STDLIB::String + if BUILD_STDLIB_PATH != STDLIB + # BUILD_STDLIB_PATH gets defined in sysinfo.jl + npath = normpath(path) + npath′ = replace(npath, normpath(BUILD_STDLIB_PATH) => normpath(STDLIB)) + return npath == npath′ ? path : npath′ + end + end + return path +end + # This function does the method location updating function updated_methodloc(m::Method)::Tuple{String, Int32} file, line = m.file, m.line if methodloc_callback[] !== nothing try - file, line = invokelatest(methodloc_callback[], m) + file, line = invokelatest(methodloc_callback[], m)::Tuple{Union{Symbol,String}, Union{Int32,Int64}} catch end end - # The file defining Base.Sys gets included after this file is included so make sure - # this function is valid even in this intermediary state - if isdefined(@__MODULE__, :Sys) && Sys.BUILD_STDLIB_PATH != Sys.STDLIB - # BUILD_STDLIB_PATH gets defined in sysinfo.jl - file = replace(string(file), normpath(Sys.BUILD_STDLIB_PATH) => normpath(Sys.STDLIB)) - end - return string(file), line + file = fixup_stdlib_path(string(file)) + return file, Int32(line) end functionloc(m::Core.MethodInstance) = functionloc(m.def) @@ -179,6 +197,15 @@ function functionloc(@nospecialize(f)) return functionloc(first(mt)) end +function sym_to_string(sym) + s = String(sym) + if endswith(s, "...") + return string(sprint(show_sym, Symbol(s[1:end-3])), "...") + else + return sprint(show_sym, sym) + end +end + function show(io::IO, m::Method) tv, decls, file, line = arg_decl_parts(m) sig = unwrap_unionall(m.sig) @@ -188,12 +215,16 @@ function show(io::IO, m::Method) return end print(io, decls[1][2], "(") - join(io, String[isempty(d[2]) ? d[1] : d[1]*"::"*d[2] for d in decls[2:end]], - ", ", ", ") + join( + io, + String[isempty(d[2]) ? d[1] : string(d[1], "::", d[2]) for d in decls[2:end]], + ", ", + ", ", + ) kwargs = kwarg_decl(m) if !isempty(kwargs) print(io, "; ") - join(io, kwargs, ", ", ", ") + join(io, map(sym_to_string, kwargs), ", ", ", ") end print(io, ")") show_method_params(io, tv) @@ -202,6 +233,7 @@ function show(io::IO, m::Method) file, line = updated_methodloc(m) print(io, " at ", file, ":", line) end + nothing end function show_method_list_header(io::IO, ms::MethodList, namefmt::Function) @@ -210,24 +242,27 @@ function show_method_list_header(io::IO, ms::MethodList, namefmt::Function) hasname = isdefined(mt.module, name) && typeof(getfield(mt.module, name)) <: Function n = length(ms) - if mt.module === Core && n == 0 && mt.defs === nothing && mt.cache !== nothing - # try to detect Builtin - print(io, "# built-in function; no methods") + m = n==1 ? "method" : "methods" + print(io, "# $n $m") + sname = string(name) + namedisplay = namefmt(sname) + if hasname + what = (startswith(sname, '@') ? + "macro" + : mt.module === Core && last(ms).sig === Tuple ? + "builtin function" + : # else + "generic function") + print(io, " for ", what, " ", namedisplay) + elseif '#' in sname + print(io, " for anonymous function ", namedisplay) + elseif mt === _TYPE_NAME.mt + print(io, " for type constructor") else - m = n==1 ? "method" : "methods" - print(io, "# $n $m") - sname = string(name) - namedisplay = namefmt(sname) - if hasname - what = startswith(sname, '@') ? "macro" : "generic function" - print(io, " for ", what, " ", namedisplay) - elseif '#' in sname - print(io, " for anonymous function ", namedisplay) - elseif mt === _TYPE_NAME.mt - print(io, " for type constructor") - end - print(io, ":") + print(io, " for callable object") end + n > 0 && print(io, ":") + nothing end function show_method_table(io::IO, ms::MethodList, max::Int=-1, header::Bool=true) @@ -245,7 +280,7 @@ function show_method_table(io::IO, ms::MethodList, max::Int=-1, header::Bool=tru last_shown_line_infos === nothing || empty!(last_shown_line_infos) for meth in ms - if max==-1 || n"*d[2]*"" - for d in decls[2:end]], ", ", ", ") + join( + io, + String[ + isempty(d[2]) ? d[1] : string(d[1], "::", d[2], "") for d in decls[2:end] + ], + ", ", + ", ", + ) kwargs = kwarg_decl(m) if !isempty(kwargs) print(io, "; ") - join(io, kwargs, ", ", ", ") + join(io, map(sym_to_string, kwargs), ", ", ", ") print(io, "") end print(io, ")") diff --git a/base/missing.jl b/base/missing.jl index 1d42188a656c01..e1988064aadc12 100644 --- a/base/missing.jl +++ b/base/missing.jl @@ -12,7 +12,7 @@ where it is not supported. The error message, in the `msg` field may provide more specific details. """ struct MissingException <: Exception - msg::AbstractString + msg::String end showerror(io::IO, ex::MissingException) = @@ -36,7 +36,7 @@ Any !!! compat "Julia 1.3" This function is exported as of Julia 1.3. """ -nonmissingtype(::Type{T}) where {T} = Core.Compiler.typesubtract(T, Missing) +nonmissingtype(::Type{T}) where {T} = typesplit(T, Missing) function nonmissingtype_checked(T::Type) R = nonmissingtype(T) @@ -91,10 +91,11 @@ isapprox(::Missing, ::Any; kwargs...) = missing isapprox(::Any, ::Missing; kwargs...) = missing # Unary operators/functions -for f in (:(!), :(~), :(+), :(-), :(zero), :(one), :(oneunit), +for f in (:(!), :(~), :(+), :(-), :(*), :(&), :(|), :(xor), + :(zero), :(one), :(oneunit), :(isfinite), :(isinf), :(isodd), :(isinteger), :(isreal), :(isnan), - :(iszero), :(transpose), :(adjoint), :(float), :(conj), + :(iszero), :(transpose), :(adjoint), :(float), :(complex), :(conj), :(abs), :(abs2), :(iseven), :(ispow2), :(real), :(imag), :(sign), :(inv)) @eval ($f)(::Missing) = missing @@ -106,6 +107,13 @@ for f in (:(Base.zero), :(Base.one), :(Base.oneunit)) $f(T) end end +for f in (:(Base.float), :(Base.complex)) + @eval $f(::Type{Missing}) = Missing + @eval function $f(::Type{Union{T, Missing}}) where T + T === Any && throw(MethodError($f, (Any,))) # To prevent StackOverflowError + Union{$f(T), Missing} + end +end # Binary operators/functions for f in (:(+), :(-), :(*), :(/), :(^), :(mod), :(rem)) @@ -171,8 +179,8 @@ xor(b::Bool, a::Missing) = missing xor(::Missing, ::Integer) = missing xor(::Integer, ::Missing) = missing -*(d::Missing, x::AbstractString) = missing -*(d::AbstractString, x::Missing) = missing +*(d::Missing, x::Union{AbstractString,AbstractChar}) = missing +*(d::Union{AbstractString,AbstractChar}, x::Missing) = missing function float(A::AbstractArray{Union{T, Missing}}) where {T} U = typeof(float(zero(T))) @@ -193,6 +201,8 @@ Use [`collect`](@ref) to obtain an `Array` containing the non-`missing` values i be a `Vector` since it is not possible to remove missings while preserving dimensions of the input. +See also [`coalesce`](@ref), [`ismissing`](@ref), [`something`](@ref). + # Examples ```jldoctest julia> x = skipmissing([1, missing, 2]) @@ -273,24 +283,24 @@ mapreduce(f, op, itr::SkipMissing{<:AbstractArray}) = function _mapreduce(f, op, ::IndexLinear, itr::SkipMissing{<:AbstractArray}) A = itr.x - local ai + ai = missing inds = LinearIndices(A) i = first(inds) ilast = last(inds) - while i <= ilast + for outer i in i:ilast @inbounds ai = A[i] - ai === missing || break - i += 1 + ai !== missing && break end - i > ilast && return mapreduce_empty(f, op, eltype(itr)) + ai === missing && return mapreduce_empty(f, op, eltype(itr)) a1::eltype(itr) = ai + i == typemax(typeof(i)) && return mapreduce_first(f, op, a1) i += 1 - while i <= ilast + ai = missing + for outer i in i:ilast @inbounds ai = A[i] - ai === missing || break - i += 1 + ai !== missing && break end - i > ilast && return mapreduce_first(f, op, a1) + ai === missing && return mapreduce_first(f, op, a1) # We know A contains at least two non-missing entries: the result cannot be nothing something(mapreduce_impl(f, op, itr, first(inds), last(inds))) end @@ -304,32 +314,35 @@ mapreduce_impl(f, op, A::SkipMissing, ifirst::Integer, ilast::Integer) = @noinline function mapreduce_impl(f, op, itr::SkipMissing{<:AbstractArray}, ifirst::Integer, ilast::Integer, blksize::Int) A = itr.x - if ifirst == ilast + if ifirst > ilast + return nothing + elseif ifirst == ilast @inbounds a1 = A[ifirst] if a1 === missing return nothing else return Some(mapreduce_first(f, op, a1)) end - elseif ifirst + blksize > ilast + elseif ilast - ifirst < blksize # sequential portion - local ai + ai = missing i = ifirst - while i <= ilast + for outer i in i:ilast @inbounds ai = A[i] - ai === missing || break - i += 1 + ai !== missing && break end - i > ilast && return nothing + ai === missing && return nothing a1 = ai::eltype(itr) + i == typemax(typeof(i)) && return Some(mapreduce_first(f, op, a1)) i += 1 - while i <= ilast + ai = missing + for outer i in i:ilast @inbounds ai = A[i] - ai === missing || break - i += 1 + ai !== missing && break end - i > ilast && return Some(mapreduce_first(f, op, a1)) + ai === missing && return Some(mapreduce_first(f, op, a1)) a2 = ai::eltype(itr) + i == typemax(typeof(i)) && return Some(op(f(a1), f(a2))) i += 1 v = op(f(a1), f(a2)) @simd for i = i:ilast @@ -341,7 +354,7 @@ mapreduce_impl(f, op, A::SkipMissing, ifirst::Integer, ilast::Integer) = return Some(v) else # pairwise portion - imid = (ifirst + ilast) >> 1 + imid = ifirst + (ilast - ifirst) >> 1 v1 = mapreduce_impl(f, op, itr, ifirst, imid, blksize) v2 = mapreduce_impl(f, op, itr, imid+1, ilast, blksize) if v1 === nothing && v2 === nothing @@ -388,12 +401,12 @@ function filter(f, itr::SkipMissing{<:AbstractArray}) end """ - coalesce(x, y...) + coalesce(x...) Return the first value in the arguments which is not equal to [`missing`](@ref), if any. Otherwise return `missing`. -See also [`something`](@ref). +See also [`skipmissing`](@ref), [`something`](@ref), [`@coalesce`](@ref). # Examples @@ -415,3 +428,38 @@ function coalesce end coalesce() = missing coalesce(x::Missing, y...) = coalesce(y...) coalesce(x::Any, y...) = x + + +""" + @coalesce(x...) + +Short-circuiting version of [`coalesce`](@ref). + +# Examples +```jldoctest +julia> f(x) = (println("f(\$x)"); missing); + +julia> a = 1; + +julia> a = @coalesce a f(2) f(3) error("`a` is still missing") +1 + +julia> b = missing; + +julia> b = @coalesce b f(2) f(3) error("`b` is still missing") +f(2) +f(3) +ERROR: `b` is still missing +[...] +``` + +!!! compat "Julia 1.7" + This macro is available as of Julia 1.7. +""" +macro coalesce(args...) + expr = :(missing) + for arg in reverse(args) + expr = :((val = $arg) !== missing ? val : $expr) + end + return esc(:(let val; $expr; end)) +end diff --git a/base/mpfr.jl b/base/mpfr.jl index 4fc190691ecd8b..60f59cdb0af7ea 100644 --- a/base/mpfr.jl +++ b/base/mpfr.jl @@ -11,19 +11,18 @@ import inv, exp, exp2, exponent, factorial, floor, fma, hypot, isinteger, isfinite, isinf, isnan, ldexp, log, log2, log10, max, min, mod, modf, nextfloat, prevfloat, promote_rule, rem, rem2pi, round, show, float, - sum, sqrt, string, print, trunc, precision, exp10, expm1, - log1p, + sum, sqrt, string, print, trunc, precision, _precision, exp10, expm1, log1p, eps, signbit, sign, sin, cos, sincos, tan, sec, csc, cot, acos, asin, atan, - cosh, sinh, tanh, sech, csch, coth, acosh, asinh, atanh, + cosh, sinh, tanh, sech, csch, coth, acosh, asinh, atanh, lerpi, cbrt, typemax, typemin, unsafe_trunc, floatmin, floatmax, rounding, setrounding, maxintfloat, widen, significand, frexp, tryparse, iszero, - isone, big, _string_n + isone, big, _string_n, decompose -import .Base.Rounding: rounding_raw, setrounding_raw +import ..Rounding: rounding_raw, setrounding_raw -import .Base.GMP: ClongMax, CulongMax, CdoubleMax, Limb +import ..GMP: ClongMax, CulongMax, CdoubleMax, Limb -import .Base.FastMath.sincos_fast +import ..FastMath.sincos_fast version() = VersionNumber(unsafe_string(ccall((:mpfr_get_version,:libmpfr), Ptr{Cchar}, ()))) patches() = split(unsafe_string(ccall((:mpfr_get_patches,:libmpfr), Ptr{Cchar}, ())),' ') @@ -152,6 +151,11 @@ global precision; `convert` will always return `x`. convenience since decimal literals are converted to `Float64` when parsed, so `BigFloat(2.1)` may not yield what you expect. +See also: +- [`@big_str`](@ref) +- [`rounding`](@ref) and [`setrounding`](@ref) +- [`precision`](@ref) and [`setprecision`](@ref) + !!! compat "Julia 1.1" `precision` as a keyword argument requires at least Julia 1.1. In Julia 1.0 `precision` is the second positional argument (`BigFloat(x, precision)`). @@ -170,11 +174,6 @@ julia> BigFloat("2.1", RoundUp) julia> BigFloat("2.1", RoundUp, precision=128) 2.100000000000000000000000000000000000007 ``` - -# See also -- [`@big_str`](@ref) -- [`rounding`](@ref) and [`setrounding`](@ref) -- [`precision`](@ref) and [`setprecision`](@ref) """ BigFloat(x, r::RoundingMode) @@ -182,7 +181,7 @@ widen(::Type{Float64}) = BigFloat widen(::Type{BigFloat}) = BigFloat function BigFloat(x::BigFloat, r::MPFRRoundingMode=ROUNDING_MODE[]; precision::Integer=DEFAULT_PRECISION[]) - if precision == MPFR.precision(x) + if precision == _precision(x) return x else z = BigFloat(;precision=precision) @@ -193,7 +192,7 @@ function BigFloat(x::BigFloat, r::MPFRRoundingMode=ROUNDING_MODE[]; precision::I end function _duplicate(x::BigFloat) - z = BigFloat(;precision=precision(x)) + z = BigFloat(;precision=_precision(x)) ccall((:mpfr_set, :libmpfr), Int32, (Ref{BigFloat}, Ref{BigFloat}, Int32), z, x, 0) return z end @@ -295,7 +294,14 @@ function round(::Type{T}, x::BigFloat, r::Union{RoundingMode, MPFRRoundingMode}) end return unsafe_trunc(T, res) end -round(::Type{BigInt}, x::BigFloat, r::Union{RoundingMode, MPFRRoundingMode}) = _unchecked_cast(BigInt, x, r) + +function round(::Type{BigInt}, x::BigFloat, r::Union{RoundingMode, MPFRRoundingMode}) + clear_flags() + res = _unchecked_cast(BigInt, x, r) + had_range_exception() && throw(InexactError(:round, BigInt, x)) + return res +end + round(::Type{T}, x::BigFloat, r::RoundingMode) where T<:Union{Signed, Unsigned} = invoke(round, Tuple{Type{<:Union{Signed, Unsigned}}, BigFloat, Union{RoundingMode, MPFRRoundingMode}}, T, x, r) round(::Type{BigInt}, x::BigFloat, r::RoundingMode) = @@ -339,8 +345,23 @@ Float32(x::BigFloat, r::MPFRRoundingMode=ROUNDING_MODE[]) = _cpynansgn(ccall((:mpfr_get_flt,:libmpfr), Float32, (Ref{BigFloat}, MPFRRoundingMode), x, r), x) Float32(x::BigFloat, r::RoundingMode) = Float32(x, convert(MPFRRoundingMode, r)) -# TODO: avoid double rounding -Float16(x::BigFloat) = Float16(Float32(x)) +function Float16(x::BigFloat) :: Float16 + res = Float32(x) + resi = reinterpret(UInt32, res) + if (resi&0x7fffffff) < 0x38800000 # if Float16(res) is subnormal + #shift so that the mantissa lines up where it would for normal Float16 + shift = 113-((resi & 0x7f800000)>>23) + if shift<23 + resi |= 0x0080_0000 # set implicit bit + resi >>= shift + end + end + if (resi & 0x1fff == 0x1000) # if we are halfway between 2 Float16 values + # adjust the value by 1 ULP in the direction that will make Float16(res) give the right answer + res = nextfloat(res, cmp(x, res)) + end + return res +end promote_rule(::Type{BigFloat}, ::Type{<:Real}) = BigFloat promote_rule(::Type{BigInt}, ::Type{<:AbstractFloat}) = BigFloat @@ -793,37 +814,37 @@ function sign(x::BigFloat) return c < 0 ? -one(x) : one(x) end -function precision(x::BigFloat) # precision of an object of type BigFloat +function _precision(x::BigFloat) # precision of an object of type BigFloat return ccall((:mpfr_get_prec, :libmpfr), Clong, (Ref{BigFloat},), x) end +precision(x::BigFloat; base::Integer=2) = _precision(x, base) -""" - precision(BigFloat) - -Get the precision (in bits) currently used for [`BigFloat`](@ref) arithmetic. -""" -precision(::Type{BigFloat}) = Int(DEFAULT_PRECISION[]) # precision of the type BigFloat itself +_precision(::Type{BigFloat}) = Int(DEFAULT_PRECISION[]) # default precision of the type BigFloat itself """ - setprecision([T=BigFloat,] precision::Int) + setprecision([T=BigFloat,] precision::Int; base=2) -Set the precision (in bits) to be used for `T` arithmetic. +Set the precision (in bits, by default) to be used for `T` arithmetic. +If `base` is specified, then the precision is the minimum required to give +at least `precision` digits in the given `base`. !!! warning This function is not thread-safe. It will affect code running on all threads, but its behavior is undefined if called concurrently with computations that use the setting. + +!!! compat "Julia 1.8" + The `base` keyword requires at least Julia 1.8. """ -function setprecision(::Type{BigFloat}, precision::Integer) - if precision < 2 - throw(DomainError(precision, "`precision` cannot be less than 2.")) - end - DEFAULT_PRECISION[] = precision +function setprecision(::Type{BigFloat}, precision::Integer; base::Integer=2) + base > 1 || throw(DomainError(base, "`base` cannot be less than 2.")) + precision > 0 || throw(DomainError(precision, "`precision` cannot be less than 1.")) + DEFAULT_PRECISION[] = base == 2 ? precision : ceil(Int, precision * log2(base)) return precision end -setprecision(precision::Integer) = setprecision(BigFloat, precision) +setprecision(precision::Integer; base::Integer=2) = setprecision(BigFloat, precision; base) maxintfloat(x::BigFloat) = BigFloat(2)^precision(x) maxintfloat(::Type{BigFloat}) = BigFloat(2)^precision(BigFloat) @@ -917,9 +938,9 @@ floatmin(::Type{BigFloat}) = nextfloat(zero(BigFloat)) floatmax(::Type{BigFloat}) = prevfloat(BigFloat(Inf)) """ - setprecision(f::Function, [T=BigFloat,] precision::Integer) + setprecision(f::Function, [T=BigFloat,] precision::Integer; base=2) -Change the `T` arithmetic precision (in bits) for the duration of `f`. +Change the `T` arithmetic precision (in the given `base`) for the duration of `f`. It is logically equivalent to: old = precision(BigFloat) @@ -930,11 +951,14 @@ It is logically equivalent to: Often used as `setprecision(T, precision) do ... end` Note: `nextfloat()`, `prevfloat()` do not use the precision mentioned by -`setprecision` +`setprecision`. + +!!! compat "Julia 1.8" + The `base` keyword requires at least Julia 1.8. """ -function setprecision(f::Function, ::Type{T}, prec::Integer) where T +function setprecision(f::Function, ::Type{T}, prec::Integer; kws...) where T old_prec = precision(T) - setprecision(T, prec) + setprecision(T, prec; kws...) try return f() finally @@ -942,7 +966,7 @@ function setprecision(f::Function, ::Type{T}, prec::Integer) where T end end -setprecision(f::Function, prec::Integer) = setprecision(f, BigFloat, prec) +setprecision(f::Function, prec::Integer; base::Integer=2) = setprecision(f, BigFloat, prec; base) function string_mpfr(x::BigFloat, fmt::String) pc = Ref{Ptr{UInt8}}() @@ -963,7 +987,7 @@ function string_mpfr(x::BigFloat, fmt::String) end function _prettify_bigfloat(s::String)::String - mantissa, exponent = split(s, 'e') + mantissa, exponent = eachsplit(s, 'e') if !occursin('.', mantissa) mantissa = string(mantissa, '.') end @@ -974,7 +998,7 @@ function _prettify_bigfloat(s::String)::String expo = parse(Int, exponent) if -5 < expo < 6 expo == 0 && return mantissa - int, frac = split(mantissa, '.') + int, frac = eachsplit(mantissa, '.') if expo > 0 expo < length(frac) ? string(int, frac[1:expo], '.', frac[expo+1:end]) : @@ -983,7 +1007,7 @@ function _prettify_bigfloat(s::String)::String neg = startswith(int, '-') neg == true && (int = lstrip(int, '-')) @assert length(int) == 1 - string(neg ? '-' : "", '0', '.', '0'^(-expo-1), int, frac) + string(neg ? '-' : "", '0', '.', '0'^(-expo-1), int, frac == "0" ? "" : frac) end else string(mantissa, 'e', exponent) @@ -1022,17 +1046,29 @@ set_emax!(x) = check_exponent_err(ccall((:mpfr_set_emax, :libmpfr), Cint, (Clong set_emin!(x) = check_exponent_err(ccall((:mpfr_set_emin, :libmpfr), Cint, (Clong,), x)) function Base.deepcopy_internal(x::BigFloat, stackdict::IdDict) - haskey(stackdict, x) && return stackdict[x] - # d = copy(x._d) - d = x._d - d′ = GC.@preserve d unsafe_string(pointer(d), sizeof(d)) # creates a definitely-new String - y = _BigFloat(x.prec, x.sign, x.exp, d′) - #ccall((:mpfr_custom_move,:libmpfr), Cvoid, (Ref{BigFloat}, Ptr{Limb}), y, d) # unnecessary - stackdict[x] = y - return y + get!(stackdict, x) do + # d = copy(x._d) + d = x._d + d′ = GC.@preserve d unsafe_string(pointer(d), sizeof(d)) # creates a definitely-new String + y = _BigFloat(x.prec, x.sign, x.exp, d′) + #ccall((:mpfr_custom_move,:libmpfr), Cvoid, (Ref{BigFloat}, Ptr{Limb}), y, d) # unnecessary + return y + end +end + +function decompose(x::BigFloat)::Tuple{BigInt, Int, Int} + isnan(x) && return 0, 0, 0 + isinf(x) && return x.sign, 0, 0 + x == 0 && return 0, 0, x.sign + s = BigInt() + s.size = cld(x.prec, 8*sizeof(Limb)) # limbs + b = s.size * sizeof(Limb) # bytes + ccall((:__gmpz_realloc2, :libgmp), Cvoid, (Ref{BigInt}, Culong), s, 8b) # bits + ccall(:memcpy, Ptr{Cvoid}, (Ptr{Cvoid}, Ptr{Cvoid}, Csize_t), s.d, x.d, b) # bytes + s, x.exp - 8b, x.sign end -function Base.lerpi(j::Integer, d::Integer, a::BigFloat, b::BigFloat) +function lerpi(j::Integer, d::Integer, a::BigFloat, b::BigFloat) t = BigFloat(j)/d fma(t, b, fma(-t, a, a)) end diff --git a/base/multidimensional.jl b/base/multidimensional.jl index f19afd78d481b6..b5e401a7834e7e 100644 --- a/base/multidimensional.jl +++ b/base/multidimensional.jl @@ -2,15 +2,16 @@ ### Multidimensional iterators module IteratorsMD - import .Base: eltype, length, size, first, last, in, getindex, - setindex!, IndexStyle, min, max, zero, oneunit, isless, eachindex, - ndims, IteratorSize, convert, show, iterate, promote_rule, to_indices + import .Base: eltype, length, size, first, last, in, getindex, setindex!, IndexStyle, + min, max, zero, oneunit, isless, eachindex, ndims, IteratorSize, + convert, show, iterate, promote_rule, to_indices, to_index import .Base: +, -, *, (:) import .Base: simd_outer_range, simd_inner_length, simd_index, setindex using .Base: IndexLinear, IndexCartesian, AbstractCartesianIndex, fill_to_length, tail, ReshapedArray, ReshapedArrayLF, OneTo using .Base.Iterators: Reverse, PartitionIterator + using .Base: @propagate_inbounds export CartesianIndex, CartesianIndices @@ -103,9 +104,9 @@ module IteratorsMD # zeros and ones zero(::CartesianIndex{N}) where {N} = zero(CartesianIndex{N}) - zero(::Type{CartesianIndex{N}}) where {N} = CartesianIndex(ntuple(x -> 0, Val(N))) + zero(::Type{CartesianIndex{N}}) where {N} = CartesianIndex(ntuple(Returns(0), Val(N))) oneunit(::CartesianIndex{N}) where {N} = oneunit(CartesianIndex{N}) - oneunit(::Type{CartesianIndex{N}}) where {N} = CartesianIndex(ntuple(x -> 1, Val(N))) + oneunit(::Type{CartesianIndex{N}}) where {N} = CartesianIndex(ntuple(Returns(1), Val(N))) # arithmetic, min/max @inline (-)(index::CartesianIndex{N}) where {N} = @@ -123,13 +124,7 @@ module IteratorsMD @inline (*)(index::CartesianIndex, a::Integer) = *(a,index) # comparison - @inline isless(I1::CartesianIndex{N}, I2::CartesianIndex{N}) where {N} = _isless(0, I1.I, I2.I) - @inline function _isless(ret, I1::NTuple{N,Int}, I2::NTuple{N,Int}) where N - newret = ifelse(ret==0, icmp(I1[N], I2[N]), ret) - _isless(newret, Base.front(I1), Base.front(I2)) - end - _isless(ret, ::Tuple{}, ::Tuple{}) = ifelse(ret==1, true, false) - icmp(a, b) = ifelse(isless(a,b), 1, ifelse(a==b, 0, -1)) + isless(I1::CartesianIndex{N}, I2::CartesianIndex{N}) where {N} = isless(reverse(I1.I), reverse(I2.I)) # conversions convert(::Type{T}, index::CartesianIndex{1}) where {T<:Number} = convert(T, index[1]) @@ -149,13 +144,13 @@ module IteratorsMD function Base.nextind(a::AbstractArray{<:Any,N}, i::CartesianIndex{N}) where {N} iter = CartesianIndices(axes(a)) # might overflow - I = inc(i.I, first(iter).I, last(iter).I) + I = inc(i.I, iter.indices) return I end function Base.prevind(a::AbstractArray{<:Any,N}, i::CartesianIndex{N}) where {N} iter = CartesianIndices(axes(a)) # might underflow - I = dec(i.I, last(iter).I, first(iter).I) + I = dec(i.I, iter.indices) return I end @@ -167,17 +162,18 @@ module IteratorsMD error("iteration is deliberately unsupported for CartesianIndex. Use `I` rather than `I...`, or use `Tuple(I)...`") # Iteration + const OrdinalRangeInt = OrdinalRange{Int, Int} """ CartesianIndices(sz::Dims) -> R - CartesianIndices((istart:istop, jstart:jstop, ...)) -> R + CartesianIndices((istart:[istep:]istop, jstart:[jstep:]jstop, ...)) -> R Define a region `R` spanning a multidimensional rectangular range of integer indices. These are most commonly encountered in the context of iteration, where `for I in R ... end` will return [`CartesianIndex`](@ref) indices `I` equivalent to the nested loops - for j = jstart:jstop - for i = istart:istop + for j = jstart:jstep:jstop + for i = istart:istep:istop ... end end @@ -190,6 +186,10 @@ module IteratorsMD As a convenience, constructing a `CartesianIndices` from an array makes a range of its indices. + !!! compat "Julia 1.6" + The step range method `CartesianIndices((istart:istep:istop, jstart:[jstep:]jstop, ...))` + requires at least Julia 1.6. + # Examples ```jldoctest julia> foreach(println, CartesianIndices((2, 2, 2))) @@ -203,9 +203,7 @@ module IteratorsMD CartesianIndex(2, 2, 2) julia> CartesianIndices(fill(1, (2,3))) - 2×3 CartesianIndices{2, Tuple{Base.OneTo{Int64}, Base.OneTo{Int64}}}: - CartesianIndex(1, 1) CartesianIndex(1, 2) CartesianIndex(1, 3) - CartesianIndex(2, 1) CartesianIndex(2, 2) CartesianIndex(2, 3) + CartesianIndices((2, 3)) ``` ## Conversion between linear and cartesian indices @@ -215,13 +213,16 @@ module IteratorsMD ```jldoctest julia> cartesian = CartesianIndices((1:3, 1:2)) - 3×2 CartesianIndices{2, Tuple{UnitRange{Int64}, UnitRange{Int64}}}: - CartesianIndex(1, 1) CartesianIndex(1, 2) - CartesianIndex(2, 1) CartesianIndex(2, 2) - CartesianIndex(3, 1) CartesianIndex(3, 2) + CartesianIndices((1:3, 1:2)) julia> cartesian[4] CartesianIndex(1, 2) + + julia> cartesian = CartesianIndices((1:2:5, 1:2)) + CartesianIndices((1:2:5, 1:2)) + + julia> cartesian[2, 2] + CartesianIndex(3, 2) ``` ## Broadcasting @@ -233,44 +234,58 @@ module IteratorsMD ```jldoctest julia> CIs = CartesianIndices((2:3, 5:6)) - 2×2 CartesianIndices{2, Tuple{UnitRange{Int64}, UnitRange{Int64}}}: - CartesianIndex(2, 5) CartesianIndex(2, 6) - CartesianIndex(3, 5) CartesianIndex(3, 6) + CartesianIndices((2:3, 5:6)) julia> CI = CartesianIndex(3, 4) CartesianIndex(3, 4) julia> CIs .+ CI - 2×2 CartesianIndices{2, Tuple{UnitRange{Int64}, UnitRange{Int64}}}: - CartesianIndex(5, 9) CartesianIndex(5, 10) - CartesianIndex(6, 9) CartesianIndex(6, 10) + CartesianIndices((5:6, 9:10)) ``` For cartesian to linear index conversion, see [`LinearIndices`](@ref). """ - struct CartesianIndices{N,R<:NTuple{N,AbstractUnitRange{Int}}} <: AbstractArray{CartesianIndex{N},N} + struct CartesianIndices{N,R<:NTuple{N,OrdinalRangeInt}} <: AbstractArray{CartesianIndex{N},N} indices::R end CartesianIndices(::Tuple{}) = CartesianIndices{0,typeof(())}(()) - CartesianIndices(inds::NTuple{N,AbstractUnitRange{<:Integer}}) where {N} = - CartesianIndices(map(r->convert(AbstractUnitRange{Int}, r), inds)) + function CartesianIndices(inds::NTuple{N,OrdinalRange{<:Integer, <:Integer}}) where {N} + indices = map(r->convert(OrdinalRangeInt, r), inds) + CartesianIndices{N, typeof(indices)}(indices) + end CartesianIndices(index::CartesianIndex) = CartesianIndices(index.I) - CartesianIndices(sz::NTuple{N,<:Integer}) where {N} = CartesianIndices(map(Base.OneTo, sz)) - CartesianIndices(inds::NTuple{N,Union{<:Integer,AbstractUnitRange{<:Integer}}}) where {N} = - CartesianIndices(map(i->first(i):last(i), inds)) + CartesianIndices(inds::NTuple{N,Union{<:Integer,OrdinalRange{<:Integer}}}) where {N} = + CartesianIndices(map(_convert2ind, inds)) CartesianIndices(A::AbstractArray) = CartesianIndices(axes(A)) + _convert2ind(sz::Bool) = Base.OneTo(Int8(sz)) + _convert2ind(sz::Integer) = Base.OneTo(sz) + _convert2ind(sz::AbstractUnitRange) = first(sz):last(sz) + _convert2ind(sz::OrdinalRange) = first(sz):step(sz):last(sz) + + function show(io::IO, iter::CartesianIndices) + print(io, "CartesianIndices(") + show(io, map(_xform_index, iter.indices)) + print(io, ")") + end + _xform_index(i) = i + _xform_index(i::OneTo) = i.stop + show(io::IO, ::MIME"text/plain", iter::CartesianIndices) = show(io, iter) + """ - (:)(I::CartesianIndex, J::CartesianIndex) + (:)(start::CartesianIndex, [step::CartesianIndex], stop::CartesianIndex) - Construct [`CartesianIndices`](@ref) from two `CartesianIndex`. + Construct [`CartesianIndices`](@ref) from two `CartesianIndex` and an optional step. !!! compat "Julia 1.1" This method requires at least Julia 1.1. + !!! compat "Julia 1.6" + The step range method start:step:stop requires at least Julia 1.6. + # Examples ```jldoctest julia> I = CartesianIndex(2,1); @@ -278,20 +293,25 @@ module IteratorsMD julia> J = CartesianIndex(3,3); julia> I:J - 2×3 CartesianIndices{2, Tuple{UnitRange{Int64}, UnitRange{Int64}}}: - CartesianIndex(2, 1) CartesianIndex(2, 2) CartesianIndex(2, 3) - CartesianIndex(3, 1) CartesianIndex(3, 2) CartesianIndex(3, 3) + CartesianIndices((2:3, 1:3)) + + julia> I:CartesianIndex(1, 2):J + CartesianIndices((2:1:3, 1:2:3)) ``` """ (:)(I::CartesianIndex{N}, J::CartesianIndex{N}) where N = CartesianIndices(map((i,j) -> i:j, Tuple(I), Tuple(J))) + (:)(I::CartesianIndex{N}, S::CartesianIndex{N}, J::CartesianIndex{N}) where N = + CartesianIndices(map((i,s,j) -> i:s:j, Tuple(I), Tuple(S), Tuple(J))) promote_rule(::Type{CartesianIndices{N,R1}}, ::Type{CartesianIndices{N,R2}}) where {N,R1,R2} = CartesianIndices{N,Base.indices_promote_type(R1,R2)} convert(::Type{Tuple{}}, R::CartesianIndices{0}) = () - convert(::Type{NTuple{N,AbstractUnitRange{Int}}}, R::CartesianIndices{N}) where {N} = - R.indices + for RT in (OrdinalRange{Int, Int}, StepRange{Int, Int}, AbstractUnitRange{Int}) + @eval convert(::Type{NTuple{N,$RT}}, R::CartesianIndices{N}) where {N} = + map(x->convert($RT, x), R.indices) + end convert(::Type{NTuple{N,AbstractUnitRange}}, R::CartesianIndices{N}) where {N} = convert(NTuple{N,AbstractUnitRange{Int}}, R) convert(::Type{NTuple{N,UnitRange{Int}}}, R::CartesianIndices{N}) where {N} = @@ -318,13 +338,43 @@ module IteratorsMD # AbstractArray implementation Base.axes(iter::CartesianIndices{N,R}) where {N,R} = map(Base.axes1, iter.indices) Base.IndexStyle(::Type{CartesianIndices{N,R}}) where {N,R} = IndexCartesian() - @inline function Base.getindex(iter::CartesianIndices{N,<:NTuple{N,Base.OneTo}}, I::Vararg{Int, N}) where {N} - @boundscheck checkbounds(iter, I...) - CartesianIndex(I) + # getindex for a 0D CartesianIndices is necessary for disambiguation + @propagate_inbounds function Base.getindex(iter::CartesianIndices{0,R}) where {R} + CartesianIndex() end @inline function Base.getindex(iter::CartesianIndices{N,R}, I::Vararg{Int, N}) where {N,R} + # Eagerly do boundscheck before calculating each item of the CartesianIndex so that + # we can pass `@inbounds` hint to inside the map and generates more efficient SIMD codes (#42115) + @boundscheck checkbounds(iter, I...) + index = map(iter.indices, I) do r, i + @inbounds getindex(r, i) + end + CartesianIndex(index) + end + + # CartesianIndices act as a multidimensional range, so cartesian indexing of CartesianIndices + # with compatible dimensions may be seen as indexing into the component ranges. + # This may use the special indexing behavior implemented for ranges to return another CartesianIndices + @inline function Base.getindex(iter::CartesianIndices{N,R}, + I::Vararg{Union{OrdinalRange{<:Integer, <:Integer}, Colon}, N}) where {N,R} @boundscheck checkbounds(iter, I...) - CartesianIndex(I .- first.(Base.axes1.(iter.indices)) .+ first.(iter.indices)) + indices = map(iter.indices, I) do r, i + @inbounds getindex(r, i) + end + CartesianIndices(indices) + end + @propagate_inbounds function Base.getindex(iter::CartesianIndices{N}, + C::CartesianIndices{N}) where {N} + getindex(iter, C.indices...) + end + @inline Base.getindex(iter::CartesianIndices{0}, ::CartesianIndices{0}) = iter + + # If dimensions permit, we may index into a CartesianIndices directly instead of constructing a SubArray wrapper + @propagate_inbounds function Base.view(c::CartesianIndices{N}, r::Vararg{Union{OrdinalRange{<:Integer, <:Integer}, Colon},N}) where {N} + getindex(c, r...) + end + @propagate_inbounds function Base.view(c::CartesianIndices{N}, C::CartesianIndices{N}) where {N} + getindex(c, C) end ndims(R::CartesianIndices) = ndims(typeof(R)) @@ -344,62 +394,79 @@ module IteratorsMD IteratorSize(::Type{<:CartesianIndices{N}}) where {N} = Base.HasShape{N}() @inline function iterate(iter::CartesianIndices) - iterfirst, iterlast = first(iter), last(iter) - if any(map(>, iterfirst.I, iterlast.I)) + iterfirst = first(iter) + if !all(map(in, iterfirst.I, iter.indices)) return nothing end iterfirst, iterfirst end @inline function iterate(iter::CartesianIndices, state) - valid, I = __inc(state.I, first(iter).I, last(iter).I) + valid, I = __inc(state.I, iter.indices) valid || return nothing return CartesianIndex(I...), CartesianIndex(I...) end # increment & carry - @inline function inc(state, start, stop) - _, I = __inc(state, start, stop) + @inline function inc(state, indices) + _, I = __inc(state, indices) return CartesianIndex(I...) end - # increment post check to avoid integer overflow - @inline __inc(::Tuple{}, ::Tuple{}, ::Tuple{}) = false, () - @inline function __inc(state::Tuple{Int}, start::Tuple{Int}, stop::Tuple{Int}) - valid = state[1] < stop[1] - return valid, (state[1]+1,) + # Unlike ordinary ranges, CartesianIndices continues the iteration in the next column when the + # current column is consumed. The implementation is written recursively to achieve this. + # `iterate` returns `Union{Nothing, Tuple}`, we explicitly pass a `valid` flag to eliminate + # the type instability inside the core `__inc` logic, and this gives better runtime performance. + __inc(::Tuple{}, ::Tuple{}) = false, () + @inline function __inc(state::Tuple{Int}, indices::Tuple{OrdinalRangeInt}) + rng = indices[1] + I = state[1] + step(rng) + valid = __is_valid_range(I, rng) && state[1] != last(rng) + return valid, (I, ) + end + @inline function __inc(state::Tuple{Int,Int,Vararg{Int}}, indices::Tuple{OrdinalRangeInt,OrdinalRangeInt,Vararg{OrdinalRangeInt}}) + rng = indices[1] + I = state[1] + step(rng) + if __is_valid_range(I, rng) && state[1] != last(rng) + return true, (I, tail(state)...) + end + valid, I = __inc(tail(state), tail(indices)) + return valid, (first(rng), I...) end - @inline function __inc(state, start, stop) - if state[1] < stop[1] - return true, (state[1]+1, tail(state)...) + @inline __is_valid_range(I, rng::AbstractUnitRange) = I in rng + @inline function __is_valid_range(I, rng::OrdinalRange) + if step(rng) > 0 + lo, hi = first(rng), last(rng) + else + lo, hi = last(rng), first(rng) end - valid, I = __inc(tail(state), tail(start), tail(stop)) - return valid, (start[1], I...) + lo <= I <= hi end # 0-d cartesian ranges are special-cased to iterate once and only once iterate(iter::CartesianIndices{0}, done=false) = done ? nothing : (CartesianIndex(), true) - size(iter::CartesianIndices) = map(dimlength, first(iter).I, last(iter).I) - dimlength(start, stop) = stop-start+1 + size(iter::CartesianIndices) = map(length, iter.indices) length(iter::CartesianIndices) = prod(size(iter)) + # make CartesianIndices a multidimensional range + Base.step(iter::CartesianIndices) = CartesianIndex(map(step, iter.indices)) + first(iter::CartesianIndices) = CartesianIndex(map(first, iter.indices)) last(iter::CartesianIndices) = CartesianIndex(map(last, iter.indices)) # When used as indices themselves, CartesianIndices can simply become its tuple of ranges - @inline to_indices(A, inds, I::Tuple{CartesianIndices, Vararg{Any}}) = - to_indices(A, inds, (I[1].indices..., tail(I)...)) + @inline function to_indices(A, inds, I::Tuple{CartesianIndices{N}, Vararg{Any}}) where N + _, indstail = split(inds, Val(N)) + (map(i -> to_index(A, i), I[1].indices)..., to_indices(A, indstail, tail(I))...) + end # but preserve CartesianIndices{0} as they consume a dimension. @inline to_indices(A, inds, I::Tuple{CartesianIndices{0},Vararg{Any}}) = (first(I), to_indices(A, inds, tail(I))...) - @inline function in(i::CartesianIndex{N}, r::CartesianIndices{N}) where {N} - _in(true, i.I, first(r).I, last(r).I) - end - _in(b, ::Tuple{}, ::Tuple{}, ::Tuple{}) = b - @inline _in(b, i, start, stop) = _in(b & (start[1] <= i[1] <= stop[1]), tail(i), tail(start), tail(stop)) + @inline in(i::CartesianIndex, r::CartesianIndices) = false + @inline in(i::CartesianIndex{N}, r::CartesianIndices{N}) where {N} = all(map(in, i.I, r.indices)) simd_outer_range(iter::CartesianIndices{0}) = iter function simd_outer_range(iter::CartesianIndices) @@ -410,13 +477,12 @@ module IteratorsMD simd_inner_length(iter::CartesianIndices, I::CartesianIndex) = Base.length(iter.indices[1]) simd_index(iter::CartesianIndices{0}, ::CartesianIndex, I1::Int) = first(iter) - @inline function simd_index(iter::CartesianIndices, Ilast::CartesianIndex, I1::Int) - CartesianIndex((I1+first(iter.indices[1]), Ilast.I...)) - end + @propagate_inbounds simd_index(iter::CartesianIndices, Ilast::CartesianIndex, I1::Int) = + CartesianIndex(iter.indices[1][I1+firstindex(iter.indices[1])], Ilast) # Split out the first N elements of a tuple @inline function split(t, V::Val) - ref = ntuple(d->true, V) # create a reference tuple of length N + ref = ntuple(Returns(true), V) # create a reference tuple of length N _split1(t, ref), _splitrest(t, ref) end @inline _split1(t, ref) = (t[1], _split1(tail(t), tail(ref))...) @@ -440,51 +506,85 @@ module IteratorsMD # reversed CartesianIndices iteration + Base.reverse(iter::CartesianIndices) = CartesianIndices(reverse.(iter.indices)) + @inline function iterate(r::Reverse{<:CartesianIndices}) - iterfirst, iterlast = last(r.itr), first(r.itr) - if any(map(<, iterfirst.I, iterlast.I)) + iterfirst = last(r.itr) + if !all(map(in, iterfirst.I, r.itr.indices)) return nothing end iterfirst, iterfirst end @inline function iterate(r::Reverse{<:CartesianIndices}, state) - valid, I = __dec(state.I, last(r.itr).I, first(r.itr).I) + valid, I = __dec(state.I, r.itr.indices) valid || return nothing return CartesianIndex(I...), CartesianIndex(I...) end # decrement & carry - @inline function dec(state, start, stop) - _, I = __dec(state, start, stop) + @inline function dec(state, indices) + _, I = __dec(state, indices) return CartesianIndex(I...) end # decrement post check to avoid integer overflow - @inline __dec(::Tuple{}, ::Tuple{}, ::Tuple{}) = false, () - @inline function __dec(state::Tuple{Int}, start::Tuple{Int}, stop::Tuple{Int}) - valid = state[1] > stop[1] - return valid, (state[1]-1,) + @inline __dec(::Tuple{}, ::Tuple{}) = false, () + @inline function __dec(state::Tuple{Int}, indices::Tuple{OrdinalRangeInt}) + rng = indices[1] + I = state[1] - step(rng) + valid = __is_valid_range(I, rng) && state[1] != first(rng) + return valid, (I,) end - - @inline function __dec(state, start, stop) - if state[1] > stop[1] - return true, (state[1]-1, tail(state)...) + @inline function __dec(state::Tuple{Int,Int,Vararg{Int}}, indices::Tuple{OrdinalRangeInt,OrdinalRangeInt,Vararg{OrdinalRangeInt}}) + rng = indices[1] + I = state[1] - step(rng) + if __is_valid_range(I, rng) && state[1] != first(rng) + return true, (I, tail(state)...) end - valid, I = __dec(tail(state), tail(start), tail(stop)) - return valid, (start[1], I...) + valid, I = __dec(tail(state), tail(indices)) + return valid, (last(rng), I...) end # 0-d cartesian ranges are special-cased to iterate once and only once iterate(iter::Reverse{<:CartesianIndices{0}}, state=false) = state ? nothing : (CartesianIndex(), true) - Base.LinearIndices(inds::CartesianIndices{N,R}) where {N,R} = LinearIndices{N,R}(inds.indices) + function Base.LinearIndices(inds::CartesianIndices{N,R}) where {N,R<:NTuple{N, AbstractUnitRange}} + LinearIndices{N,R}(inds.indices) + end + function Base.LinearIndices(inds::CartesianIndices) + indices = inds.indices + if all(x->step(x)==1, indices) + indices = map(rng->first(rng):last(rng), indices) + LinearIndices{length(indices), typeof(indices)}(indices) + else + # Given the fact that StepRange 1:2:4 === 1:2:3, we lost the original size information + # and thus cannot calculate the correct linear indices when the steps are not 1. + throw(ArgumentError("LinearIndices for $(typeof(inds)) with non-1 step size is not yet supported.")) + end + end + + # This is currently needed because converting to LinearIndices is only available when steps are + # all 1 + # NOTE: this is only a temporary patch and could be possibly removed when StepRange support to + # LinearIndices is done + function Base.collect(inds::CartesianIndices{N, R}) where {N,R<:NTuple{N, AbstractUnitRange}} + Base._collect_indices(axes(inds), inds) + end + function Base.collect(inds::CartesianIndices) + dest = Array{eltype(inds), ndims(inds)}(undef, size(inds)) + i = 0 + @inbounds for a in inds + dest[i+=1] = a + end + dest + end # array operations Base.intersect(a::CartesianIndices{N}, b::CartesianIndices{N}) where N = CartesianIndices(intersect.(a.indices, b.indices)) # Views of reshaped CartesianIndices are used for partitions — ensure these are fast - const CartesianPartition{T<:CartesianIndex, P<:CartesianIndices, R<:ReshapedArray{T,1,P}} = SubArray{T,1,R,Tuple{UnitRange{Int}},false} + const CartesianPartition{T<:CartesianIndex, P<:CartesianIndices, R<:ReshapedArray{T,1,P}} = SubArray{T,1,R,<:Tuple{AbstractUnitRange{Int}},false} eltype(::Type{PartitionIterator{T}}) where {T<:ReshapedArrayLF} = SubArray{eltype(T), 1, T, Tuple{UnitRange{Int}}, true} eltype(::Type{PartitionIterator{T}}) where {T<:ReshapedArray} = SubArray{eltype(T), 1, T, Tuple{UnitRange{Int}}, false} Iterators.IteratorEltype(::Type{<:PartitionIterator{T}}) where {T<:ReshapedArray} = Iterators.IteratorEltype(T) @@ -493,7 +593,6 @@ module IteratorsMD eltype(::Type{PartitionIterator{T}}) where {T<:Union{UnitRange, StepRange, StepRangeLen, LinRange}} = T Iterators.IteratorEltype(::Type{<:PartitionIterator{T}}) where {T<:Union{OneTo, UnitRange, StepRange, StepRangeLen, LinRange}} = Iterators.IteratorEltype(T) - @inline function iterate(iter::CartesianPartition) isempty(iter) && return nothing f = first(iter) @@ -501,7 +600,7 @@ module IteratorsMD end @inline function iterate(iter::CartesianPartition, (state, n)) n >= length(iter) && return nothing - I = IteratorsMD.inc(state.I, first(iter.parent.parent).I, last(iter.parent.parent).I) + I = IteratorsMD.inc(state.I, iter.parent.parent.indices) return I, (I, n+1) end @@ -509,33 +608,45 @@ module IteratorsMD # In general, the Cartesian Partition might start and stop in the middle of the outer # dimensions — thus the outer range of a CartesianPartition is itself a # CartesianPartition. - t = tail(iter.parent.parent.indices) - ci = CartesianIndices(t) - li = LinearIndices(t) - return @inbounds view(ci, li[tail(iter[1].I)...]:li[tail(iter[end].I)...]) + mi = iter.parent.mi + ci = iter.parent.parent + ax, ax1 = axes(ci), Base.axes1(ci) + subs = Base.ind2sub_rs(ax, mi, first(iter.indices[1])) + vl, fl = Base._sub2ind(tail(ax), tail(subs)...), subs[1] + vr, fr = divrem(last(iter.indices[1]) - 1, mi[end]) .+ (1, first(ax1)) + oci = CartesianIndices(tail(ci.indices)) + # A fake CartesianPartition to reuse the outer iterate fallback + outer = @inbounds view(ReshapedArray(oci, (length(oci),), mi), vl:vr) + init = @inbounds dec(oci[tail(subs)...].I, oci.indices) # real init state + # Use Generator to make inner loop branchless + @inline function skip_len_I(i::Int, I::CartesianIndex) + l = i == 1 ? fl : first(ax1) + r = i == length(outer) ? fr : last(ax1) + l - first(ax1), r - l + 1, I + end + (skip_len_I(i, I) for (i, I) in Iterators.enumerate(Iterators.rest(outer, (init, 0)))) end - function simd_outer_range(iter::CartesianPartition{CartesianIndex{2}}) + @inline function simd_outer_range(iter::CartesianPartition{CartesianIndex{2}}) # But for two-dimensional Partitions the above is just a simple one-dimensional range # over the second dimension; we don't need to worry about non-rectangular staggers in # higher dimensions. - return @inbounds CartesianIndices((iter[1][2]:iter[end][2],)) - end - @inline function simd_inner_length(iter::CartesianPartition, I::CartesianIndex) - inner = iter.parent.parent.indices[1] - @inbounds fi = iter[1].I - @inbounds li = iter[end].I - inner_start = I.I == tail(fi) ? fi[1] : first(inner) - inner_end = I.I == tail(li) ? li[1] : last(inner) - return inner_end - inner_start + 1 - end - @inline function simd_index(iter::CartesianPartition, Ilast::CartesianIndex, I1::Int) - # I1 is the 0-based distance from the first dimension's offest - offset = first(iter.parent.parent.indices[1]) # (this is 1 for 1-based arrays) - # In the first column we need to also add in the iter's starting point (branchlessly) - f = @inbounds iter[1] - startoffset = (Ilast.I == tail(f.I))*(f[1] - 1) - CartesianIndex((I1 + offset + startoffset, Ilast.I...)) + mi = iter.parent.mi + ci = iter.parent.parent + ax, ax1 = axes(ci), Base.axes1(ci) + fl, vl = Base.ind2sub_rs(ax, mi, first(iter.indices[1])) + fr, vr = Base.ind2sub_rs(ax, mi, last(iter.indices[1])) + outer = @inbounds CartesianIndices((ci.indices[2][vl:vr],)) + # Use Generator to make inner loop branchless + @inline function skip_len_I(I::CartesianIndex{1}) + l = I == first(outer) ? fl : first(ax1) + r = I == last(outer) ? fr : last(ax1) + l - first(ax1), r - l + 1, I + end + (skip_len_I(I) for I in outer) end + @inline simd_inner_length(iter::CartesianPartition, (_, len, _)::Tuple{Int,Int,CartesianIndex}) = len + @propagate_inbounds simd_index(iter::CartesianPartition, (skip, _, I)::Tuple{Int,Int,CartesianIndex}, n::Int) = + simd_index(iter.parent.parent, I, n + skip) end # IteratorsMD @@ -544,7 +655,7 @@ using .IteratorsMD ## Bounds-checking with CartesianIndex # Disallow linear indexing with CartesianIndex function checkbounds(::Type{Bool}, A::AbstractArray, i::Union{CartesianIndex, AbstractArray{<:CartesianIndex}}) - @_inline_meta + @inline checkbounds_indices(Bool, axes(A), (i,)) end @@ -584,6 +695,16 @@ end checkindex(Bool, IA1, I[1]) & checkbounds_indices(Bool, IArest, tail(I)) end + +@inline function checkbounds_indices(::Type{Bool}, IA::Tuple{}, + I::Tuple{AbstractArray{Bool,N},Vararg{Any}}) where N + return checkbounds_indices(Bool, IA, (LogicalIndex(I[1]), tail(I)...)) +end +@inline function checkbounds_indices(::Type{Bool}, IA::Tuple, + I::Tuple{AbstractArray{Bool,N},Vararg{Any}}) where N + return checkbounds_indices(Bool, IA, (LogicalIndex(I[1]), tail(I)...)) +end + function checkindex(::Type{Bool}, inds::Tuple, I::AbstractArray{<:CartesianIndex}) b = true for i in I @@ -598,10 +719,10 @@ checkindex(::Type{Bool}, inds::Tuple, I::CartesianIndices) = all(checkindex.(Boo # rather than returning N, it returns an NTuple{N,Bool} so the result is inferrable @inline index_ndims(i1, I...) = (true, index_ndims(I...)...) @inline function index_ndims(i1::CartesianIndex, I...) - (map(x->true, i1.I)..., index_ndims(I...)...) + (map(Returns(true), i1.I)..., index_ndims(I...)...) end @inline function index_ndims(i1::AbstractArray{CartesianIndex{N}}, I...) where N - (ntuple(x->true, Val(N))..., index_ndims(I...)...) + (ntuple(Returns(true), Val(N))..., index_ndims(I...)...) end index_ndims() = () @@ -611,7 +732,7 @@ index_ndims() = () @inline index_dimsum(::Colon, I...) = (true, index_dimsum(I...)...) @inline index_dimsum(::AbstractArray{Bool}, I...) = (true, index_dimsum(I...)...) @inline function index_dimsum(::AbstractArray{<:Any,N}, I...) where N - (ntuple(x->true, Val(N))..., index_dimsum(I...)...) + (ntuple(Returns(true), Val(N))..., index_dimsum(I...)...) end index_dimsum() = () @@ -675,29 +796,35 @@ end end end # When wrapping a BitArray, lean heavily upon its internals. -@inline function iterate(L::Base.LogicalIndex{Int,<:BitArray}) +@inline function iterate(L::LogicalIndex{Int,<:BitArray}) + L.sum == 0 && return nothing + Bc = L.mask.chunks + return iterate(L, (1, 1, (), @inbounds Bc[1])) +end +@inline function iterate(L::LogicalIndex{<:CartesianIndex,<:BitArray}) L.sum == 0 && return nothing Bc = L.mask.chunks - return iterate(L, (1, @inbounds Bc[1])) + irest = ntuple(one, ndims(L.mask)-1) + return iterate(L, (1, 1, irest, @inbounds Bc[1])) end -@inline function iterate(L::Base.LogicalIndex{Int,<:BitArray}, s) +@inline function iterate(L::LogicalIndex{<:Any,<:BitArray}, (i1, Bi, irest, c)) Bc = L.mask.chunks - i1, c = s - while c==0 - i1 % UInt >= length(Bc) % UInt && return nothing - i1 += 1 - @inbounds c = Bc[i1] + while c == 0 + Bi >= length(Bc) && return nothing + i1 += 64 + @inbounds c = Bc[Bi+=1] end - tz = trailing_zeros(c) + 1 + tz = trailing_zeros(c) c = _blsr(c) - return ((i1-1)<<6 + tz, (i1, c)) + i1, irest = _overflowind(i1 + tz, irest, size(L.mask)) + return eltype(L)(i1, irest...), (i1 - tz, Bi, irest, c) end @inline checkbounds(::Type{Bool}, A::AbstractArray, I::LogicalIndex{<:Any,<:AbstractArray{Bool,1}}) = eachindex(IndexLinear(), A) == eachindex(IndexLinear(), I.mask) @inline checkbounds(::Type{Bool}, A::AbstractArray, I::LogicalIndex) = axes(A) == axes(I.mask) @inline checkindex(::Type{Bool}, indx::AbstractUnitRange, I::LogicalIndex) = (indx,) == axes(I.mask) -checkindex(::Type{Bool}, inds::Tuple, I::LogicalIndex) = false +checkindex(::Type{Bool}, inds::Tuple, I::LogicalIndex) = checkbounds_indices(Bool, inds, axes(I.mask)) ensure_indexable(I::Tuple{}) = () @inline ensure_indexable(I::Tuple{Any, Vararg{Any}}) = (I[1], ensure_indexable(tail(I))...) @@ -707,10 +834,12 @@ ensure_indexable(I::Tuple{}) = () # until Julia gets smart enough to elide the call on its own: @inline to_indices(A, I::Tuple{Vararg{Union{Integer, CartesianIndex}}}) = to_indices(A, (), I) # But some index types require more context spanning multiple indices -# CartesianIndexes are simple; they just splat out -@inline to_indices(A, inds, I::Tuple{CartesianIndex, Vararg{Any}}) = - to_indices(A, inds, (I[1].I..., tail(I)...)) -# But for arrays of CartesianIndex, we just skip the appropriate number of inds +# CartesianIndex is unfolded outside the inner to_indices for better inference +@inline function to_indices(A, inds, I::Tuple{CartesianIndex{N}, Vararg{Any}}) where N + _, indstail = IteratorsMD.split(inds, Val(N)) + (map(i -> to_index(A, i), I[1].I)..., to_indices(A, indstail, tail(I))...) +end +# For arrays of CartesianIndex, we just skip the appropriate number of inds @inline function to_indices(A, inds, I::Tuple{AbstractArray{CartesianIndex{N}}, Vararg{Any}}) where N _, indstail = IteratorsMD.split(inds, Val(N)) (to_index(A, I[1]), to_indices(A, indstail, tail(I))...) @@ -735,7 +864,7 @@ uncolon(inds::Tuple{}, I::Tuple{Colon, Vararg{Any}}) = Slice(OneTo(1)) uncolon(inds::Tuple, I::Tuple{Colon, Vararg{Any}}) = Slice(inds[1]) ### From abstractarray.jl: Internal multidimensional indexing definitions ### -getindex(x::Number, i::CartesianIndex{0}) = x +getindex(x::Union{Number,AbstractChar}, ::CartesianIndex{0}) = x getindex(t::Tuple, i::CartesianIndex{1}) = getindex(t, i.I[1]) # These are not defined on directly on getindex to avoid @@ -756,14 +885,14 @@ function _unsafe_getindex(::IndexStyle, A::AbstractArray, I::Vararg{Union{Real, # This is specifically not inlined to prevent excessive allocations in type unstable code shape = index_shape(I...) dest = similar(A, shape) - map(unsafe_length, axes(dest)) == map(unsafe_length, shape) || throw_checksize_error(dest, shape) + map(length, axes(dest)) == map(length, shape) || throw_checksize_error(dest, shape) _unsafe_getindex!(dest, A, I...) # usually a generated function, don't allow it to impact inference result return dest end function _generate_unsafe_getindex!_body(N::Int) quote - @_inline_meta + @inline D = eachindex(dest) Dy = iterate(D) @inbounds @nloops $N j d->I[d] begin @@ -796,7 +925,7 @@ end ## setindex! ## function _setindex!(l::IndexStyle, A::AbstractArray, x, I::Union{Real, AbstractArray}...) - @_inline_meta + @inline @boundscheck checkbounds(A, I...) _unsafe_setindex!(l, _maybe_reshape(l, A, I...), x, I...) A @@ -877,7 +1006,7 @@ function diff(a::AbstractArray{T,N}; dims::Integer) where {T,N} end function diff(r::AbstractRange{T}; dims::Integer=1) where {T} dims == 1 || throw(ArgumentError("dimension $dims out of range (1:1)")) - return T[@inbounds r[i+1] - r[i] for i in firstindex(r):lastindex(r)-1] + return [@inbounds r[i+1] - r[i] for i in firstindex(r):lastindex(r)-1] end ### from abstractarray.jl @@ -1004,6 +1133,25 @@ end Copy the block of `src` in the range of `Rsrc` to the block of `dest` in the range of `Rdest`. The sizes of the two regions must match. + +# Examples +```jldoctest +julia> A = zeros(5, 5); + +julia> B = [1 2; 3 4]; + +julia> Ainds = CartesianIndices((2:3, 2:3)); + +julia> Binds = CartesianIndices(B); + +julia> copyto!(A, Ainds, B, Binds) +5×5 Matrix{Float64}: + 0.0 0.0 0.0 0.0 0.0 + 0.0 1.0 2.0 0.0 0.0 + 0.0 3.0 4.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 +``` """ copyto!(::AbstractArray, ::CartesianIndices, ::AbstractArray, ::CartesianIndices) @@ -1024,6 +1172,7 @@ See also [`circshift`](@ref). dest === src && throw(ArgumentError("dest and src must be separate arrays")) inds = axes(src) axes(dest) == inds || throw(ArgumentError("indices of src and dest must match (got $inds and $(axes(dest)))")) + isempty(src) && return dest _circshift!(dest, (), src, (), inds, fill_to_length(shiftamt, 0, Val(N))) end @@ -1073,6 +1222,8 @@ their indices; any offset results in a (circular) wraparound. If the arrays have overlapping indices, then on the domain of the overlap `dest` agrees with `src`. +See also: [`circshift`](@ref). + # Examples ```julia-repl julia> src = reshape(Vector(1:16), (4,4)) @@ -1132,14 +1283,14 @@ end # contiguous multidimensional indexing: if the first dimension is a range, # we can get some performance from using copy_chunks! -@inline function _unsafe_getindex!(X::BitArray, B::BitArray, I0::Union{UnitRange{Int},Slice}) +@inline function _unsafe_getindex!(X::BitArray, B::BitArray, I0::Union{AbstractUnitRange{Int},Slice}) copy_chunks!(X.chunks, 1, B.chunks, indexoffset(I0)+1, length(I0)) return X end # Optimization where the inner dimension is contiguous improves perf dramatically @generated function _unsafe_getindex!(X::BitArray, B::BitArray, - I0::Union{Slice,UnitRange{Int}}, I::Union{Int,UnitRange{Int},Slice}...) + I0::Union{Slice,UnitRange{Int}}, I::Union{Int,AbstractUnitRange{Int},Slice}...) N = length(I) quote $(Expr(:meta, :inline)) @@ -1274,7 +1425,7 @@ end # contiguous multidimensional indexing: if the first dimension is a range, # we can get some performance from using copy_chunks! -@inline function setindex!(B::BitArray, X::Union{StridedArray,BitArray}, J0::Union{Colon,UnitRange{Int}}) +@inline function setindex!(B::BitArray, X::Union{StridedArray,BitArray}, J0::Union{Colon,AbstractUnitRange{Int}}) I0 = to_indices(B, (J0,))[1] @boundscheck checkbounds(B, I0) l0 = length(I0) @@ -1286,13 +1437,13 @@ end end @inline function setindex!(B::BitArray, X::Union{StridedArray,BitArray}, - I0::Union{Colon,UnitRange{Int}}, I::Union{Int,UnitRange{Int},Colon}...) + I0::Union{Colon,AbstractUnitRange{Int}}, I::Union{Int,AbstractUnitRange{Int},Colon}...) J = to_indices(B, (I0, I...)) @boundscheck checkbounds(B, J...) _unsafe_setindex!(B, X, J...) end @generated function _unsafe_setindex!(B::BitArray, X::Union{StridedArray,BitArray}, - I0::Union{Slice,UnitRange{Int}}, I::Union{Int,UnitRange{Int},Slice}...) + I0::Union{Slice,AbstractUnitRange{Int}}, I::Union{Int,AbstractUnitRange{Int},Slice}...) N = length(I) quote idxlens = @ncall $N index_lengths I0 d->I[d] @@ -1327,12 +1478,12 @@ end end @propagate_inbounds function setindex!(B::BitArray, X::AbstractArray, - I0::Union{Colon,UnitRange{Int}}, I::Union{Int,UnitRange{Int},Colon}...) + I0::Union{Colon,AbstractUnitRange{Int}}, I::Union{Int,AbstractUnitRange{Int},Colon}...) _setindex!(IndexStyle(B), B, X, to_indices(B, (I0, I...))...) end ## fill! contiguous views of BitArrays with a single value -function fill!(V::SubArray{Bool, <:Any, <:BitArray, Tuple{AbstractUnitRange{Int}}}, x) +function fill!(V::SubArray{Bool, <:Any, <:BitArray, <:Tuple{AbstractUnitRange{Int}}}, x) B = V.parent I0 = V.indices[1] l0 = length(I0) @@ -1341,7 +1492,7 @@ function fill!(V::SubArray{Bool, <:Any, <:BitArray, Tuple{AbstractUnitRange{Int} return V end -fill!(V::SubArray{Bool, <:Any, <:BitArray, Tuple{AbstractUnitRange{Int}, Vararg{Union{Int,AbstractUnitRange{Int}}}}}, x) = +fill!(V::SubArray{Bool, <:Any, <:BitArray, <:Tuple{AbstractUnitRange{Int}, Vararg{Union{Int,AbstractUnitRange{Int}}}}}, x) = _unsafe_fill_indices!(V.parent, x, V.indices...) @generated function _unsafe_fill_indices!(B::BitArray, x, @@ -1432,13 +1583,12 @@ for (V, PT, BT) in Any[((:N,), BitArray, BitArray), ((:T,:N), Array, StridedArra #Creates offset, because indexing starts at 1 offset = 1 - sum(@ntuple $N d->strides_{d+1}) + sumc = 0 ind = 1 - @nexprs 1 d->(counts_{$N+1} = strides_{$N+1}) # a trick to set counts_($N+1) @nloops($N, i, P, - d->(counts_d = strides_d), # PRE - d->(counts_{d+1} += strides_{d+1}), # POST + d->(sumc += i_d*strides_{d+1}), # PRE + d->(sumc -= i_d*strides_{d+1}), # POST begin # BODY - sumc = sum(@ntuple $N d->counts_{d+1}) @inbounds P[ind] = B[sumc+offset] ind += 1 end) @@ -1528,7 +1678,7 @@ _unique_dims(A::AbstractArray, dims::Colon) = invoke(unique, Tuple{Any}, A) else j_d = i_d end) begin - if (@nref $N A j) != (@nref $N A i) + if !isequal((@nref $N A j), (@nref $N A i)) collided[k] = true end end @@ -1558,7 +1708,7 @@ _unique_dims(A::AbstractArray, dims::Colon) = invoke(unique, Tuple{Any}, A) j_d = i_d end end begin - if (@nref $N A j) != (@nref $N A i) + if !isequal((@nref $N A j), (@nref $N A i)) nowcollided[k] = true end end @@ -1570,80 +1720,6 @@ _unique_dims(A::AbstractArray, dims::Colon) = invoke(unique, Tuple{Any}, A) end end -""" - extrema(A::AbstractArray; dims) -> Array{Tuple} - -Compute the minimum and maximum elements of an array over the given dimensions. - -# Examples -```jldoctest -julia> A = reshape(Vector(1:2:16), (2,2,2)) -2×2×2 Array{Int64, 3}: -[:, :, 1] = - 1 5 - 3 7 - -[:, :, 2] = - 9 13 - 11 15 - -julia> extrema(A, dims = (1,2)) -1×1×2 Array{Tuple{Int64, Int64}, 3}: -[:, :, 1] = - (1, 7) - -[:, :, 2] = - (9, 15) -``` -""" -extrema(A::AbstractArray; dims = :) = _extrema_dims(identity, A, dims) - -""" - extrema(f, A::AbstractArray; dims) -> Array{Tuple} - -Compute the minimum and maximum of `f` applied to each element in the given dimensions -of `A`. - -!!! compat "Julia 1.2" - This method requires Julia 1.2 or later. -""" -extrema(f, A::AbstractArray; dims=:) = _extrema_dims(f, A, dims) - -_extrema_dims(f, A::AbstractArray, ::Colon) = _extrema_itr(f, A) - -function _extrema_dims(f, A::AbstractArray, dims) - sz = [size(A)...] - for d in dims - sz[d] = 1 - end - T = promote_op(f, eltype(A)) - B = Array{Tuple{T,T}}(undef, sz...) - return extrema!(f, B, A) -end - -@noinline function extrema!(f, B, A) - require_one_based_indexing(B, A) - sA = size(A) - sB = size(B) - for I in CartesianIndices(sB) - fAI = f(A[I]) - B[I] = (fAI, fAI) - end - Bmax = CartesianIndex(sB) - @inbounds @simd for I in CartesianIndices(sA) - J = min(Bmax,I) - BJ = B[J] - fAI = f(A[I]) - if fAI < BJ[1] - B[J] = (fAI, BJ[2]) - elseif fAI > BJ[2] - B[J] = (BJ[1], fAI) - end - end - return B -end -extrema!(B, A) = extrema!(identity, B, A) - # Show for pairs() with Cartesian indices. Needs to be here rather than show.jl for bootstrap order function Base.showarg(io::IO, r::Iterators.Pairs{<:Integer, <:Any, <:Any, T}, toplevel) where T <: Union{AbstractVector, Tuple} print(io, "pairs(::$T)") diff --git a/base/multimedia.jl b/base/multimedia.jl index 45e6b9532e9fae..d15768affd012b 100644 --- a/base/multimedia.jl +++ b/base/multimedia.jl @@ -69,7 +69,7 @@ methods; for example, if the available MIME formats depend on the *value* of `x` julia> showable(MIME("text/plain"), rand(5)) true -julia> showable("img/png", rand(5)) +julia> showable("image/png", rand(5)) false ``` """ @@ -176,7 +176,7 @@ data except for a set of types known to be text data (possibly Unicode). julia> istextmime(MIME("text/plain")) true -julia> istextmime(MIME("img/png")) +julia> istextmime(MIME("image/png")) false ``` """ @@ -239,14 +239,14 @@ objects are printed in the Julia REPL.) struct TextDisplay <: AbstractDisplay io::IO end -display(d::TextDisplay, M::MIME"text/plain", @nospecialize x) = show(d.io, M, x) +display(d::TextDisplay, M::MIME"text/plain", @nospecialize x) = (show(d.io, M, x); println(d.io)) display(d::TextDisplay, @nospecialize x) = display(d, MIME"text/plain"(), x) # if you explicitly call display("text/foo", x), it should work on a TextDisplay: displayable(d::TextDisplay, M::MIME) = istextmime(M) function display(d::TextDisplay, M::MIME, @nospecialize x) displayable(d, M) || throw(MethodError(display, (d, M, x))) - show(d.io, M, x) + show(d.io, M, x); println(d.io) end import Base: close, flush @@ -300,7 +300,7 @@ xdisplayable(D::AbstractDisplay, @nospecialize args...) = applicable(display, D, display(mime, x) display(d::AbstractDisplay, mime, x) -AbstractDisplay `x` using the topmost applicable display in the display stack, typically using the +Display `x` using the topmost applicable display in the display stack, typically using the richest supported multimedia output for `x`, with plain-text [`stdout`](@ref) output as a fallback. The `display(d, x)` variant attempts to display `x` on the given display `d` only, throwing a [`MethodError`](@ref) if `d` cannot display objects of this type. diff --git a/base/namedtuple.jl b/base/namedtuple.jl index e8829086207702..b2ebb3f9d0d7e3 100644 --- a/base/namedtuple.jl +++ b/base/namedtuple.jl @@ -9,9 +9,9 @@ tuple-like collection of values, where each entry has a unique name, represented can be modified in place after construction. Accessing the value associated with a name in a named tuple can be done using field -access syntax, e.g. `x.a`, or using [`getindex`](@ref), e.g. `x[:a]`. A tuple of the -names can be obtained using [`keys`](@ref), and a tuple of the values can be obtained -using [`values`](@ref). +access syntax, e.g. `x.a`, or using [`getindex`](@ref), e.g. `x[:a]` or `x[(:a, :b)]`. +A tuple of the names can be obtained using [`keys`](@ref), and a tuple of the values +can be obtained using [`values`](@ref). !!! note Iteration over `NamedTuple`s produces the *values* without the names. (See example @@ -30,6 +30,9 @@ julia> x.a julia> x[:a] 1 +julia> x[(:a,)] +(a = 1,) + julia> keys(x) (:a, :b) @@ -76,6 +79,9 @@ julia> (; t.x) !!! compat "Julia 1.5" Implicit names from identifiers and dot expressions are available as of Julia 1.5. + +!!! compat "Julia 1.7" + Use of `getindex` methods with multiple `Symbol`s is available as of Julia 1.7. """ Core.NamedTuple @@ -90,6 +96,15 @@ if nameof(@__MODULE__) === :Base $(Expr(:splatnew, :(NamedTuple{names,T}), :(T(args)))) end +function NamedTuple{names, T}(nt::NamedTuple) where {names, T <: Tuple} + if @generated + Expr(:new, :(NamedTuple{names, T}), + Any[ :(convert(fieldtype(T, $n), getfield(nt, $(QuoteNode(names[n]))))) for n in 1:length(names) ]...) + else + NamedTuple{names, T}(map(Fix1(getfield, nt), names)) + end +end + function NamedTuple{names}(nt::NamedTuple) where {names} if @generated idx = Int[ fieldindex(nt, names[n]) for n in 1:length(names) ] @@ -97,7 +112,7 @@ function NamedTuple{names}(nt::NamedTuple) where {names} Expr(:new, :(NamedTuple{names, $types}), Any[ :(getfield(nt, $(idx[n]))) for n in 1:length(idx) ]...) else types = Tuple{(fieldtype(typeof(nt), names[n]) for n in 1:length(names))...} - NamedTuple{names, types}(Tuple(getfield(nt, n) for n in 1:length(names))) + NamedTuple{names, types}(map(Fix1(getfield, nt), names)) end end @@ -106,19 +121,29 @@ NamedTuple{names}(itr) where {names} = NamedTuple{names}(Tuple(itr)) NamedTuple(itr) = (; itr...) +# avoids invalidating Union{}(...) +NamedTuple{names, Union{}}(itr::Tuple) where {names} = throw(MethodError(NamedTuple{names, Union{}}, (itr,))) + end # if Base length(t::NamedTuple) = nfields(t) iterate(t::NamedTuple, iter=1) = iter > nfields(t) ? nothing : (getfield(t, iter), iter + 1) +rest(t::NamedTuple) = t +@inline rest(t::NamedTuple{names}, i::Int) where {names} = NamedTuple{rest(names,i)}(t) firstindex(t::NamedTuple) = 1 lastindex(t::NamedTuple) = nfields(t) getindex(t::NamedTuple, i::Int) = getfield(t, i) getindex(t::NamedTuple, i::Symbol) = getfield(t, i) +@inline getindex(t::NamedTuple, idxs::Tuple{Vararg{Symbol}}) = NamedTuple{idxs}(t) +@inline getindex(t::NamedTuple, idxs::AbstractVector{Symbol}) = NamedTuple{Tuple(idxs)}(t) indexed_iterate(t::NamedTuple, i::Int, state=1) = (getfield(t, i), i+1) isempty(::NamedTuple{()}) = true isempty(::NamedTuple) = false empty(::NamedTuple) = NamedTuple() +prevind(@nospecialize(t::NamedTuple), i::Integer) = Int(i)-1 +nextind(@nospecialize(t::NamedTuple), i::Integer) = Int(i)+1 + convert(::Type{NamedTuple{names,T}}, nt::NamedTuple{names,T}) where {names,T<:Tuple} = nt convert(::Type{NamedTuple{names}}, nt::NamedTuple{names}) where {names} = nt @@ -149,7 +174,8 @@ function show(io::IO, t::NamedTuple) typeinfo = get(io, :typeinfo, Any) print(io, "(") for i = 1:n - print(io, fieldname(typeof(t),i), " = ") + show_sym(io, fieldname(typeof(t), i)) + print(io, " = ") show(IOContext(io, :typeinfo => t isa typeinfo <: NamedTuple ? fieldtype(typeinfo, i) : Any), getfield(t, i)) @@ -178,8 +204,8 @@ _nt_names(::Type{T}) where {names,T<:NamedTuple{names}} = names hash(x::NamedTuple, h::UInt) = xor(objectid(_nt_names(x)), hash(Tuple(x), h)) +(<)(a::NamedTuple{n}, b::NamedTuple{n}) where {n} = Tuple(a) < Tuple(b) isless(a::NamedTuple{n}, b::NamedTuple{n}) where {n} = isless(Tuple(a), Tuple(b)) -# TODO: case where one argument's names are a prefix of the other's same_names(::NamedTuple{names}...) where {names} = true same_names(::NamedTuple...) = false @@ -192,7 +218,7 @@ function map(f, nt::NamedTuple{names}, nts::NamedTuple...) where names NamedTuple{names}(map(f, map(Tuple, (nt, nts...))...)) end -@pure function merge_names(an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}}) +@assume_effects :total function merge_names(an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}}) @nospecialize an bn names = Symbol[an...] for n in bn @@ -203,7 +229,7 @@ end (names...,) end -@pure function merge_types(names::Tuple{Vararg{Symbol}}, a::Type{<:NamedTuple}, b::Type{<:NamedTuple}) +@assume_effects :total function merge_types(names::Tuple{Vararg{Symbol}}, a::Type{<:NamedTuple}, b::Type{<:NamedTuple}) @nospecialize names a b bn = _nt_names(b) return Tuple{Any[ fieldtype(sym_in(names[n], bn) ? b : a, names[n]) for n in 1:length(names) ]...} @@ -251,7 +277,7 @@ merge(a::NamedTuple, b::NamedTuple{()}) = a merge(a::NamedTuple{()}, b::NamedTuple{()}) = a merge(a::NamedTuple{()}, b::NamedTuple) = b -merge(a::NamedTuple, b::Iterators.Pairs{<:Any,<:Any,<:Any,<:NamedTuple}) = merge(a, b.data) +merge(a::NamedTuple, b::Iterators.Pairs{<:Any,<:Any,<:Any,<:NamedTuple}) = merge(a, getfield(b, :data)) merge(a::NamedTuple, b::Iterators.Zip{<:Tuple{Any,Any}}) = merge(a, NamedTuple{Tuple(b.is[1])}(b.is[2])) @@ -273,7 +299,8 @@ function merge(a::NamedTuple, itr) names = Symbol[] vals = Any[] inds = IdDict{Symbol,Int}() - for (k::Symbol, v) in itr + for (k, v) in itr + k = k::Symbol oldind = get(inds, k, 0) if oldind > 0 vals[oldind] = v @@ -289,12 +316,12 @@ end keys(nt::NamedTuple{names}) where {names} = names values(nt::NamedTuple) = Tuple(nt) haskey(nt::NamedTuple, key::Union{Integer, Symbol}) = isdefined(nt, key) -get(nt::NamedTuple, key::Union{Integer, Symbol}, default) = haskey(nt, key) ? getfield(nt, key) : default -get(f::Callable, nt::NamedTuple, key::Union{Integer, Symbol}) = haskey(nt, key) ? getfield(nt, key) : f() +get(nt::NamedTuple, key::Union{Integer, Symbol}, default) = isdefined(nt, key) ? getfield(nt, key) : default +get(f::Callable, nt::NamedTuple, key::Union{Integer, Symbol}) = isdefined(nt, key) ? getfield(nt, key) : f() tail(t::NamedTuple{names}) where names = NamedTuple{tail(names)}(t) front(t::NamedTuple{names}) where names = NamedTuple{front(names)}(t) -@pure function diff_names(an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}}) +@assume_effects :total function diff_names(an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}}) @nospecialize an bn names = Symbol[] for n in an @@ -321,7 +348,7 @@ function structdiff(a::NamedTuple{an}, b::Union{NamedTuple{bn}, Type{NamedTuple{ else names = diff_names(an, bn) types = Tuple{Any[ fieldtype(typeof(a), names[n]) for n in 1:length(names) ]...} - NamedTuple{names,types}(map(n->getfield(a, n), names)) + NamedTuple{names,types}(map(Fix1(getfield, a), names)) end end @@ -388,3 +415,9 @@ macro NamedTuple(ex) types = [esc(e isa Symbol ? :Any : e.args[2]) for e in decls] return :(NamedTuple{($(vars...),), Tuple{$(types...)}}) end + +function split_rest(t::NamedTuple{names}, n::Int, st...) where {names} + _check_length_split_rest(length(t), n) + names_front, names_last_n = split_rest(names, n, st...) + return NamedTuple{names_front}(t), NamedTuple{names_last_n}(t) +end diff --git a/base/ntuple.jl b/base/ntuple.jl index a5608dfa927c33..6f70b494812230 100644 --- a/base/ntuple.jl +++ b/base/ntuple.jl @@ -32,22 +32,22 @@ julia> ntuple(i -> 2*i, 4) end function _ntuple(f::F, n) where F - @_noinline_meta + @noinline (n >= 0) || throw(ArgumentError(string("tuple length should be ≥ 0, got ", n))) ([f(i) for i = 1:n]...,) end function ntupleany(f, n) - @_noinline_meta + @noinline (n >= 0) || throw(ArgumentError(string("tuple length should be ≥ 0, got ", n))) (Any[f(i) for i = 1:n]...,) end # inferrable ntuple (enough for bootstrapping) ntuple(f, ::Val{0}) = () -ntuple(f, ::Val{1}) = (@_inline_meta; (f(1),)) -ntuple(f, ::Val{2}) = (@_inline_meta; (f(1), f(2))) -ntuple(f, ::Val{3}) = (@_inline_meta; (f(1), f(2), f(3))) +ntuple(f, ::Val{1}) = (@inline; (f(1),)) +ntuple(f, ::Val{2}) = (@inline; (f(1), f(2))) +ntuple(f, ::Val{3}) = (@inline; (f(1), f(2), f(3))) """ ntuple(f, ::Val{N}) diff --git a/base/number.jl b/base/number.jl index 142796d3903ac5..7436655bfad38a 100644 --- a/base/number.jl +++ b/base/number.jl @@ -25,6 +25,8 @@ isinteger(x::Integer) = true Return `true` if `x == zero(x)`; if `x` is an array, this checks whether all of the elements of `x` are zero. +See also: [`isone`](@ref), [`isinteger`](@ref), [`isfinite`](@ref), [`isnan`](@ref). + # Examples ```jldoctest julia> iszero(0.0) @@ -92,15 +94,20 @@ keys(::Number) = OneTo(1) getindex(x::Number) = x function getindex(x::Number, i::Integer) - @_inline_meta + @inline @boundscheck i == 1 || throw(BoundsError()) x end function getindex(x::Number, I::Integer...) - @_inline_meta + @inline @boundscheck all(isone, I) || throw(BoundsError()) x end +get(x::Number, i::Integer, default) = isone(i) ? x : default +get(x::Number, ind::Tuple, default) = all(isone, ind) ? x : default +get(f::Callable, x::Number, i::Integer) = isone(i) ? x : f() +get(f::Callable, x::Number, ind::Tuple) = all(isone, ind) ? x : f() + first(x::Number) = x last(x::Number) = x copy(x::Number) = x # some code treats numbers as collection-like @@ -110,6 +117,8 @@ copy(x::Number) = x # some code treats numbers as collection-like Returns `true` if the value of the sign of `x` is negative, otherwise `false`. +See also [`sign`](@ref) and [`copysign`](@ref). + # Examples ```jldoctest julia> signbit(-4) @@ -131,6 +140,23 @@ signbit(x::Real) = x < 0 sign(x) Return zero if `x==0` and ``x/|x|`` otherwise (i.e., ±1 for real `x`). + +See also [`signbit`](@ref), [`zero`](@ref), [`copysign`](@ref), [`flipsign`](@ref). + +# Examples +```jldoctest +julia> sign(-4.0) +-1.0 + +julia> sign(99) +1 + +julia> sign(-0.0) +-0.0 + +julia> sign(0 + im) +0.0 + 1.0im +``` """ sign(x::Number) = iszero(x) ? x/abs(oneunit(x)) : x/abs(x) sign(x::Real) = ifelse(x < zero(x), oftype(one(x),-1), ifelse(x > zero(x), one(x), typeof(one(x))(x))) @@ -148,6 +174,7 @@ julia> abs2(-3) 9 ``` """ +abs2(x::Number) = abs(x)^2 abs2(x::Real) = x*x """ @@ -222,10 +249,18 @@ inv(x::Number) = one(x)/x Multiply `x` and `y`, giving the result as a larger type. +See also [`promote`](@ref), [`Base.add_sum`](@ref). + # Examples ```jldoctest -julia> widemul(Float32(3.), 4.) -12.0 +julia> widemul(Float32(3.0), 4.0) isa BigFloat +true + +julia> typemax(Int8) * typemax(Int8) +1 + +julia> widemul(typemax(Int8), typemax(Int8)) # == 127^2 +16129 ``` """ widemul(x::Number, y::Number) = widen(x)*widen(y) @@ -243,6 +278,8 @@ map(f, x::Number, ys::Number...) = f(x, ys...) Get the additive identity element for the type of `x` (`x` can also specify the type itself). +See also [`iszero`](@ref), [`one`](@ref), [`oneunit`](@ref), [`oftype`](@ref). + # Examples ```jldoctest julia> zero(1) @@ -280,6 +317,9 @@ should return an identity value of the same precision If you want a quantity that is of the same type as `x`, or of type `T`, even if `x` is dimensionful, use [`oneunit`](@ref) instead. +See also the [`identity`](@ref) function, +and `I` in [`LinearAlgebra`](@ref man-linalg) for the identity matrix. + # Examples ```jldoctest julia> one(3.7) diff --git a/base/opaque_closure.jl b/base/opaque_closure.jl new file mode 100644 index 00000000000000..ac2ae2e8bf3c04 --- /dev/null +++ b/base/opaque_closure.jl @@ -0,0 +1,69 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +""" + @opaque ([type, ]args...) -> body + +Marks a given closure as "opaque". Opaque closures capture the +world age of their creation (as opposed to their invocation). +This allows for more aggressive optimization of the capture +list, but trades off against the ability to inline opaque +closures at the call site, if their creation is not statically +visible. + +An argument tuple type (`type`) may optionally be specified, to +specify allowed argument types in a more flexible way. In particular, +the argument type may be fixed length even if the function is variadic. + +!!! warning + This interface is experimental and subject to change or removal without notice. +""" +macro opaque(ex) + esc(Expr(:opaque_closure, ex)) +end + +macro opaque(ty, ex) + esc(Expr(:opaque_closure, ty, ex)) +end + +# OpaqueClosure construction from pre-inferred CodeInfo/IRCode +using Core.Compiler: IRCode +using Core: CodeInfo + +function compute_ir_rettype(ir::IRCode) + rt = Union{} + for i = 1:length(ir.stmts) + stmt = ir.stmts[i][:inst] + if isa(stmt, Core.Compiler.ReturnNode) && isdefined(stmt, :val) + rt = Core.Compiler.tmerge(Core.Compiler.argextype(stmt.val, ir), rt) + end + end + return Core.Compiler.widenconst(rt) +end + +function Core.OpaqueClosure(ir::IRCode, env...; + nargs::Int = length(ir.argtypes)-1, + isva::Bool = false, + rt = compute_ir_rettype(ir)) + if (isva && nargs > length(ir.argtypes)) || (!isva && nargs != length(ir.argtypes)-1) + throw(ArgumentError("invalid argument count")) + end + src = ccall(:jl_new_code_info_uninit, Ref{CodeInfo}, ()) + src.slotflags = UInt8[] + src.slotnames = fill(:none, nargs+1) + src.slottypes = copy(ir.argtypes) + Core.Compiler.replace_code_newstyle!(src, ir, nargs+1) + Core.Compiler.widen_all_consts!(src) + src.inferred = true + # NOTE: we need ir.argtypes[1] == typeof(env) + + ccall(:jl_new_opaque_closure_from_code_info, Any, (Any, Any, Any, Any, Any, Cint, Any, Cint, Cint, Any), + Tuple{ir.argtypes[2:end]...}, Union{}, rt, @__MODULE__, src, 0, nothing, nargs, isva, env) +end + +function Core.OpaqueClosure(src::CodeInfo, env...) + M = src.parent.def + sig = Base.tuple_type_tail(src.parent.specTypes) + + ccall(:jl_new_opaque_closure_from_code_info, Any, (Any, Any, Any, Any, Any, Cint, Any, Cint, Cint, Any), + sig, Union{}, src.rettype, @__MODULE__, src, 0, nothing, M.nargs - 1, M.isva, env) +end diff --git a/base/operators.jl b/base/operators.jl index 3ac24abfb611c2..92c016d00bf030 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -40,15 +40,8 @@ julia> supertype(Int32) Signed ``` """ -function supertype(T::DataType) - @_pure_meta - T.super -end - -function supertype(T::UnionAll) - @_pure_meta - UnionAll(T.var, supertype(T.body)) -end +supertype(T::DataType) = (@_total_meta; T.super) +supertype(T::UnionAll) = (@_total_meta; UnionAll(T.var, supertype(T.body))) ## generic comparison ## @@ -79,6 +72,9 @@ handle comparison to other types via promotion rules where possible. [`isequal`](@ref) falls back to `==`, so new methods of `==` will be used by the [`Dict`](@ref) type to compare keys. If your type will be used as a dictionary key, it should therefore also implement [`hash`](@ref). + +If some type defines `==`, [`isequal`](@ref), and [`isless`](@ref) then it should +also implement [`<`](@ref) to ensure consistency of comparisons. """ == @@ -90,6 +86,10 @@ and of missing values. `isequal` treats all floating-point `NaN` values as equal to each other, treats `-0.0` as unequal to `0.0`, and [`missing`](@ref) as equal to `missing`. Always returns a `Bool` value. +`isequal` is an equivalence relation - it is reflexive (`===` implies `isequal`), symmetric +(`isequal(a, b)` implies `isequal(b, a)`) and transitive (`isequal(a, b)` and +`isequal(b, c)` implies `isequal(a, c)`). + # Implementation The default implementation of `isequal` calls `==`, so a type that does not involve floating-point values generally only needs to define `==`. @@ -98,8 +98,12 @@ floating-point values generally only needs to define `==`. that `hash(x) == hash(y)`. This typically means that types for which a custom `==` or `isequal` method exists must -implement a corresponding `hash` method (and vice versa). Collections typically implement -`isequal` by calling `isequal` recursively on all contents. +implement a corresponding [`hash`](@ref) method (and vice versa). Collections typically +implement `isequal` by calling `isequal` recursively on all contents. + +Furthermore, `isequal` is linked with [`isless`](@ref), and they work together to +define a fixed total ordering, where exactly one of `isequal(x, y)`, `isless(x, y)`, or +`isless(y, x)` must be `true` (and the other two `false`). Scalar types generally do not need to implement `isequal` separate from `==`, unless they represent floating-point numbers amenable to a more efficient implementation than that @@ -118,9 +122,15 @@ true julia> isequal(0.0, -0.0) false + +julia> missing == missing +missing + +julia> isequal(missing, missing) +true ``` """ -isequal(x, y) = x == y +isequal(x, y) = (x == y)::Bool # all `missing` cases are handled in missing.jl signequal(x, y) = signbit(x)::Bool == signbit(y)::Bool signless(x, y) = signbit(x)::Bool & !signbit(y)::Bool @@ -132,8 +142,8 @@ isequal(x::AbstractFloat, y::Real ) = (isnan(x) & isnan(y)) | signequal( """ isless(x, y) -Test whether `x` is less than `y`, according to a fixed total order. -`isless` is not defined on all pairs of values `(x, y)`. However, if it +Test whether `x` is less than `y`, according to a fixed total order (defined together with +[`isequal`](@ref)). `isless` is not defined on all pairs of values `(x, y)`. However, if it is defined, it is expected to satisfy the following: - If `isless(x, y)` is defined, then so is `isless(y, x)` and `isequal(x, y)`, and exactly one of those three yields `true`. @@ -141,7 +151,7 @@ is defined, it is expected to satisfy the following: `isless(x, y) && isless(y, z)` implies `isless(x, z)`. Values that are normally unordered, such as `NaN`, -are ordered in an arbitrary but consistent fashion. +are ordered after regular values. [`missing`](@ref) values are ordered last. This is the default comparison used by [`sort`](@ref). @@ -150,15 +160,17 @@ This is the default comparison used by [`sort`](@ref). Non-numeric types with a total order should implement this function. Numeric types only need to implement it if they have special values such as `NaN`. Types with a partial order should implement [`<`](@ref). +See the documentation on [Alternate orderings](@ref) for how to define alternate +ordering methods that can be used in sorting and related functions. # Examples - ```jldoctest - julia> isless(1, 3) - true +```jldoctest +julia> isless(1, 3) +true - julia> isless("Red", "Blue") - false - ``` +julia> isless("Red", "Blue") +false +``` """ function isless end @@ -166,15 +178,70 @@ isless(x::AbstractFloat, y::AbstractFloat) = (!isnan(x) & (isnan(y) | signless(x isless(x::Real, y::AbstractFloat) = (!isnan(x) & (isnan(y) | signless(x, y))) | (x < y) isless(x::AbstractFloat, y::Real ) = (!isnan(x) & (isnan(y) | signless(x, y))) | (x < y) +""" + isgreater(x, y) -function ==(T::Type, S::Type) - @_pure_meta - return ccall(:jl_types_equal, Cint, (Any, Any), T, S) != 0 -end -function !=(T::Type, S::Type) - @_pure_meta - return !(T == S) -end +Not the inverse of `isless`! Test whether `x` is greater than `y`, according to +a fixed total order compatible with `min`. + +Defined with `isless`, this function is usually `isless(y, x)`, but `NaN` and +[`missing`](@ref) are ordered as smaller than any ordinary value with `missing` +smaller than `NaN`. + +So `isless` defines an ascending total order with `NaN` and `missing` as the +largest values and `isgreater` defines a descending total order with `NaN` and +`missing` as the smallest values. + +!!! note + + Like `min`, `isgreater` orders containers (tuples, vectors, etc) + lexicographically with `isless(y, x)` rather than recursively with itself: + + ```jldoctest + julia> Base.isgreater(1, NaN) # 1 is greater than NaN + true + + julia> Base.isgreater((1,), (NaN,)) # But (1,) is not greater than (NaN,) + false + + julia> sort([1, 2, 3, NaN]; lt=Base.isgreater) + 4-element Vector{Float64}: + 3.0 + 2.0 + 1.0 + NaN + + julia> sort(tuple.([1, 2, 3, NaN]); lt=Base.isgreater) + 4-element Vector{Tuple{Float64}}: + (NaN,) + (3.0,) + (2.0,) + (1.0,) + ``` + +# Implementation +This is unexported. Types should not usually implement this function. Instead, implement `isless`. +""" +isgreater(x, y) = isunordered(x) || isunordered(y) ? isless(x, y) : isless(y, x) + +""" + isunordered(x) + +Return `true` if `x` is a value that is not orderable according to [`<`](@ref), such as `NaN` +or `missing`. + +The values that evaluate to `true` with this predicate may be orderable with respect to other +orderings such as [`isless`](@ref). + +!!! compat "Julia 1.7" + This function requires Julia 1.7 or later. +""" +isunordered(x) = false +isunordered(x::AbstractFloat) = isnan(x) +isunordered(x::Missing) = true + +==(T::Type, S::Type) = (@_total_meta; ccall(:jl_types_equal, Cint, (Any, Any), T, S) != 0) +!=(T::Type, S::Type) = (@_total_meta; !(T == S)) ==(T::TypeVar, S::Type) = false ==(T::Type, S::TypeVar) = false @@ -260,7 +327,6 @@ a partial order. New numeric types with a canonical partial order should implement this function for two arguments of the new type. Types with a canonical total order should implement [`isless`](@ref) instead. -(x < y) | (x == y) # Examples ```jldoctest @@ -354,22 +420,6 @@ const ≥ = >= # which is more idiomatic: isless(x::Real, y::Real) = x ifelse(1 > 2, 1, 2) -2 -``` -""" -ifelse - """ cmp(x,y) @@ -405,7 +455,7 @@ cmp(x::Integer, y::Integer) = ifelse(isless(x, y), -1, ifelse(isless(y, x), 1, 0 """ max(x, y, ...) -Return the maximum of the arguments. See also the [`maximum`](@ref) function +Return the maximum of the arguments (with respect to [`isless`](@ref)). See also the [`maximum`](@ref) function to take the maximum element from a collection. # Examples @@ -419,7 +469,7 @@ max(x, y) = ifelse(isless(y, x), x, y) """ min(x, y, ...) -Return the minimum of the arguments. See also the [`minimum`](@ref) function +Return the minimum of the arguments (with respect to [`isless`](@ref)). See also the [`minimum`](@ref) function to take the minimum element from a collection. # Examples @@ -433,7 +483,9 @@ min(x,y) = ifelse(isless(y, x), y, x) """ minmax(x, y) -Return `(min(x,y), max(x,y))`. See also: [`extrema`](@ref) that returns `(minimum(x), maximum(x))`. +Return `(min(x,y), max(x,y))`. + +See also [`extrema`](@ref) that returns `(minimum(x), maximum(x))`. # Examples ```jldoctest @@ -443,58 +495,6 @@ julia> minmax('c','b') """ minmax(x,y) = isless(y, x) ? (y, x) : (x, y) -""" - extrema(itr) -> Tuple - -Compute both the minimum and maximum element in a single pass, and return them as a 2-tuple. - -# Examples -```jldoctest -julia> extrema(2:10) -(2, 10) - -julia> extrema([9,pi,4.5]) -(3.141592653589793, 9.0) -``` -""" -extrema(itr) = _extrema_itr(identity, itr) - -""" - extrema(f, itr) -> Tuple - -Compute both the minimum and maximum of `f` applied to each element in `itr` and return -them as a 2-tuple. Only one pass is made over `itr`. - -!!! compat "Julia 1.2" - This method requires Julia 1.2 or later. - -# Examples -```jldoctest -julia> extrema(sin, 0:π) -(0.0, 0.9092974268256817) -``` -""" -extrema(f, itr) = _extrema_itr(f, itr) - -function _extrema_itr(f, itr) - y = iterate(itr) - y === nothing && throw(ArgumentError("collection must be non-empty")) - (v, s) = y - vmin = vmax = f(v) - while true - y = iterate(itr, s) - y === nothing && break - (x, s) = y - fx = f(x) - vmax = max(fx, vmax) - vmin = min(fx, vmin) - end - return (vmin, vmax) -end - -extrema(x::Real) = (x, x) -extrema(f, x::Real) = (y = f(x); (y, y)) - ## definitions providing basic traits of arithmetic operators ## """ @@ -502,13 +502,15 @@ extrema(f, x::Real) = (y = f(x); (y, y)) The identity function. Returns its argument. +See also: [`one`](@ref), [`oneunit`](@ref), and [`LinearAlgebra`](@ref man-linalg)'s `I`. + # Examples ```jldoctest julia> identity("Well, what did you expect?") "Well, what did you expect?" ``` """ -identity(x) = x +identity(@nospecialize x) = x +(x::Number) = x *(x::Number) = x @@ -517,25 +519,63 @@ identity(x) = x xor(x::Integer) = x const ⊻ = xor +const ⊼ = nand +const ⊽ = nor -# foldl for argument lists. expand recursively up to a point, then -# switch to a loop. this allows small cases like `a+b+c+d` to be inlined +# foldl for argument lists. expand fully up to a point, then +# switch to a loop. this allows small cases like `a+b+c+d` to be managed # efficiently, without a major slowdown for `+(x...)` when `x` is big. -afoldl(op,a) = a -afoldl(op,a,b) = op(a,b) -afoldl(op,a,b,c...) = afoldl(op, op(a,b), c...) -function afoldl(op,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,qs...) - y = op(op(op(op(op(op(op(op(op(op(op(op(op(op(op(a,b),c),d),e),f),g),h),i),j),k),l),m),n),o),p) - for x in qs; y = op(y,x); end - y +# n.b.: keep this method count small, so it can be inferred without hitting the +# method count limit in inference +afoldl(op, a) = a +function afoldl(op, a, bs...) + l = length(bs) + i = 0; y = a; l == i && return y + #@nexprs 31 i -> (y = op(y, bs[i]); l == i && return y) + i = 1; y = op(y, bs[i]); l == i && return y + i = 2; y = op(y, bs[i]); l == i && return y + i = 3; y = op(y, bs[i]); l == i && return y + i = 4; y = op(y, bs[i]); l == i && return y + i = 5; y = op(y, bs[i]); l == i && return y + i = 6; y = op(y, bs[i]); l == i && return y + i = 7; y = op(y, bs[i]); l == i && return y + i = 8; y = op(y, bs[i]); l == i && return y + i = 9; y = op(y, bs[i]); l == i && return y + i = 10; y = op(y, bs[i]); l == i && return y + i = 11; y = op(y, bs[i]); l == i && return y + i = 12; y = op(y, bs[i]); l == i && return y + i = 13; y = op(y, bs[i]); l == i && return y + i = 14; y = op(y, bs[i]); l == i && return y + i = 15; y = op(y, bs[i]); l == i && return y + i = 16; y = op(y, bs[i]); l == i && return y + i = 17; y = op(y, bs[i]); l == i && return y + i = 18; y = op(y, bs[i]); l == i && return y + i = 19; y = op(y, bs[i]); l == i && return y + i = 20; y = op(y, bs[i]); l == i && return y + i = 21; y = op(y, bs[i]); l == i && return y + i = 22; y = op(y, bs[i]); l == i && return y + i = 23; y = op(y, bs[i]); l == i && return y + i = 24; y = op(y, bs[i]); l == i && return y + i = 25; y = op(y, bs[i]); l == i && return y + i = 26; y = op(y, bs[i]); l == i && return y + i = 27; y = op(y, bs[i]); l == i && return y + i = 28; y = op(y, bs[i]); l == i && return y + i = 29; y = op(y, bs[i]); l == i && return y + i = 30; y = op(y, bs[i]); l == i && return y + i = 31; y = op(y, bs[i]); l == i && return y + for i in (i + 1):l + y = op(y, bs[i]) + end + return y end +typeof(afoldl).name.mt.max_args = 34 for op in (:+, :*, :&, :|, :xor, :min, :max, :kron) @eval begin # note: these definitions must not cause a dispatch loop when +(a,b) is # not defined, and must only try to call 2-argument definitions, so # that defining +(a,b) is sufficient for full functionality. - ($op)(a, b, c, xs...) = afoldl($op, ($op)(($op)(a,b),c), xs...) + ($op)(a, b, c, xs...) = (@inline; afoldl($op, ($op)(($op)(a,b),c), xs...)) # a further concern is that it's easy for a type like (Int,Int...) # to match many definitions, so we need to keep the number of # definitions down to avoid losing type information. @@ -597,16 +637,16 @@ julia> bitstring(Int8(3)) julia> bitstring(Int8(12)) "00001100" ``` -See also [`>>`](@ref), [`>>>`](@ref). +See also [`>>`](@ref), [`>>>`](@ref), [`exp2`](@ref), [`ldexp`](@ref). """ function <<(x::Integer, c::Integer) - @_inline_meta + @inline typemin(Int) <= c <= typemax(Int) && return x << (c % Int) (x >= 0 || c >= 0) && return zero(x) << 0 # for type stability oftype(x, -1) end function <<(x::Integer, c::Unsigned) - @_inline_meta + @inline if c isa UInt throw(MethodError(<<, (x, c))) end @@ -645,7 +685,7 @@ julia> bitstring(Int8(-4)) See also [`>>>`](@ref), [`<<`](@ref). """ function >>(x::Integer, c::Integer) - @_inline_meta + @inline if c isa UInt throw(MethodError(>>, (x, c))) end @@ -683,11 +723,11 @@ is equivalent to [`>>`](@ref). See also [`>>`](@ref), [`<<`](@ref). """ function >>>(x::Integer, c::Integer) - @_inline_meta + @inline typemin(Int) <= c <= typemax(Int) ? x >>> (c % Int) : zero(x) >>> 0 end function >>>(x::Integer, c::Unsigned) - @_inline_meta + @inline if c isa UInt throw(MethodError(>>>, (x, c))) end @@ -704,6 +744,8 @@ end Remainder from Euclidean division, returning a value of the same sign as `x`, and smaller in magnitude than `y`. This value is always exact. +See also: [`div`](@ref), [`mod`](@ref), [`mod1`](@ref), [`divrem`](@ref). + # Examples ```jldoctest julia> x = 15; y = 4; @@ -713,6 +755,10 @@ julia> x % y julia> x == div(x, y) * y + rem(x, y) true + +julia> rem.(-5:5, 3)' +1×11 adjoint(::Vector{Int64}) with eltype Int64: + -2 -1 0 -2 -1 0 1 2 0 1 2 ``` """ rem @@ -722,7 +768,10 @@ const % = rem div(x, y) ÷(x, y) -The quotient from Euclidean division. Computes `x/y`, truncated to an integer. +The quotient from Euclidean (integer) division. Generally equivalent +to a mathematical operation x/y without a fractional part. + +See also: [`cld`](@ref), [`fld`](@ref), [`rem`](@ref), [`divrem`](@ref). # Examples ```jldoctest @@ -734,6 +783,10 @@ julia> -5 ÷ 3 julia> 5.0 ÷ 2 2.0 + +julia> div.(-5:5, 3)' +1×11 adjoint(::Vector{Int64}) with eltype Int64: + -1 -1 -1 0 0 0 0 0 1 1 1 ``` """ div @@ -745,15 +798,24 @@ const ÷ = div Modulus after flooring division, returning a value `r` such that `mod(r, y) == mod(x, y)` in the range ``(0, y]`` for positive `y` and in the range ``[y,0)`` for negative `y`. -See also: [`fld1`](@ref), [`fldmod1`](@ref). +With integer arguments and positive `y`, this is equal to `mod(x, 1:y)`, and hence natural +for 1-based indexing. By comparison, `mod(x, y) == mod(x, 0:y-1)` is natural for computations with +offsets or strides. + +See also [`mod`](@ref), [`fld1`](@ref), [`fldmod1`](@ref). # Examples ```jldoctest julia> mod1(4, 2) 2 -julia> mod1(4, 3) -1 +julia> mod1.(-5:5, 3)' +1×11 adjoint(::Vector{Int64}) with eltype Int64: + 1 2 3 1 2 3 1 2 3 1 2 + +julia> mod1.([-0.1, 0, 0.1, 1, 2, 2.9, 3, 3.1]', 3) +1×8 Matrix{Float64}: + 2.9 3.0 0.1 1.0 2.0 2.9 3.0 0.1 ``` """ mod1(x::T, y::T) where {T<:Real} = (m = mod(x, y); ifelse(m == 0, y, m)) @@ -764,7 +826,7 @@ mod1(x::T, y::T) where {T<:Real} = (m = mod(x, y); ifelse(m == 0, y, m)) Flooring division, returning a value consistent with `mod1(x,y)` -See also: [`mod1`](@ref), [`fldmod1`](@ref). +See also [`mod1`](@ref), [`fldmod1`](@ref). # Examples ```jldoctest @@ -791,7 +853,7 @@ end Return `(fld1(x,y), mod1(x,y))`. -See also: [`fld1`](@ref), [`mod1`](@ref). +See also [`fld1`](@ref), [`mod1`](@ref). """ fldmod1(x, y) = (fld1(x, y), mod1(x, y)) @@ -826,21 +888,53 @@ widen(x::Type{T}) where {T} = throw(MethodError(widen, (T,))) |>(x, f) Applies a function to the preceding argument. This allows for easy function chaining. +When used with anonymous functions, parentheses are typically required around the definition to get the intended chain. # Examples ```jldoctest -julia> [1:5;] |> x->x.^2 |> sum |> inv +julia> [1:5;] .|> (x -> x^2) |> sum |> inv 0.01818181818181818 ``` """ |>(x, f) = f(x) +""" + f = Returns(value) + +Create a callable `f` such that `f(args...; kw...) === value` holds. + +# Examples + +```jldoctest +julia> f = Returns(42); + +julia> f(1) +42 + +julia> f("hello", x=32) +42 + +julia> f.value +42 +``` + +!!! compat "Julia 1.7" + `Returns` requires at least Julia 1.7. +""" +struct Returns{V} <: Function + value::V + Returns{V}(value) where {V} = new{V}(value) + Returns(value) = new{Core.Typeof(value)}(value) +end + +(obj::Returns)(@nospecialize(args...); @nospecialize(kw...)) = obj.value + # function composition """ f ∘ g -Compose functions: i.e. `(f ∘ g)(args...)` means `f(g(args...))`. The `∘` symbol can be +Compose functions: i.e. `(f ∘ g)(args...; kwargs...)` means `f(g(args...; kwargs...))`. The `∘` symbol can be entered in the Julia REPL (and most editors, appropriately configured) by typing `\\circ`. Function composition also works in prefix form: `∘(f, g)` is the same as `f ∘ g`. @@ -851,7 +945,10 @@ and splatting `∘(fs...)` for composing an iterable collection of functions. Multiple function composition requires at least Julia 1.4. !!! compat "Julia 1.5" - Composition of one function ∘(f) requires at least Julia 1.5. + Composition of one function ∘(f) requires at least Julia 1.5. + +!!! compat "Julia 1.7" + Using keyword arguments requires at least Julia 1.7. # Examples ```jldoctest @@ -871,33 +968,80 @@ julia> fs = [ julia> ∘(fs...)(3) 3.0 ``` +See also [`ComposedFunction`](@ref), [`!f::Function`](@ref). """ function ∘ end -struct ComposedFunction{F,G} <: Function - f::F - g::G - ComposedFunction{F, G}(f, g) where {F, G} = new{F, G}(f, g) - ComposedFunction(f, g) = new{Core.Typeof(f),Core.Typeof(g)}(f, g) +""" + ComposedFunction{Outer,Inner} <: Function + +Represents the composition of two callable objects `outer::Outer` and `inner::Inner`. That is +```julia +ComposedFunction(outer, inner)(args...; kw...) === outer(inner(args...; kw...)) +``` +The preferred way to construct instance of `ComposedFunction` is to use the composition operator [`∘`](@ref): +```jldoctest +julia> sin ∘ cos === ComposedFunction(sin, cos) +true + +julia> typeof(sin∘cos) +ComposedFunction{typeof(sin), typeof(cos)} +``` +The composed pieces are stored in the fields of `ComposedFunction` and can be retrieved as follows: +```jldoctest +julia> composition = sin ∘ cos +sin ∘ cos + +julia> composition.outer === sin +true + +julia> composition.inner === cos +true +``` +!!! compat "Julia 1.6" + ComposedFunction requires at least Julia 1.6. In earlier versions `∘` returns an anonymous function instead. + +See also [`∘`](@ref). +""" +struct ComposedFunction{O,I} <: Function + outer::O + inner::I + ComposedFunction{O, I}(outer, inner) where {O, I} = new{O, I}(outer, inner) + ComposedFunction(outer, inner) = new{Core.Typeof(outer),Core.Typeof(inner)}(outer, inner) end -(c::ComposedFunction)(x...) = c.f(c.g(x...)) +(c::ComposedFunction)(x...; kw...) = c.outer(c.inner(x...; kw...)) ∘(f) = f ∘(f, g) = ComposedFunction(f, g) ∘(f, g, h...) = ∘(f ∘ g, h...) function show(io::IO, c::ComposedFunction) - show(io, c.f) + c.outer isa ComposedFunction ? show(io, c.outer) : _showcomposed(io, c.outer) print(io, " ∘ ") - show(io, c.g) + _showcomposed(io, c.inner) +end + +#shows !f instead of (!) ∘ f when ! is the outermost function +function show(io::IO, c::ComposedFunction{typeof(!)}) + print(io, '!') + _showcomposed(io, c.inner) end +_showcomposed(io::IO, x) = show(io, x) +#display operators like + and - inside parens +_showcomposed(io::IO, f::Function) = isoperator(Symbol(f)) ? (print(io, '('); show(io, f); print(io, ')')) : show(io, f) +#nesting for chained composition +_showcomposed(io::IO, f::ComposedFunction) = (print(io, '('); show(io, f); print(io, ')')) +#no nesting when ! is the outer function in a composition chain +_showcomposed(io::IO, f::ComposedFunction{typeof(!)}) = show(io, f) + """ !f::Function -Predicate function negation: when the argument of `!` is a function, it returns a -function which computes the boolean negation of `f`. +Predicate function negation: when the argument of `!` is a function, it returns a composed function which computes the boolean negation of `f`. + +See also [`∘`](@ref). # Examples ```jldoctest @@ -910,8 +1054,12 @@ julia> filter(isletter, str) julia> filter(!isletter, str) "∀ > 0, ∃ > 0: |-| < ⇒ |()-()| < " ``` + +!!! compat "Julia 1.9" + Starting with Julia 1.9, `!f` returns a [`ComposedFunction`](@ref) instead of an anonymous function. """ -!(f::Function) = (x...)->!f(x...) +!(f::Function) = (!) ∘ f +!(f::ComposedFunction{typeof(!)}) = f.inner #allows !!f === f """ Fix1(f, x) @@ -919,6 +1067,8 @@ julia> filter(!isletter, str) A type representing a partially-applied version of the two-argument function `f`, with the first argument fixed to the value "x". In other words, `Fix1(f, x)` behaves similarly to `y->f(x, y)`. + +See also [`Fix2`](@ref Base.Fix2). """ struct Fix1{F,T} <: Function f::F @@ -1057,13 +1207,15 @@ julia> map(Base.splat(+), zip(1:3,4:6)) """ splat(f) = args->f(args...) -## in & contains +## in and related operators """ - in(x) + in(collection) + ∈(collection) -Create a function that checks whether its argument is [`in`](@ref) `x`, i.e. -a function equivalent to `y -> y in x`. +Create a function that checks whether its argument is [`in`](@ref) `collection`, i.e. +a function equivalent to `y -> y in collection`. See also [`insorted`](@ref) for use +with sorted collections. The returned function is of type `Base.Fix2{typeof(in)}`, which can be used to implement specialized methods. @@ -1084,14 +1236,34 @@ function in(x, itr) end const ∈ = in -∋(itr, x) = ∈(x, itr) ∉(x, itr) = !∈(x, itr) +∉(itr) = Fix2(∉, itr) + +""" + ∋(collection, item) -> Bool + +Like [`in`](@ref), but with arguments in reverse order. +Avoid adding methods to this function; define `in` instead. +""" +∋(itr, x) = in(x, itr) + +""" + ∋(item) + +Create a function that checks whether its argument contains the given `item`, i.e. +a function equivalent to `y -> item in y`. + +!!! compat "Julia 1.6" + This method requires Julia 1.6 or later. +""" +∋(x) = Fix2(∋, x) + ∌(itr, x) = !∋(itr, x) +∌(x) = Fix2(∌, x) """ in(item, collection) -> Bool ∈(item, collection) -> Bool - ∋(collection, item) -> Bool Determine whether an item is in the given collection, in the sense that it is [`==`](@ref) to one of the values generated by iterating over the collection. @@ -1157,8 +1329,10 @@ julia> [1, 2] .∈ ([2, 3],) 0 1 ``` + +See also: [`insorted`](@ref), [`contains`](@ref), [`occursin`](@ref), [`issubset`](@ref). """ -in, ∋ +in """ ∉(item, collection) -> Bool diff --git a/base/options.jl b/base/options.jl index a23dd62f78b438..63f73982b2e8ec 100644 --- a/base/options.jl +++ b/base/options.jl @@ -1,6 +1,6 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -# NOTE: This type needs to be kept in sync with jl_options in src/julia.h +# NOTE: This type needs to be kept in sync with jl_options in src/jloptions.h struct JLOptions quiet::Int8 banner::Int8 @@ -9,7 +9,9 @@ struct JLOptions commands::Ptr{Ptr{UInt8}} # (e)eval, (E)print, (L)load image_file::Ptr{UInt8} cpu_target::Ptr{UInt8} - nthreads::Int32 + nthreadpools::Int16 + nthreads::Int16 + nthreads_per_pool::Ptr{Int16} nprocs::Int32 machine_file::Ptr{UInt8} project::Ptr{UInt8} @@ -20,7 +22,9 @@ struct JLOptions compile_enabled::Int8 code_coverage::Int8 malloc_log::Int8 + tracked_path::Ptr{UInt8} opt_level::Int8 + opt_level_min::Int8 debug_level::Int8 check_bounds::Int8 depwarn::Int8 @@ -45,6 +49,9 @@ struct JLOptions image_file_specified::Int8 warn_scope::Int8 image_codegen::Int8 + rr_detach::Int8 + strip_metadata::Int8 + strip_ir::Int8 end # This runs early in the sysimage != is not defined yet @@ -84,3 +91,7 @@ function unsafe_load_commands(v::Ptr{Ptr{UInt8}}) end return cmds end + +function is_file_tracked(file::Symbol) + return ccall(:jl_is_file_tracked, Cint, (Any,), file) == 1 +end diff --git a/base/ordering.jl b/base/ordering.jl index dc2f2c8595be23..e49102159c9620 100644 --- a/base/ordering.jl +++ b/base/ordering.jl @@ -6,7 +6,7 @@ module Order import ..@__MODULE__, ..parentmodule const Base = parentmodule(@__MODULE__) import .Base: - AbstractVector, @propagate_inbounds, isless, identity, getindex, + AbstractVector, @propagate_inbounds, isless, identity, getindex, reverse, +, -, !, &, <, | ## notions of element ordering ## @@ -18,9 +18,26 @@ export # not exported by Base DirectOrdering, lt, ord, ordtype +""" + Base.Order.Ordering + +Abstract type which represents a total order on some set of elements. + +Use [`Base.Order.lt`](@ref) to compare two elements according to the ordering. +""" abstract type Ordering end struct ForwardOrdering <: Ordering end + +""" + ReverseOrdering(fwd::Ordering=Forward) + +A wrapper which reverses an ordering. + +For a given `Ordering` `o`, the following holds for all `a`, `b`: + + lt(ReverseOrdering(o), a, b) == lt(o, b, a) +""" struct ReverseOrdering{Fwd<:Ordering} <: Ordering fwd::Fwd end @@ -29,11 +46,36 @@ ReverseOrdering(rev::ReverseOrdering) = rev.fwd ReverseOrdering(fwd::Fwd) where {Fwd} = ReverseOrdering{Fwd}(fwd) ReverseOrdering() = ReverseOrdering(ForwardOrdering()) +""" + reverse(o::Base.Ordering) + +reverses ordering specified by `o`. + +""" +reverse(o::Ordering) = ReverseOrdering(o) + const DirectOrdering = Union{ForwardOrdering,ReverseOrdering{ForwardOrdering}} +""" + Base.Order.Forward + +Default ordering according to [`isless`](@ref). +""" const Forward = ForwardOrdering() + +""" + Base.Order.Reverse + +Reverse ordering according to [`isless`](@ref). +""" const Reverse = ReverseOrdering() +""" + By(by, order::Ordering=Forward) + +`Ordering` which applies `order` to elements after they have been transformed +by the function `by`. +""" struct By{T, O} <: Ordering by::T order::O @@ -42,10 +84,23 @@ end # backwards compatibility with VERSION < v"1.5-" By(by) = By(by, Forward) +""" + Lt(lt) + +`Ordering` which calls `lt(a, b)` to compare elements. `lt` should +obey the same rules as implementations of [`isless`](@ref). +""" struct Lt{T} <: Ordering lt::T end +""" + Perm(order::Ordering, data::AbstractVector) + +`Ordering` on the indices of `data` where `i` is less than `j` if `data[i]` is +less than `data[j]` according to `order`. In the case that `data[i]` and +`data[j]` are equal, `i` and `j` are compared by numeric value. +""" struct Perm{O<:Ordering,V<:AbstractVector} <: Ordering order::O data::V @@ -54,6 +109,11 @@ end ReverseOrdering(by::By) = By(by.by, ReverseOrdering(by.order)) ReverseOrdering(perm::Perm) = Perm(ReverseOrdering(perm.order), perm.data) +""" + lt(o::Ordering, a, b) + +Test whether `a` is less than `b` according to the ordering `o`. +""" lt(o::ForwardOrdering, a, b) = isless(a,b) lt(o::ReverseOrdering, a, b) = lt(o.fwd,b,a) lt(o::By, a, b) = lt(o.order,o.by(a),o.by(b)) @@ -78,6 +138,22 @@ function _ord(lt, by, order::Ordering) end end +""" + ord(lt, by, rev::Union{Bool, Nothing}, order::Ordering=Forward) + +Construct an [`Ordering`](@ref) object from the same arguments used by +[`sort!`](@ref). +Elements are first transformed by the function `by` (which may be +[`identity`](@ref)) and are then compared according to either the function `lt` +or an existing ordering `order`. `lt` should be [`isless`](@ref) or a function +which obeys similar rules. Finally, the resulting order is reversed if +`rev=true`. + +Passing an `lt` other than `isless` along with an `order` other than +[`Base.Order.Forward`](@ref) or [`Base.Order.Reverse`](@ref) is not permitted, +otherwise all options are independent and can be used together in all possible +combinations. +""" ord(lt, by, rev::Nothing, order::Ordering=Forward) = _ord(lt, by, order) function ord(lt, by, rev::Bool, order::Ordering=Forward) diff --git a/base/pair.jl b/base/pair.jl index 30fd91892ce4b5..b5dffbb4e7e866 100644 --- a/base/pair.jl +++ b/base/pair.jl @@ -1,18 +1,5 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -struct Pair{A, B} - first::A - second::B - function Pair{A, B}(@nospecialize(a), @nospecialize(b)) where {A, B} - @_inline_meta - # if we didn't inline this, it's probably because the callsite was actually dynamic - # to avoid potentially compiling many copies of this, we mark the arguments with `@nospecialize` - # but also mark the whole function with `@inline` to ensure we will inline it whenever possible - # (even if `convert(::Type{A}, a::A)` for some reason was expensive) - return new(a, b) - end -end -Pair(a, b) = Pair{typeof(a), typeof(b)}(a, b) const => = Pair """ @@ -23,7 +10,7 @@ Construct a `Pair` object with type `Pair{typeof(x), typeof(y)}`. The elements are stored in the fields `first` and `second`. They can also be accessed via iteration (but a `Pair` is treated as a single "scalar" for broadcasting operations). -See also: [`Dict`](@ref) +See also [`Dict`](@ref). # Examples ```jldoctest diff --git a/base/parse.jl b/base/parse.jl index 1097e8a19b8040..1c911c96e1479c 100644 --- a/base/parse.jl +++ b/base/parse.jl @@ -194,10 +194,10 @@ function tryparse_internal(::Type{Bool}, sbuff::Union{String,SubString{String}}, orig_end = endpos # Ignore leading and trailing whitespace - while isspace(sbuff[startpos]) && startpos <= endpos + while startpos <= endpos && isspace(sbuff[startpos]) startpos = nextind(sbuff, startpos) end - while isspace(sbuff[endpos]) && endpos >= startpos + while endpos >= startpos && isspace(sbuff[endpos]) endpos = prevind(sbuff, endpos) end diff --git a/base/partr.jl b/base/partr.jl new file mode 100644 index 00000000000000..a4cfcb60fe5201 --- /dev/null +++ b/base/partr.jl @@ -0,0 +1,194 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +module Partr + +using ..Threads: SpinLock, nthreads, threadid + +# a task minheap +mutable struct taskheap + const lock::SpinLock + const tasks::Vector{Task} + @atomic ntasks::Int32 + @atomic priority::UInt16 + taskheap() = new(SpinLock(), Vector{Task}(undef, 256), zero(Int32), typemax(UInt16)) +end + + +# multiqueue minheap state +const heap_d = UInt32(8) +const heaps = [Vector{taskheap}(undef, 0), Vector{taskheap}(undef, 0)] +const heaps_lock = [SpinLock(), SpinLock()] +const cong_unbias = [typemax(UInt32), typemax(UInt32)] + + +cong(max::UInt32, unbias::UInt32) = + ccall(:jl_rand_ptls, UInt32, (UInt32, UInt32), max, unbias) + UInt32(1) + +function unbias_cong(max::UInt32) + return typemax(UInt32) - ((typemax(UInt32) % max) + UInt32(1)) +end + + +function multiq_sift_up(heap::taskheap, idx::Int32) + while idx > Int32(1) + parent = (idx - Int32(2)) ÷ heap_d + Int32(1) + if heap.tasks[idx].priority < heap.tasks[parent].priority + t = heap.tasks[parent] + heap.tasks[parent] = heap.tasks[idx] + heap.tasks[idx] = t + idx = parent + else + break + end + end +end + + +function multiq_sift_down(heap::taskheap, idx::Int32) + if idx <= heap.ntasks + for child = (heap_d * idx - heap_d + 2):(heap_d * idx + 1) + child = Int(child) + child > length(heap.tasks) && break + if isassigned(heap.tasks, child) && + heap.tasks[child].priority < heap.tasks[idx].priority + t = heap.tasks[idx] + heap.tasks[idx] = heap.tasks[child] + heap.tasks[child] = t + multiq_sift_down(heap, Int32(child)) + end + end + end +end + + +function multiq_size(tpid::Int8) + nt = UInt32(Threads._nthreads_in_pool(tpid)) + tp = tpid + 1 + tpheaps = heaps[tp] + heap_c = UInt32(2) + heap_p = UInt32(length(tpheaps)) + + if heap_c * nt <= heap_p + return heap_p + end + + @lock heaps_lock[tp] begin + heap_p = UInt32(length(tpheaps)) + nt = UInt32(Threads._nthreads_in_pool(tpid)) + if heap_c * nt <= heap_p + return heap_p + end + + heap_p += heap_c * nt + newheaps = Vector{taskheap}(undef, heap_p) + copyto!(newheaps, tpheaps) + for i = (1 + length(tpheaps)):heap_p + newheaps[i] = taskheap() + end + heaps[tp] = newheaps + cong_unbias[tp] = unbias_cong(heap_p) + end + + return heap_p +end + + +function multiq_insert(task::Task, priority::UInt16) + tpid = ccall(:jl_get_task_threadpoolid, Int8, (Any,), task) + heap_p = multiq_size(tpid) + tp = tpid + 1 + + task.priority = priority + + rn = cong(heap_p, cong_unbias[tp]) + tpheaps = heaps[tp] + while !trylock(tpheaps[rn].lock) + rn = cong(heap_p, cong_unbias[tp]) + end + + heap = tpheaps[rn] + if heap.ntasks >= length(heap.tasks) + resize!(heap.tasks, length(heap.tasks) * 2) + end + + ntasks = heap.ntasks + Int32(1) + @atomic :monotonic heap.ntasks = ntasks + heap.tasks[ntasks] = task + multiq_sift_up(heap, ntasks) + priority = heap.priority + if task.priority < priority + @atomic :monotonic heap.priority = task.priority + end + unlock(heap.lock) + + return true +end + + +function multiq_deletemin() + local rn1, rn2 + local prio1, prio2 + + tid = Threads.threadid() + tp = ccall(:jl_threadpoolid, Int8, (Int16,), tid-1) + 1 + tpheaps = heaps[tp] + + @label retry + GC.safepoint() + heap_p = UInt32(length(tpheaps)) + for i = UInt32(0):heap_p + if i == heap_p + return nothing + end + rn1 = cong(heap_p, cong_unbias[tp]) + rn2 = cong(heap_p, cong_unbias[tp]) + prio1 = tpheaps[rn1].priority + prio2 = tpheaps[rn2].priority + if prio1 > prio2 + prio1 = prio2 + rn1 = rn2 + elseif prio1 == prio2 && prio1 == typemax(UInt16) + continue + end + if trylock(tpheaps[rn1].lock) + if prio1 == tpheaps[rn1].priority + break + end + unlock(tpheaps[rn1].lock) + end + end + + heap = tpheaps[rn1] + task = heap.tasks[1] + if ccall(:jl_set_task_tid, Cint, (Any, Cint), task, tid-1) == 0 + unlock(heap.lock) + @goto retry + end + ntasks = heap.ntasks + @atomic :monotonic heap.ntasks = ntasks - Int32(1) + heap.tasks[1] = heap.tasks[ntasks] + Base._unsetindex!(heap.tasks, Int(ntasks)) + prio1 = typemax(UInt16) + if ntasks > 1 + multiq_sift_down(heap, Int32(1)) + prio1 = heap.tasks[1].priority + end + @atomic :monotonic heap.priority = prio1 + unlock(heap.lock) + + return task +end + + +function multiq_check_empty() + for j = UInt32(1):length(heaps) + for i = UInt32(1):length(heaps[j]) + if heaps[j][i].ntasks != 0 + return false + end + end + end + return true +end + +end diff --git a/base/path.jl b/base/path.jl index 231f772923eee1..454fe5bd65d32f 100644 --- a/base/path.jl +++ b/base/path.jl @@ -36,7 +36,7 @@ elseif Sys.iswindows() function splitdrive(path::String) m = match(r"^([^\\]+:|\\\\[^\\]+\\[^\\]+|\\\\\?\\UNC\\[^\\]+\\[^\\]+|\\\\\?\\[^\\]+:|)(.*)$"s, path) - String(m.captures[1]), String(m.captures[2]) + String(something(m.captures[1])), String(something(m.captures[2])) end else error("path primitives for this OS need to be defined") @@ -72,7 +72,7 @@ function homedir() elseif rc == Base.UV_ENOBUFS resize!(buf, sz[] - 1) # space for null-terminator implied by StringVector else - uv_error(:homedir, rc) + uv_error("homedir()", rc) end end end @@ -137,8 +137,11 @@ _splitdir_nodrive(path::String) = _splitdir_nodrive("", path) function _splitdir_nodrive(a::String, b::String) m = match(path_dir_splitter,b) m === nothing && return (a,b) - a = string(a, isempty(m.captures[1]) ? m.captures[2][1] : m.captures[1]) - a, String(m.captures[3]) + cs = m.captures + getcapture(cs, i) = cs[i]::AbstractString + c1, c2, c3 = getcapture(cs, 1), getcapture(cs, 2), getcapture(cs, 3) + a = string(a, isempty(c1) ? c2[1] : c1) + a, String(c3) end """ @@ -156,7 +159,7 @@ julia> dirname("/home/myuser/") "/home/myuser" ``` -See also: [`basename`](@ref) +See also [`basename`](@ref). """ dirname(path::AbstractString) = splitdir(path)[1] @@ -165,21 +168,28 @@ See also: [`basename`](@ref) Get the file name part of a path. +!!! note + This function differs slightly from the Unix `basename` program, where trailing slashes are ignored, + i.e. `\$ basename /foo/bar/` returns `bar`, whereas `basename` in Julia returns an empty string `""`. + # Examples ```jldoctest julia> basename("/home/myuser/example.jl") "example.jl" + +julia> basename("/home/myuser/") +"" ``` -See also: [`dirname`](@ref) +See also [`dirname`](@ref). """ basename(path::AbstractString) = splitdir(path)[2] """ splitext(path::AbstractString) -> (AbstractString, AbstractString) -If the last component of a path contains a dot, split the path into everything before the -dot and everything including and after the dot. Otherwise, return a tuple of the argument +If the last component of a path contains one or more dots, split the path into everything before the +last dot and everything including and after the dot. Otherwise, return a tuple of the argument unmodified and the empty string. "splitext" is short for "split extension". # Examples @@ -187,15 +197,18 @@ unmodified and the empty string. "splitext" is short for "split extension". julia> splitext("/home/myuser/example.jl") ("/home/myuser/example", ".jl") -julia> splitext("/home/myuser/example") -("/home/myuser/example", "") +julia> splitext("/home/myuser/example.tar.gz") +("/home/myuser/example.tar", ".gz") + +julia> splitext("/home/my.user/example") +("/home/my.user/example", "") ``` """ function splitext(path::String) a, b = splitdrive(path) m = match(path_ext_splitter, b) m === nothing && return (path,"") - a*m.captures[1], String(m.captures[2]) + (a*something(m.captures[1])), String(something(m.captures[2])) end # NOTE: deprecated in 1.4 @@ -241,16 +254,19 @@ function splitpath(p::String) return out end -joinpath(path::AbstractString)::String = path - if Sys.iswindows() -function joinpath(path::AbstractString, paths::AbstractString...)::String - result_drive, result_path = splitdrive(path) +function joinpath(paths::Union{Tuple, AbstractVector})::String + assertstring(x) = x isa AbstractString || throw(ArgumentError("path component is not a string: $(repr(x))")) + + isempty(paths) && throw(ArgumentError("collection of path components must be non-empty")) + assertstring(paths[1]) + result_drive, result_path = splitdrive(paths[1]) - local p_drive, p_path - for p in paths - p_drive, p_path = splitdrive(p) + p_path = "" + for i in firstindex(paths)+1:lastindex(paths) + assertstring(paths[i]) + p_drive, p_path = splitdrive(paths[i]) if startswith(p_path, ('\\', '/')) # second path is absolute @@ -286,8 +302,15 @@ end else -function joinpath(path::AbstractString, paths::AbstractString...)::String - for p in paths +function joinpath(paths::Union{Tuple, AbstractVector})::String + assertstring(x) = x isa AbstractString || throw(ArgumentError("path component is not a string: $(repr(x))")) + + isempty(paths) && throw(ArgumentError("collection of path components must be non-empty")) + assertstring(paths[1]) + path = paths[1] + for i in firstindex(paths)+1:lastindex(paths) + p = paths[i] + assertstring(p) if isabspath(p) path = p elseif isempty(path) || path[end] == '/' @@ -301,8 +324,12 @@ end end # os-test +joinpath(paths::AbstractString...)::String = joinpath(paths) + """ joinpath(parts::AbstractString...) -> String + joinpath(parts::Vector{AbstractString}) -> String + joinpath(parts::Tuple{AbstractString}) -> String Join path components into a full path. If some argument is an absolute path or (on Windows) has a drive specification that doesn't match the drive computed for @@ -318,26 +345,35 @@ letter casing, hence `joinpath("C:\\A","c:b") = "C:\\A\\b"`. julia> joinpath("/home/myuser", "example.jl") "/home/myuser/example.jl" ``` + +```jldoctest +julia> joinpath(["/home/myuser", "example.jl"]) +"/home/myuser/example.jl" +``` """ joinpath """ normpath(path::AbstractString) -> String -Normalize a path, removing "." and ".." entries. +Normalize a path, removing "." and ".." entries and changing "/" to the canonical path separator +for the system. # Examples ```jldoctest julia> normpath("/home/myuser/../example.jl") "/home/example.jl" + +julia> normpath("Documents/Julia") == joinpath("Documents", "Julia") +true ``` """ function normpath(path::String) isabs = isabspath(path) isdir = isdirpath(path) drive, path = splitdrive(path) - parts = split(path, path_separator_re) - filter!(x->!isempty(x) && x!=".", parts) + parts = split(path, path_separator_re; keepempty=false) + filter!(!=("."), parts) while true clean = true for j = 1:length(parts)-1 @@ -380,7 +416,19 @@ normpath(a::AbstractString, b::AbstractString...) = normpath(joinpath(a,b...)) Convert a path to an absolute path by adding the current directory if necessary. Also normalizes the path as in [`normpath`](@ref). """ -abspath(a::String) = normpath(isabspath(a) ? a : joinpath(pwd(),a)) +function abspath(a::String)::String + if !isabspath(a) + cwd = pwd() + a_drive, a_nodrive = splitdrive(a) + if a_drive != "" && lowercase(splitdrive(cwd)[1]) != lowercase(a_drive) + cwd = a_drive * path_separator + a = joinpath(cwd, a_nodrive) + else + a = joinpath(cwd, a) + end + end + return normpath(a) +end """ abspath(path::AbstractString, paths::AbstractString...) -> String @@ -425,11 +473,11 @@ function realpath(path::AbstractString) (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Ptr{Cvoid}), C_NULL, req, path, C_NULL) if ret < 0 - ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req) - uv_error("realpath", ret) + uv_fs_req_cleanup(req) + uv_error("realpath($(repr(path)))", ret) end path = unsafe_string(ccall(:jl_uv_fs_t_ptr, Cstring, (Ptr{Cvoid},), req)) - ccall(:uv_fs_req_cleanup, Cvoid, (Ptr{Cvoid},), req) + uv_fs_req_cleanup(req) return path finally Libc.free(req) @@ -485,6 +533,9 @@ contractuser(path::AbstractString) Return a relative filepath to `path` either from the current directory or from an optional start directory. This is a path computation: the filesystem is not accessed to confirm the existence or nature of `path` or `startpath`. + +On Windows, case sensitivity is applied to every part of the path except drive letters. If +`path` and `startpath` refer to different drives, the absolute path of `path` is returned. """ function relpath(path::String, startpath::String = ".") isempty(path) && throw(ArgumentError("`path` must be specified")) @@ -492,8 +543,17 @@ function relpath(path::String, startpath::String = ".") curdir = "." pardir = ".." path == startpath && return curdir - path_arr = split(abspath(path), path_separator_re) - start_arr = split(abspath(startpath), path_separator_re) + if Sys.iswindows() + path_drive, path_without_drive = splitdrive(path) + startpath_drive, startpath_without_drive = splitdrive(startpath) + isempty(startpath_drive) && (startpath_drive = path_drive) # by default assume same as path drive + uppercase(path_drive) == uppercase(startpath_drive) || return abspath(path) # if drives differ return first path + path_arr = split(abspath(path_drive * path_without_drive), path_separator_re) + start_arr = split(abspath(path_drive * startpath_without_drive), path_separator_re) + else + path_arr = split(abspath(path), path_separator_re) + start_arr = split(abspath(startpath), path_separator_re) + end i = 0 while i < min(length(path_arr), length(start_arr)) i += 1 diff --git a/base/pcre.jl b/base/pcre.jl index 1508eb90b19931..d689e9be29113a 100644 --- a/base/pcre.jl +++ b/base/pcre.jl @@ -24,69 +24,106 @@ function create_match_context() return ctx end -const THREAD_MATCH_CONTEXTS = Ptr{Cvoid}[C_NULL] +THREAD_MATCH_CONTEXTS::Vector{Ptr{Cvoid}} = [C_NULL] PCRE_COMPILE_LOCK = nothing -_tid() = Int(ccall(:jl_threadid, Int16, ())+1) +_tid() = Int(ccall(:jl_threadid, Int16, ())) + 1 _nth() = Int(unsafe_load(cglobal(:jl_n_threads, Cint))) function get_local_match_context() tid = _tid() - ctx = @inbounds THREAD_MATCH_CONTEXTS[tid] + ctxs = THREAD_MATCH_CONTEXTS + if length(ctxs) < tid + # slow path to allocate it + l = PCRE_COMPILE_LOCK::Threads.SpinLock + lock(l) + try + ctxs = THREAD_MATCH_CONTEXTS + if length(ctxs) < tid + global THREAD_MATCH_CONTEXTS = ctxs = copyto!(fill(C_NULL, _nth()), ctxs) + end + finally + unlock(l) + end + end + ctx = @inbounds ctxs[tid] if ctx == C_NULL - @inbounds THREAD_MATCH_CONTEXTS[tid] = ctx = create_match_context() + # slow path to allocate it + ctx = create_match_context() + THREAD_MATCH_CONTEXTS[tid] = ctx end return ctx end -function __init__() - resize!(THREAD_MATCH_CONTEXTS, _nth()) - fill!(THREAD_MATCH_CONTEXTS, C_NULL) - global PCRE_COMPILE_LOCK = Threads.SpinLock() -end - # supported options for different use cases +# arguments to pcre2_compile const COMPILE_MASK = + ALT_BSUX | + ALT_CIRCUMFLEX | + ALT_VERBNAMES | ANCHORED | + # AUTO_CALLOUT | CASELESS | DOLLAR_ENDONLY | DOTALL | + # DUPNAMES | ENDANCHORED | EXTENDED | + EXTENDED_MORE | FIRSTLINE | + LITERAL | + MATCH_INVALID_UTF | + MATCH_UNSET_BACKREF | MULTILINE | - NEWLINE_ANY | - NEWLINE_ANYCRLF | - NEWLINE_CR | - NEWLINE_CRLF | - NEWLINE_LF | + NEVER_BACKSLASH_C | + NEVER_UCP | + NEVER_UTF | NO_AUTO_CAPTURE | + NO_AUTO_POSSESS | + NO_DOTSTAR_ANCHOR | NO_START_OPTIMIZE | NO_UTF_CHECK | + UCP | UNGREEDY | - UTF | - UCP - + USE_OFFSET_LIMIT | + UTF + +# arguments to pcre2_set_newline +const COMPILE_NEWLINE_MASK = ( + NEWLINE_CR, + NEWLINE_LF, + NEWLINE_CRLF, + NEWLINE_ANY, + NEWLINE_ANYCRLF, + NEWLINE_NUL) + +# arguments to pcre2_set_compile_extra_options +const COMPILE_EXTRA_MASK = + EXTRA_ALLOW_SURROGATE_ESCAPES | + EXTRA_ALT_BSUX | + EXTRA_BAD_ESCAPE_IS_LITERAL | + EXTRA_ESCAPED_CR_IS_LF | + EXTRA_MATCH_LINE | + EXTRA_MATCH_WORD + +# arguments to match const EXECUTE_MASK = - NEWLINE_ANY | - NEWLINE_ANYCRLF | - NEWLINE_CR | - NEWLINE_CRLF | - NEWLINE_LF | + # ANCHORED | + # COPY_MATCHED_SUBJECT | + # ENDANCHORED | NOTBOL | NOTEMPTY | NOTEMPTY_ATSTART | NOTEOL | + # NO_JIT | NO_START_OPTIMIZE | NO_UTF_CHECK | PARTIAL_HARD | PARTIAL_SOFT -const OPTIONS_MASK = COMPILE_MASK | EXECUTE_MASK - const UNSET = ~Csize_t(0) # Indicates that an output vector element is unset function info(regex::Ptr{Cvoid}, what::Integer, ::Type{T}) where T @@ -201,7 +238,10 @@ function substring_length_bynumber(match_data, number) s = RefValue{Csize_t}() rc = ccall((:pcre2_substring_length_bynumber_8, PCRE_LIB), Cint, (Ptr{Cvoid}, Cint, Ref{Csize_t}), match_data, number, s) - rc < 0 && error("PCRE error: $(err_message(rc))") + if rc < 0 + rc == ERROR_UNSET && return 0 + error("PCRE error: $(err_message(rc))") + end return Int(s[]) end diff --git a/base/permuteddimsarray.jl b/base/permuteddimsarray.jl index d951daa1252738..ea966c44efc38b 100644 --- a/base/permuteddimsarray.jl +++ b/base/permuteddimsarray.jl @@ -24,7 +24,7 @@ Given an AbstractArray `A`, create a view `B` such that the dimensions appear to be permuted. Similar to `permutedims`, except that no copying occurs (`B` shares storage with `A`). -See also: [`permutedims`](@ref). +See also [`permutedims`](@ref), [`invperm`](@ref). # Examples ```jldoctest @@ -83,10 +83,10 @@ end """ permutedims(A::AbstractArray, perm) -Permute the dimensions of array `A`. `perm` is a vector specifying a permutation of length -`ndims(A)`. +Permute the dimensions of array `A`. `perm` is a vector or a tuple of length `ndims(A)` +specifying the permutation. -See also: [`PermutedDimsArray`](@ref). +See also [`permutedims!`](@ref), [`PermutedDimsArray`](@ref), [`transpose`](@ref), [`invperm`](@ref). # Examples ```jldoctest @@ -100,7 +100,7 @@ julia> A = reshape(Vector(1:8), (2,2,2)) 5 7 6 8 -julia> permutedims(A, [3, 2, 1]) +julia> permutedims(A, (3, 2, 1)) 2×2×2 Array{Int64, 3}: [:, :, 1] = 1 3 @@ -109,6 +109,16 @@ julia> permutedims(A, [3, 2, 1]) [:, :, 2] = 2 4 6 8 + +julia> B = randn(5, 7, 11, 13); + +julia> perm = [4,1,3,2]; + +julia> size(permutedims(B, perm)) +(13, 5, 11, 7) + +julia> size(B)[perm] == ans +true ``` """ function permutedims(A::AbstractArray, perm) @@ -144,7 +154,7 @@ julia> permutedims(X) [5 6; 7 8] [13 14; 15 16] julia> transpose(X) -2×2 Transpose{Transpose{Int64, Matrix{Int64}}, Matrix{Matrix{Int64}}}: +2×2 transpose(::Matrix{Matrix{Int64}}) with eltype Transpose{Int64, Matrix{Int64}}: [1 3; 2 4] [9 11; 10 12] [5 7; 6 8] [13 15; 14 16] ``` @@ -174,7 +184,7 @@ julia> permutedims(V) [1 2; 3 4] [5 6; 7 8] julia> transpose(V) -1×2 Transpose{Transpose{Int64, Matrix{Int64}}, Vector{Matrix{Int64}}}: +1×2 transpose(::Vector{Matrix{Int64}}) with eltype Transpose{Int64, Matrix{Int64}}: [1 3; 2 4] [5 7; 6 8] ``` """ @@ -253,11 +263,22 @@ end P end +function Base._mapreduce_dim(f, op, init::Base._InitialValue, A::PermutedDimsArray, dims::Colon) + Base._mapreduce_dim(f, op, init, parent(A), dims) +end + +function Base.mapreducedim!(f, op, B::AbstractArray{T,N}, A::PermutedDimsArray{T,N,perm,iperm}) where {T,N,perm,iperm} + C = PermutedDimsArray{T,N,iperm,perm,typeof(B)}(B) # make the inverse permutation for the output + Base.mapreducedim!(f, op, C, parent(A)) + B +end + function Base.showarg(io::IO, A::PermutedDimsArray{T,N,perm}, toplevel) where {T,N,perm} print(io, "PermutedDimsArray(") Base.showarg(io, parent(A), false) print(io, ", ", perm, ')') toplevel && print(io, " with eltype ", eltype(A)) + return nothing end end diff --git a/base/pkgid.jl b/base/pkgid.jl index 6d588ffe6647dd..20d9de559b3341 100644 --- a/base/pkgid.jl +++ b/base/pkgid.jl @@ -1,3 +1,5 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + struct PkgId uuid::Union{UUID,Nothing} name::String @@ -40,4 +42,3 @@ function binunpack(s::String) name = read(io, String) return PkgId(UUID(uuid), name) end - diff --git a/base/pointer.jl b/base/pointer.jl index 0813d0a0c97350..60db18f2ca8555 100644 --- a/base/pointer.jl +++ b/base/pointer.jl @@ -77,7 +77,10 @@ element type. `dims` is either an integer (for a 1d array) or a tuple of the arr calling `free` on the pointer when the array is no longer referenced. This function is labeled "unsafe" because it will crash if `pointer` is not -a valid memory address to data of the requested length. +a valid memory address to data of the requested length. Unlike [`unsafe_load`](@ref) +and [`unsafe_store!`](@ref), the programmer is responsible also for ensuring that the +underlying data is not accessed through two arrays of different element type, similar +to the strict aliasing rule in C. """ function unsafe_wrap(::Union{Type{Array},Type{Array{T}},Type{Array{T,N}}}, p::Ptr{T}, dims::NTuple{N,Int}; own::Bool = false) where {T,N} @@ -99,8 +102,10 @@ Load a value of type `T` from the address of the `i`th element (1-indexed) start This is equivalent to the C expression `p[i-1]`. The `unsafe` prefix on this function indicates that no validation is performed on the -pointer `p` to ensure that it is valid. Incorrect usage may segfault your program or return -garbage answers, in the same manner as C. +pointer `p` to ensure that it is valid. Like C, the programmer is responsible for ensuring +that referenced memory is not freed or garbage collected while invoking this function. +Incorrect usage may segfault your program or return garbage answers. Unlike C, dereferencing +memory region allocated as different type may be valid provided that the types are compatible. """ unsafe_load(p::Ptr, i::Integer=1) = pointerref(p, Int(i), 1) @@ -111,8 +116,10 @@ Store a value of type `T` to the address of the `i`th element (1-indexed) starti This is equivalent to the C expression `p[i-1] = x`. The `unsafe` prefix on this function indicates that no validation is performed on the -pointer `p` to ensure that it is valid. Incorrect usage may corrupt or segfault your -program, in the same manner as C. +pointer `p` to ensure that it is valid. Like C, the programmer is responsible for ensuring +that referenced memory is not freed or garbage collected while invoking this function. +Incorrect usage may segfault your program. Unlike C, storing memory region allocated as +different type may be valid provided that that the types are compatible. """ unsafe_store!(p::Ptr{Any}, @nospecialize(x), i::Integer=1) = pointerset(p, x, Int(i), 1) unsafe_store!(p::Ptr{T}, x, i::Integer=1) where {T} = pointerset(p, convert(T,x), Int(i), 1) @@ -125,7 +132,7 @@ Convert a `Ptr` to an object reference. Assumes the pointer refers to a valid he Julia object. If this is not the case, undefined behavior results, hence this function is considered "unsafe" and should be used with care. -See also: [`pointer_from_objref`](@ref). +See also [`pointer_from_objref`](@ref). """ unsafe_pointer_to_objref(x::Ptr) = ccall(:jl_value_ptr, Any, (Ptr{Cvoid},), x) @@ -139,11 +146,11 @@ remains referenced for the whole time that the `Ptr` will be used. This function may not be called on immutable objects, since they do not have stable memory addresses. -See also: [`unsafe_pointer_to_objref`](@ref). +See also [`unsafe_pointer_to_objref`](@ref). """ function pointer_from_objref(@nospecialize(x)) - @_inline_meta - typeof(x).mutable || error("pointer_from_objref cannot be used on immutable objects") + @inline + ismutable(x) || error("pointer_from_objref cannot be used on immutable objects") ccall(:jl_value_ptr, Ptr{Cvoid}, (Any,), x) end diff --git a/base/process.jl b/base/process.jl index 302387ce5b3a6d..aa378e72b2dce2 100644 --- a/base/process.jl +++ b/base/process.jl @@ -38,9 +38,13 @@ pipe_writer(p::ProcessChain) = p.in # release ownership of the libuv handle function uvfinalize(proc::Process) if proc.handle != C_NULL - disassociate_julia_struct(proc.handle) - ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), proc.handle) - proc.handle = C_NULL + iolock_begin() + if proc.handle != C_NULL + disassociate_julia_struct(proc.handle) + ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), proc.handle) + proc.handle = C_NULL + end + iolock_end() end nothing end @@ -52,6 +56,7 @@ function uv_return_spawn(p::Ptr{Cvoid}, exit_status::Int64, termsignal::Int32) proc = unsafe_pointer_to_objref(data)::Process proc.exitcode = exit_status proc.termsignal = termsignal + disassociate_julia_struct(proc.handle) # ensure that data field is set to C_NULL ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), proc.handle) proc.handle = C_NULL lock(proc.exitnotify) @@ -65,45 +70,67 @@ end # called when the libuv handle is destroyed function _uv_hook_close(proc::Process) - proc.handle = C_NULL + Libc.free(@atomicswap :not_atomic proc.handle = C_NULL) nothing end -const SpawnIOs = Vector{Any} # convenience name for readability +const SpawnIO = Union{IO, RawFD, OS_HANDLE} +const SpawnIOs = Vector{SpawnIO} # convenience name for readability + +function as_cpumask(cpus::Vector{UInt16}) + n = max(Int(maximum(cpus)), Int(ccall(:uv_cpumask_size, Cint, ()))) + cpumask = zeros(Bool, n) + for i in cpus + cpumask[i] = true + end + return cpumask +end # handle marshalling of `Cmd` arguments from Julia to C @noinline function _spawn_primitive(file, cmd::Cmd, stdio::SpawnIOs) loop = eventloop() - iohandles = Tuple{Cint, UInt}[ # assuming little-endian layout - let h = rawhandle(io) - h === C_NULL ? (0x00, UInt(0)) : - h isa OS_HANDLE ? (0x02, UInt(cconvert(@static(Sys.iswindows() ? Ptr{Cvoid} : Cint), h))) : - h isa Ptr{Cvoid} ? (0x04, UInt(h)) : - error("invalid spawn handle $h from $io") + cpumask = cmd.cpus + cpumask === nothing || (cpumask = as_cpumask(cpumask)) + GC.@preserve stdio begin + iohandles = Tuple{Cint, UInt}[ # assuming little-endian layout + let h = rawhandle(io) + h === C_NULL ? (0x00, UInt(0)) : + h isa OS_HANDLE ? (0x02, UInt(cconvert(@static(Sys.iswindows() ? Ptr{Cvoid} : Cint), h))) : + h isa Ptr{Cvoid} ? (0x04, UInt(h)) : + error("invalid spawn handle $h from $io") + end + for io in stdio] + handle = Libc.malloc(_sizeof_uv_process) + disassociate_julia_struct(handle) + (; exec, flags, env, dir) = cmd + iolock_begin() + err = ccall(:jl_spawn, Int32, + (Cstring, Ptr{Cstring}, Ptr{Cvoid}, Ptr{Cvoid}, + Ptr{Tuple{Cint, UInt}}, Int, + UInt32, Ptr{Cstring}, Cstring, Ptr{Bool}, Csize_t, Ptr{Cvoid}), + file, exec, loop, handle, + iohandles, length(iohandles), + flags, + env === nothing ? C_NULL : env, + isempty(dir) ? C_NULL : dir, + cpumask === nothing ? C_NULL : cpumask, + cpumask === nothing ? 0 : length(cpumask), + @cfunction(uv_return_spawn, Cvoid, (Ptr{Cvoid}, Int64, Int32))) + if err == 0 + pp = Process(cmd, handle) + associate_julia_struct(handle, pp) + else + ccall(:jl_forceclose_uv, Cvoid, (Ptr{Cvoid},), handle) # will call free on handle eventually end - for io in stdio] - handle = Libc.malloc(_sizeof_uv_process) - disassociate_julia_struct(handle) # ensure that data field is set to C_NULL - err = ccall(:jl_spawn, Int32, - (Cstring, Ptr{Cstring}, Ptr{Cvoid}, Ptr{Cvoid}, - Ptr{Tuple{Cint, UInt}}, Int, - UInt32, Ptr{Cstring}, Cstring, Ptr{Cvoid}), - file, cmd.exec, loop, handle, - iohandles, length(iohandles), - cmd.flags, - cmd.env === nothing ? C_NULL : cmd.env, - isempty(cmd.dir) ? C_NULL : cmd.dir, - @cfunction(uv_return_spawn, Cvoid, (Ptr{Cvoid}, Int64, Int32))) + iolock_end() + end if err != 0 - ccall(:jl_forceclose_uv, Cvoid, (Ptr{Cvoid},), handle) # will call free on handle eventually throw(_UVError("could not spawn " * repr(cmd), err)) end - pp = Process(cmd, handle) - associate_julia_struct(handle, pp) return pp end -_spawn(cmds::AbstractCmd) = _spawn(cmds, Any[]) +_spawn(cmds::AbstractCmd) = _spawn(cmds, SpawnIO[]) # optimization: we can spawn `Cmd` directly without allocating the ProcessChain function _spawn(cmd::Cmd, stdios::SpawnIOs) @@ -187,7 +214,7 @@ end # open the child end of each element of `stdios`, and initialize the parent end function setup_stdios(f, stdios::SpawnIOs) nstdio = length(stdios) - open_io = Vector{Any}(undef, nstdio) + open_io = SpawnIOs(undef, nstdio) close_io = falses(nstdio) try for i in 1:nstdio @@ -209,10 +236,10 @@ function setup_stdio(stdio::PipeEndpoint, child_readable::Bool) rd, wr = link_pipe(!child_readable, child_readable) try open_pipe!(stdio, child_readable ? wr : rd) - catch ex + catch close_pipe_sync(rd) close_pipe_sync(wr) - rethrow(ex) + rethrow() end child = child_readable ? rd : wr return (child, true) @@ -251,18 +278,19 @@ function setup_stdio(stdio::FileRedirect, child_readable::Bool) return (io, true) end -# incrementally move data between an IOBuffer and a system Pipe +# incrementally move data between an arbitrary IO and a system Pipe, +# including copying the EOF (shutdown) when finished # TODO: probably more efficient (when valid) to use `stdio` directly as the # PipeEndpoint buffer field in some cases -function setup_stdio(stdio::Union{IOBuffer, BufferStream}, child_readable::Bool) +function setup_stdio(stdio::IO, child_readable::Bool) parent = PipeEndpoint() rd, wr = link_pipe(!child_readable, child_readable) try open_pipe!(parent, child_readable ? wr : rd) - catch ex + catch close_pipe_sync(rd) close_pipe_sync(wr) - rethrow(ex) + rethrow() end child = child_readable ? rd : wr try @@ -271,24 +299,19 @@ function setup_stdio(stdio::Union{IOBuffer, BufferStream}, child_readable::Bool) @async try write(in, out) catch ex - @warn "Process error" exception=(ex, catch_backtrace()) + @warn "Process I/O error" exception=(ex, catch_backtrace()) finally close(parent) + child_readable || closewrite(stdio) end end - catch ex + catch close_pipe_sync(child) - rethrow(ex) + rethrow() end return (child, true) end -function setup_stdio(io, child_readable::Bool) - # if there is no specialization, - # assume that rawhandle is defined for it - return (io, false) -end - close_stdio(stdio::OS_HANDLE) = close_pipe_sync(stdio) close_stdio(stdio) = close(stdio) @@ -302,19 +325,19 @@ close_stdio(stdio) = close(stdio) # - An Filesystem.File or IOStream object to redirect the output to # - A FileRedirect, containing a string specifying a filename to be opened for the child -spawn_opts_swallow(stdios::StdIOSet) = Any[stdios...] -spawn_opts_inherit(stdios::StdIOSet) = Any[stdios...] +spawn_opts_swallow(stdios::StdIOSet) = SpawnIO[stdios...] +spawn_opts_inherit(stdios::StdIOSet) = SpawnIO[stdios...] spawn_opts_swallow(in::Redirectable=devnull, out::Redirectable=devnull, err::Redirectable=devnull) = - Any[in, out, err] + SpawnIO[in, out, err] # pass original descriptors to child processes by default, because we might # have already exhausted and closed the libuv object for our standard streams. # ref issue #8529 spawn_opts_inherit(in::Redirectable=RawFD(0), out::Redirectable=RawFD(1), err::Redirectable=RawFD(2)) = - Any[in, out, err] + SpawnIO[in, out, err] function eachline(cmd::AbstractCmd; keep::Bool=false) out = PipeEndpoint() - processes = _spawn(cmd, Any[devnull, out, stderr]) + processes = _spawn(cmd, SpawnIO[devnull, out, stderr]) # if the user consumes all the data, also check process exit status for success ondone = () -> (success(processes) || pipeline_error(processes); nothing) return EachLine(out, keep=keep, ondone=ondone)::EachLine @@ -362,20 +385,20 @@ function open(cmds::AbstractCmd, stdio::Redirectable=devnull; write::Bool=false, stdio === devnull || throw(ArgumentError("no stream can be specified for `stdio` in read-write mode")) in = PipeEndpoint() out = PipeEndpoint() - processes = _spawn(cmds, Any[in, out, stderr]) + processes = _spawn(cmds, SpawnIO[in, out, stderr]) processes.in = in processes.out = out elseif read out = PipeEndpoint() - processes = _spawn(cmds, Any[stdio, out, stderr]) + processes = _spawn(cmds, SpawnIO[stdio, out, stderr]) processes.out = out elseif write in = PipeEndpoint() - processes = _spawn(cmds, Any[in, stdio, stderr]) + processes = _spawn(cmds, SpawnIO[in, stdio, stderr]) processes.in = in else stdio === devnull || throw(ArgumentError("no stream can be specified for `stdio` in no-access mode")) - processes = _spawn(cmds, Any[devnull, devnull, stderr]) + processes = _spawn(cmds, SpawnIO[devnull, devnull, stderr]) end return processes end @@ -383,19 +406,33 @@ end """ open(f::Function, command, args...; kwargs...) -Similar to `open(command, args...; kwargs...)`, but calls `f(stream)` on the resulting process -stream, then closes the input stream and waits for the process to complete. -Returns the value returned by `f`. +Similar to `open(command, args...; kwargs...)`, but calls `f(stream)` on the +resulting process stream, then closes the input stream and waits for the process +to complete. Return the value returned by `f` on success. Throw an error if the +process failed, or if the process attempts to print anything to stdout. """ function open(f::Function, cmds::AbstractCmd, args...; kwargs...) P = open(cmds, args...; kwargs...) + function waitkill(P::Process) + close(P) + # 0.1 seconds after we hope it dies (from closing stdio), + # we kill the process with SIGTERM (15) + local t = Timer(0.1) do t + process_running(P) && kill(P) + end + wait(P) + close(t) + end ret = try f(P) catch - kill(P) + waitkill(P) rethrow() - finally - close(P.in) + end + close(P.in) + if !eof(P.out) + waitkill(P) + throw(_UVError("open(do)", UV_EPIPE)) end success(P) || pipeline_error(P) return ret @@ -418,7 +455,7 @@ end Run `command` and return the resulting output as a `String`. """ -read(cmd::AbstractCmd, ::Type{String}) = String(read(cmd)) +read(cmd::AbstractCmd, ::Type{String}) = String(read(cmd))::String """ run(command, args...; wait::Bool = true) @@ -476,7 +513,7 @@ function test_success(proc::Process) #TODO: this codepath is not currently tested throw(_UVError("could not start process " * repr(proc.cmd), proc.exitcode)) end - return proc.exitcode == 0 && (proc.termsignal == 0 || proc.termsignal == SIGPIPE) + return proc.exitcode == 0 && proc.termsignal == 0 end function success(x::Process) @@ -570,10 +607,10 @@ Get the child process ID, if it still exists. This function requires at least Julia 1.1. """ function Libc.getpid(p::Process) - # TODO: due to threading, this method is no longer synchronized with the user application + # TODO: due to threading, this method is only weakly synchronized with the user application iolock_begin() ppid = Int32(0) - if p.handle != C_NULL + if p.handle != C_NULL # e.g. process_running ppid = ccall(:jl_uv_process_pid, Int32, (Ptr{Cvoid},), p.handle) end iolock_end() @@ -637,6 +674,7 @@ show(io::IO, p::Process) = print(io, "Process(", p.cmd, ", ", process_status(p), for f in (:length, :firstindex, :lastindex, :keys, :first, :last, :iterate) @eval $f(cmd::Cmd) = $f(cmd.exec) end +Iterators.reverse(cmd::Cmd) = Iterators.reverse(cmd.exec) eltype(::Type{Cmd}) = eltype(fieldtype(Cmd, :exec)) for f in (:iterate, :getindex) @eval $f(cmd::Cmd, i) = $f(cmd.exec, i) diff --git a/base/promotion.jl b/base/promotion.jl index 1b9c8c882324a7..8e05a86b8b7630 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -5,15 +5,14 @@ """ typejoin(T, S) - Return the closest common ancestor of `T` and `S`, i.e. the narrowest type from which they both inherit. """ typejoin() = Bottom typejoin(@nospecialize(t)) = t -typejoin(@nospecialize(t), ts...) = (@_pure_meta; typejoin(t, typejoin(ts...))) +typejoin(@nospecialize(t), ts...) = (@_total_meta; typejoin(t, typejoin(ts...))) function typejoin(@nospecialize(a), @nospecialize(b)) - @_pure_meta + @_total_meta if isa(a, TypeVar) return typejoin(a.ub, b) elseif isa(b, TypeVar) @@ -30,11 +29,15 @@ function typejoin(@nospecialize(a), @nospecialize(b)) return typejoin(typejoin(a.a, a.b), b) elseif isa(b, Union) return typejoin(a, typejoin(b.a, b.b)) - elseif a <: Tuple + end + # a and b are DataTypes + # We have to hide Constant info from inference, see #44390 + a, b = inferencebarrier(a)::DataType, inferencebarrier(b)::DataType + if a <: Tuple if !(b <: Tuple) return Any end - ap, bp = a.parameters::Core.SimpleVector, b.parameters::Core.SimpleVector + ap, bp = a.parameters, b.parameters lar = length(ap) lbr = length(bp) if lar == 0 @@ -78,7 +81,6 @@ function typejoin(@nospecialize(a), @nospecialize(b)) elseif b <: Tuple return Any end - a, b = a::DataType, b::DataType while b !== Any if a <: b.name.wrapper while a.name !== b.name @@ -121,32 +123,104 @@ function typejoin(@nospecialize(a), @nospecialize(b)) return Any end +# return an upper-bound on type `a` with type `b` removed +# such that `return <: a` && `Union{return, b} == Union{a, b}` +# WARNING: this is wrong for some objects for which subtyping is broken +# (Core.Compiler.isnotbrokensubtype), use only simple types for `b` +function typesplit(@nospecialize(a), @nospecialize(b)) + @_total_may_throw_meta + if a <: b + return Bottom + end + if isa(a, Union) + return Union{typesplit(a.a, b), + typesplit(a.b, b)} + end + return a +end + + """ promote_typejoin(T, S) Compute a type that contains both `T` and `S`, which could be either a parent of both types, or a `Union` if appropriate. Falls back to [`typejoin`](@ref). + +See instead [`promote`](@ref), [`promote_type`](@ref). + +# Examples +```jldoctest +julia> Base.promote_typejoin(Int, Float64) +Real + +julia> Base.promote_type(Int, Float64) +Float64 +``` """ function promote_typejoin(@nospecialize(a), @nospecialize(b)) c = typejoin(_promote_typesubtract(a), _promote_typesubtract(b)) return Union{a, b, c}::Type end -_promote_typesubtract(@nospecialize(a)) = Core.Compiler.typesubtract(a, Union{Nothing, Missing}) +_promote_typesubtract(@nospecialize(a)) = typesplit(a, Union{Nothing, Missing}) + +function promote_typejoin_union(::Type{T}) where T + if T === Union{} + return Union{} + elseif T isa UnionAll + return Any # TODO: compute more precise bounds + elseif T isa Union + return promote_typejoin(promote_typejoin_union(T.a), promote_typejoin_union(T.b)) + elseif T isa DataType + T <: Tuple && return typejoin_union_tuple(T) + return T + else + error("unreachable") # not a type?? + end +end +function typejoin_union_tuple(T::DataType) + @_total_may_throw_meta + u = Base.unwrap_unionall(T) + p = (u::DataType).parameters + lr = length(p)::Int + if lr == 0 + return Tuple{} + end + c = Vector{Any}(undef, lr) + for i = 1:lr + pi = p[i] + U = Core.Compiler.unwrapva(pi) + if U === Union{} + ci = Union{} + elseif U isa Union + ci = typejoin(U.a, U.b) + elseif U isa UnionAll + return Any # TODO: compute more precise bounds + else + ci = promote_typejoin_union(U) + end + if i == lr && Core.Compiler.isvarargtype(pi) + c[i] = isdefined(pi, :N) ? Vararg{ci, pi.N} : Vararg{ci} + else + c[i] = ci + end + end + return Base.rewrap_unionall(Tuple{c...}, T) +end # Returns length, isfixed -function full_va_len(p) +function full_va_len(p::Core.SimpleVector) isempty(p) && return 0, true last = p[end] if isvarargtype(last) - N = unwrap_unionall(last).parameters[2] - if isa(N, Int) - return length(p)::Int + N - 1, true + if isdefined(last, :N) + N = last.N + isa(N, Int) && return length(p) + N - 1, true end - return length(p)::Int, false + return length(p), false end - return length(p)::Int, true + return length(p), true end # reduce typejoin over A[i:end] @@ -164,7 +238,7 @@ end ## promotion mechanism ## """ - promote_type(type1, type2) + promote_type(type1, type2, ...) Promotion refers to converting values of mixed types to a single common type. `promote_type` represents the default promotion behavior in Julia when @@ -175,6 +249,9 @@ tolerated; for example, `promote_type(Int64, Float64)` returns [`Float64`](@ref) even though strictly, not all [`Int64`](@ref) values can be represented exactly as `Float64` values. +See also: [`promote`](@ref), [`promote_typejoin`](@ref), [`promote_rule`](@ref). + +# Examples ```jldoctest julia> promote_type(Int64, Float64) Float64 @@ -194,12 +271,17 @@ Float16 julia> promote_type(Int8, UInt16) UInt16 ``` + +!!! warning "Don't overload this directly" + To overload promotion for your own types you should overload [`promote_rule`](@ref). + `promote_type` calls `promote_rule` internally to determine the type. + Overloading `promote_type` directly can cause ambiguity errors. """ function promote_type end promote_type() = Bottom promote_type(T) = T -promote_type(T, S, U, V...) = (@_inline_meta; promote_type(T, promote_type(S, U, V...))) +promote_type(T, S, U, V...) = (@inline; promote_type(T, promote_type(S, U, V...))) promote_type(::Type{Bottom}, ::Type{Bottom}) = Bottom promote_type(::Type{T}, ::Type{T}) where {T} = T @@ -207,7 +289,7 @@ promote_type(::Type{T}, ::Type{Bottom}) where {T} = T promote_type(::Type{Bottom}, ::Type{T}) where {T} = T function promote_type(::Type{T}, ::Type{S}) where {T,S} - @_inline_meta + @inline # Try promote_rule in both orders. Typically only one is defined, # and there is a fallback returning Bottom below, so the common case is # promote_type(T, S) => @@ -225,12 +307,12 @@ it for new types as appropriate. """ function promote_rule end -promote_rule(::Type{<:Any}, ::Type{<:Any}) = Bottom +promote_rule(::Type, ::Type) = Bottom -promote_result(::Type{<:Any},::Type{<:Any},::Type{T},::Type{S}) where {T,S} = (@_inline_meta; promote_type(T,S)) +promote_result(::Type,::Type,::Type{T},::Type{S}) where {T,S} = (@inline; promote_type(T,S)) # If no promote_rule is defined, both directions give Bottom. In that # case use typejoin on the original types instead. -promote_result(::Type{T},::Type{S},::Type{Bottom},::Type{Bottom}) where {T,S} = (@_inline_meta; typejoin(T, S)) +promote_result(::Type{T},::Type{S},::Type{Bottom},::Type{Bottom}) where {T,S} = (@inline; typejoin(T, S)) """ promote(xs...) @@ -238,6 +320,8 @@ promote_result(::Type{T},::Type{S},::Type{Bottom},::Type{Bottom}) where {T,S} = Convert all arguments to a common type, and return them all (as a tuple). If no arguments can be converted, an error is raised. +See also: [`promote_type`], [`promote_rule`]. + # Examples ```jldoctest julia> promote(Int8(1), Float16(4.5), Float32(4.1)) @@ -247,19 +331,19 @@ julia> promote(Int8(1), Float16(4.5), Float32(4.1)) function promote end function _promote(x::T, y::S) where {T,S} - @_inline_meta + @inline R = promote_type(T, S) return (convert(R, x), convert(R, y)) end promote_typeof(x) = typeof(x) -promote_typeof(x, xs...) = (@_inline_meta; promote_type(typeof(x), promote_typeof(xs...))) +promote_typeof(x, xs...) = (@inline; promote_type(typeof(x), promote_typeof(xs...))) function _promote(x, y, z) - @_inline_meta + @inline R = promote_typeof(x, y, z) return (convert(R, x), convert(R, y), convert(R, z)) end function _promote(x, y, zs...) - @_inline_meta + @inline R = promote_typeof(x, y, zs...) return (convert(R, x), convert(R, y), convert(Tuple{Vararg{R}}, zs)...) end @@ -271,13 +355,13 @@ promote() = () promote(x) = (x,) function promote(x, y) - @_inline_meta + @inline px, py = _promote(x, y) not_sametype((x,y), (px,py)) px, py end function promote(x, y, z) - @_inline_meta + @inline px, py, pz = _promote(x, y, z) not_sametype((x,y,z), (px,py,pz)) px, py, pz @@ -295,7 +379,7 @@ not_sametype(x::T, y::T) where {T} = sametype_error(x) not_sametype(x, y) = nothing function sametype_error(input) - @_noinline_meta + @noinline error("promotion of types ", join(map(x->string(typeof(x)), input), ", ", " and "), " failed to change any arguments") @@ -316,8 +400,10 @@ If `y` is an `Int` literal (e.g. `2` in `x^2` or `-3` in `x^-3`), the Julia code enable compile-time specialization on the value of the exponent. (As a default fallback we have `Base.literal_pow(^, x, Val(y)) = ^(x,y)`, where usually `^ == Base.^` unless `^` has been defined in the calling -namespace.) +namespace.) If `y` is a negative integer literal, then `Base.literal_pow` +transforms the operation to `inv(x)^-y` by default, where `-y` is positive. +# Examples ```jldoctest julia> 3^5 243 diff --git a/base/range.jl b/base/range.jl index 8f1a1443b22fc9..23735aaa87f1c7 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1,21 +1,21 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -(:)(a::Real, b::Real) = (:)(promote(a,b)...) +(:)(a::Real, b::Real) = (:)(promote(a, b)...) (:)(start::T, stop::T) where {T<:Real} = UnitRange{T}(start, stop) -(:)(start::T, stop::T) where {T} = (:)(start, oftype(stop-start, 1), stop) +(:)(start::T, stop::T) where {T} = (:)(start, oftype(stop >= start ? stop - start : start - stop, 1), stop) # promote start and stop, leaving step alone -(:)(start::A, step, stop::C) where {A<:Real,C<:Real} = - (:)(convert(promote_type(A,C),start), step, convert(promote_type(A,C),stop)) +(:)(start::A, step, stop::C) where {A<:Real, C<:Real} = + (:)(convert(promote_type(A, C), start), step, convert(promote_type(A, C), stop)) # AbstractFloat specializations (:)(a::T, b::T) where {T<:AbstractFloat} = (:)(a, T(1), b) -(:)(a::T, b::AbstractFloat, c::T) where {T<:Real} = (:)(promote(a,b,c)...) -(:)(a::T, b::AbstractFloat, c::T) where {T<:AbstractFloat} = (:)(promote(a,b,c)...) -(:)(a::T, b::Real, c::T) where {T<:AbstractFloat} = (:)(promote(a,b,c)...) +(:)(a::T, b::AbstractFloat, c::T) where {T<:Real} = (:)(promote(a, b, c)...) +(:)(a::T, b::AbstractFloat, c::T) where {T<:AbstractFloat} = (:)(promote(a, b, c)...) +(:)(a::T, b::Real, c::T) where {T<:AbstractFloat} = (:)(promote(a, b, c)...) (:)(start::T, step::T, stop::T) where {T<:AbstractFloat} = _colon(OrderStyle(T), ArithmeticStyle(T), start, step, stop) @@ -24,9 +24,9 @@ _colon(::Ordered, ::Any, start::T, step, stop::T) where {T} = StepRange(start, step, stop) # for T<:Union{Float16,Float32,Float64} see twiceprecision.jl _colon(::Ordered, ::ArithmeticRounds, start::T, step, stop::T) where {T} = - StepRangeLen(start, step, floor(Int, (stop-start)/step)+1) + StepRangeLen(start, step, floor(Integer, (stop-start)/step)+1) _colon(::Any, ::Any, start::T, step, stop::T) where {T} = - StepRangeLen(start, step, floor(Int, (stop-start)/step)+1) + StepRangeLen(start, step, floor(Integer, (stop-start)/step)+1) """ (:)(start, [step], stop) @@ -47,25 +47,20 @@ function _colon(start::T, step, stop::T) where T end """ - range(start[, stop]; length, stop, step=1) + range(start, stop, length) + range(start, stop; length, step) + range(start; length, stop, step) + range(;start, length, stop, step) -Given a starting value, construct a range either by length or from `start` to `stop`, -optionally with a given step (defaults to 1, a [`UnitRange`](@ref)). -One of `length` or `stop` is required. If `length`, `stop`, and `step` are all specified, they must agree. +Construct a specialized array with evenly spaced elements and optimized storage (an [`AbstractRange`](@ref)) from the arguments. +Mathematically a range is uniquely determined by any three of `start`, `step`, `stop` and `length`. +Valid invocations of range are: +* Call `range` with any three of `start`, `step`, `stop`, `length`. +* Call `range` with two of `start`, `stop`, `length`. In this case `step` will be assumed + to be one. If both arguments are Integers, a [`UnitRange`](@ref) will be returned. +* Call `range` with one of `stop` or `length`. `start` and `step` will be assumed to be one. -If `length` and `stop` are provided and `step` is not, the step size will be computed -automatically such that there are `length` linearly spaced elements in the range. - -If `step` and `stop` are provided and `length` is not, the overall range length will be computed -automatically such that the elements are `step` spaced. - -Special care is taken to ensure intermediate values are computed rationally. -To avoid this induced overhead, see the [`LinRange`](@ref) constructor. - -`stop` may be specified as either a positional or keyword argument. - -!!! compat "Julia 1.1" - `stop` as a positional argument requires at least Julia 1.1. +See Extended Help for additional details on the returned type. # Examples ```jldoctest @@ -86,51 +81,164 @@ julia> range(1, 10, length=101) julia> range(1, 100, step=5) 1:5:96 + +julia> range(stop=10, length=5) +6:10 + +julia> range(stop=10, step=1, length=5) +6:1:10 + +julia> range(start=1, step=1, stop=10) +1:1:10 + +julia> range(; length = 10) +Base.OneTo(10) + +julia> range(; stop = 6) +Base.OneTo(6) + +julia> range(; stop = 6.5) +1.0:1.0:6.0 ``` +If `length` is not specified and `stop - start` is not an integer multiple of `step`, a range that ends before `stop` will be produced. +```jldoctest +julia> range(1, 3.5, step=2) +1.0:2.0:3.0 +``` + +Special care is taken to ensure intermediate values are computed rationally. +To avoid this induced overhead, see the [`LinRange`](@ref) constructor. + +!!! compat "Julia 1.1" + `stop` as a positional argument requires at least Julia 1.1. + +!!! compat "Julia 1.7" + The versions without keyword arguments and `start` as a keyword argument + require at least Julia 1.7. + +!!! compat "Julia 1.8" + The versions with `stop` as a sole keyword argument, + or `length` as a sole keyword argument require at least Julia 1.8. + + +# Extended Help + +`range` will produce a `Base.OneTo` when the arguments are Integers and +* Only `length` is provided +* Only `stop` is provided + +`range` will produce a `UnitRange` when the arguments are Integers and +* Only `start` and `stop` are provided +* Only `length` and `stop` are provided + +A `UnitRange` is not produced if `step` is provided even if specified as one. """ -range(start; length::Union{Integer,Nothing}=nothing, stop=nothing, step=nothing) = +function range end + +range(start; stop=nothing, length::Union{Integer,Nothing}=nothing, step=nothing) = _range(start, step, stop, length) +range(start, stop; length::Union{Integer,Nothing}=nothing, step=nothing) = _range(start, step, stop, length) +range(start, stop, length::Integer) = _range(start, nothing, stop, length) -range(start, stop; length::Union{Integer,Nothing}=nothing, step=nothing) = - _range2(start, step, stop, length) - -_range2(start, ::Nothing, stop, ::Nothing) = - throw(ArgumentError("At least one of `length` or `step` must be specified")) - -_range2(start, step, stop, length) = _range(start, step, stop, length) - -# Range from start to stop: range(a, [step=s,] stop=b), no length -_range(start, step, stop, ::Nothing) = (:)(start, step, stop) -_range(start, ::Nothing, stop, ::Nothing) = (:)(start, stop) - -# Range of a given length: range(a, [step=s,] length=l), no stop -_range(a::Real, ::Nothing, ::Nothing, len::Integer) = UnitRange{typeof(a)}(a, oftype(a, a+len-1)) -_range(a::AbstractFloat, ::Nothing, ::Nothing, len::Integer) = _range(a, oftype(a, 1), nothing, len) -_range(a::AbstractFloat, st::AbstractFloat, ::Nothing, len::Integer) = _range(promote(a, st)..., nothing, len) -_range(a::Real, st::AbstractFloat, ::Nothing, len::Integer) = _range(float(a), st, nothing, len) -_range(a::AbstractFloat, st::Real, ::Nothing, len::Integer) = _range(a, float(st), nothing, len) -_range(a, ::Nothing, ::Nothing, len::Integer) = _range(a, oftype(a-a, 1), nothing, len) - -_range(a::T, step::T, ::Nothing, len::Integer) where {T <: AbstractFloat} = - _rangestyle(OrderStyle(T), ArithmeticStyle(T), a, step, len) -_range(a::T, step, ::Nothing, len::Integer) where {T} = - _rangestyle(OrderStyle(T), ArithmeticStyle(T), a, step, len) -_rangestyle(::Ordered, ::ArithmeticWraps, a::T, step::S, len::Integer) where {T,S} = - StepRange{T,S}(a, step, convert(T, a+step*(len-1))) -_rangestyle(::Any, ::Any, a::T, step::S, len::Integer) where {T,S} = - StepRangeLen{typeof(a+0*step),T,S}(a, step, len) - -# Malformed calls -_range(start, step, ::Nothing, ::Nothing) = # range(a, step=s) - throw(ArgumentError("At least one of `length` or `stop` must be specified")) -_range(start, ::Nothing, ::Nothing, ::Nothing) = # range(a) - throw(ArgumentError("At least one of `length` or `stop` must be specified")) -_range(::Nothing, ::Nothing, ::Nothing, ::Nothing) = # range(nothing) - throw(ArgumentError("At least one of `length` or `stop` must be specified")) -_range(start::Real, step::Real, stop::Real, length::Integer) = # range(a, step=s, stop=b, length=l) - throw(ArgumentError("Too many arguments specified; try passing only one of `stop` or `length`")) -_range(::Nothing, ::Nothing, ::Nothing, ::Integer) = # range(nothing, length=l) - throw(ArgumentError("Can't start a range at `nothing`")) +range(;start=nothing, stop=nothing, length::Union{Integer, Nothing}=nothing, step=nothing) = + _range(start, step, stop, length) + +_range(start::Nothing, step::Nothing, stop::Nothing, len::Nothing) = range_error(start, step, stop, len) +_range(start::Nothing, step::Nothing, stop::Nothing, len::Any ) = range_length(len) +_range(start::Nothing, step::Nothing, stop::Any , len::Nothing) = range_stop(stop) +_range(start::Nothing, step::Nothing, stop::Any , len::Any ) = range_stop_length(stop, len) +_range(start::Nothing, step::Any , stop::Nothing, len::Nothing) = range_error(start, step, stop, len) +_range(start::Nothing, step::Any , stop::Nothing, len::Any ) = range_error(start, step, stop, len) +_range(start::Nothing, step::Any , stop::Any , len::Nothing) = range_error(start, step, stop, len) +_range(start::Nothing, step::Any , stop::Any , len::Any ) = range_step_stop_length(step, stop, len) +_range(start::Any , step::Nothing, stop::Nothing, len::Nothing) = range_error(start, step, stop, len) +_range(start::Any , step::Nothing, stop::Nothing, len::Any ) = range_start_length(start, len) +_range(start::Any , step::Nothing, stop::Any , len::Nothing) = range_start_stop(start, stop) +_range(start::Any , step::Nothing, stop::Any , len::Any ) = range_start_stop_length(start, stop, len) +_range(start::Any , step::Any , stop::Nothing, len::Nothing) = range_error(start, step, stop, len) +_range(start::Any , step::Any , stop::Nothing, len::Any ) = range_start_step_length(start, step, len) +_range(start::Any , step::Any , stop::Any , len::Nothing) = range_start_step_stop(start, step, stop) +_range(start::Any , step::Any , stop::Any , len::Any ) = range_error(start, step, stop, len) + +# Length as the only argument +range_length(len::Integer) = OneTo(len) + +# Stop as the only argument +range_stop(stop) = range_start_stop(oftype(stop, 1), stop) +range_stop(stop::Integer) = range_length(stop) + +function range_step_stop_length(step, a, len::Integer) + start = a - step * (len - oneunit(len)) + if start isa Signed + # overflow in recomputing length from stop is okay + return StepRange{typeof(start),typeof(step)}(start, step, convert(typeof(start), a)) + end + return StepRangeLen{typeof(start),typeof(start),typeof(step)}(start, step, len) +end + +# Stop and length as the only argument +function range_stop_length(a, len::Integer) + step = oftype(a - a, 1) # assert that step is representable + start = a - (len - oneunit(len)) + if start isa Signed + # overflow in recomputing length from stop is okay + return UnitRange(start, oftype(start, a)) + end + return StepRangeLen{typeof(start),typeof(start),typeof(step)}(start, step, len) +end + +# Start and length as the only argument +function range_start_length(a, len::Integer) + step = oftype(a - a, 1) # assert that step is representable + stop = a + (len - oneunit(len)) + if stop isa Signed + # overflow in recomputing length from stop is okay + return UnitRange(oftype(stop, a), stop) + end + return StepRangeLen{typeof(stop),typeof(a),typeof(step)}(a, step, len) +end + +range_start_stop(start, stop) = start:stop + +function range_start_step_length(a, step, len::Integer) + stop = a + step * (len - oneunit(len)) + if stop isa Signed + # overflow in recomputing length from stop is okay + return StepRange{typeof(stop),typeof(step)}(convert(typeof(stop), a), step, stop) + end + return StepRangeLen{typeof(stop),typeof(a),typeof(step)}(a, step, len) +end + +range_start_step_stop(start, step, stop) = start:step:stop + +function range_error(start, step, stop, length) + hasstart = start !== nothing + hasstep = step !== nothing + hasstop = stop !== nothing + haslength = start !== nothing + + hint = if hasstart && hasstep && hasstop && haslength + "Try specifying only three arguments" + elseif !hasstop && !haslength + "At least one of `length` or `stop` must be specified." + elseif !hasstep && !haslength + "At least one of `length` or `step` must be specified." + elseif !hasstart && !hasstop + "At least one of `start` or `stop` must be specified." + else + "Try specifying more arguments." + end + + msg = """ + Cannot construct range from arguments: + start = $start + step = $step + stop = $stop + length = $length + $hint + """ + throw(ArgumentError(msg)) +end ## 1-dimensional ranges ## @@ -202,18 +310,21 @@ struct StepRange{T,S} <: OrdinalRange{T,S} stop::T function StepRange{T,S}(start, step, stop) where {T,S} - sta = convert(T, start) - ste = convert(S, step) - sto = convert(T, stop) - new(sta, ste, steprange_last(sta,ste,sto)) + start = convert(T, start) + step = convert(S, step) + stop = convert(T, stop) + return new(start, step, steprange_last(start, step, stop)) end end # to make StepRange constructor inlineable, so optimizer can see `step` value -function steprange_last(start::T, step, stop) where T - if isa(start,AbstractFloat) || isa(step,AbstractFloat) +function steprange_last(start, step, stop)::typeof(stop) + if isa(start, AbstractFloat) || isa(step, AbstractFloat) throw(ArgumentError("StepRange should not be used with floating point")) end + if isa(start, Integer) && !isinteger(start + step) + throw(ArgumentError("StepRange{<:Integer} cannot have non-integer step")) + end z = zero(step) step == z && throw(ArgumentError("step cannot be zero")) @@ -228,33 +339,31 @@ function steprange_last(start::T, step, stop) where T absdiff, absstep = stop > start ? (stop - start, step) : (start - stop, -step) # Compute remainder as a nonnegative number: - if T <: Signed && absdiff < zero(absdiff) - # handle signed overflow with unsigned rem - remain = convert(T, unsigned(absdiff) % absstep) + if absdiff isa Signed && absdiff < zero(absdiff) + # unlikely, but handle the signed overflow case with unsigned rem + remain = convert(typeof(absdiff), unsigned(absdiff) % absstep) else - remain = absdiff % absstep + remain = convert(typeof(absdiff), absdiff % absstep) end # Move `stop` closer to `start` if there is a remainder: last = stop > start ? stop - remain : stop + remain end end - last + return last end -function steprange_last_empty(start::Integer, step, stop) - # empty range has a special representation where stop = start-1 - # this is needed to avoid the wrap-around that can happen computing - # start - step, which leads to a range that looks very large instead - # of empty. +function steprange_last_empty(start::Integer, step, stop)::typeof(stop) + # empty range has a special representation where stop = start-1, + # which simplifies arithmetic for Signed numbers if step > zero(step) - last = start - oneunit(stop-start) + last = start - oneunit(step) else - last = start + oneunit(stop-start) + last = start + oneunit(step) end - last + return last end -# For types where x+oneunit(x) may not be well-defined -steprange_last_empty(start, step, stop) = start - step +# For types where x+oneunit(x) may not be well-defined use the user-given value for stop +steprange_last_empty(start, step, stop) = stop StepRange{T}(start, step::S, stop) where {T,S} = StepRange{T,S}(start, step, stop) StepRange(start::T, step::S, stop::T) where {T,S} = StepRange{T,S}(start, step, stop) @@ -281,29 +390,36 @@ UnitRange{Int64} struct UnitRange{T<:Real} <: AbstractUnitRange{T} start::T stop::T - UnitRange{T}(start, stop) where {T<:Real} = new(start, unitrange_last(start,stop)) + UnitRange{T}(start::T, stop::T) where {T<:Real} = new(start, unitrange_last(start, stop)) end +UnitRange{T}(start, stop) where {T<:Real} = UnitRange{T}(convert(T, start), convert(T, stop)) UnitRange(start::T, stop::T) where {T<:Real} = UnitRange{T}(start, stop) +UnitRange(start, stop) = UnitRange(promote(start, stop)...) -unitrange_last(::Bool, stop::Bool) = stop -unitrange_last(start::T, stop::T) where {T<:Integer} = - ifelse(stop >= start, stop, convert(T,start-oneunit(stop-start))) -unitrange_last(start::T, stop::T) where {T} = - ifelse(stop >= start, convert(T,start+floor(stop-start)), - convert(T,start-oneunit(stop-start))) +# if stop and start are integral, we know that their difference is a multiple of 1 +unitrange_last(start::Integer, stop::Integer) = + stop >= start ? stop : convert(typeof(stop), start - oneunit(start - stop)) +# otherwise, use `floor` as a more efficient way to compute modulus with step=1 +unitrange_last(start, stop) = + stop >= start ? convert(typeof(stop), start + floor(stop - start)) : + convert(typeof(stop), start - oneunit(start - stop)) + +unitrange(x::AbstractUnitRange) = UnitRange(x) # convenience conversion for promoting the range type if isdefined(Main, :Base) # Constant-fold-able indexing into tuples to functionally expose Base.tail and Base.front - function getindex(@nospecialize(t::Tuple), r::UnitRange) - @_inline_meta - r.start > r.stop && return () - if r.start == 1 - r.stop == length(t) && return t - r.stop == length(t)-1 && return front(t) - r.stop == length(t)-2 && return front(front(t)) - elseif r.stop == length(t) - r.start == 2 && return tail(t) - r.start == 3 && return tail(tail(t)) + function getindex(@nospecialize(t::Tuple), r::AbstractUnitRange) + @inline + require_one_based_indexing(r) + if length(r) <= 10 + return ntuple(i -> t[i + first(r) - 1], length(r)) + elseif first(r) == 1 + last(r) == length(t) && return t + last(r) == length(t)-1 && return front(t) + last(r) == length(t)-2 && return front(front(t)) + elseif last(r) == length(t) + first(r) == 2 && return tail(t) + first(r) == 3 && return tail(tail(t)) end return (eltype(t)[t[ri] for ri in r]...,) end @@ -318,25 +434,34 @@ be 1. """ struct OneTo{T<:Integer} <: AbstractUnitRange{T} stop::T - OneTo{T}(stop) where {T<:Integer} = new(max(zero(T), stop)) + function OneTo{T}(stop) where {T<:Integer} + throwbool(r) = (@noinline; throw(ArgumentError("invalid index: $r of type Bool"))) + T === Bool && throwbool(stop) + return new(max(zero(T), stop)) + end + function OneTo{T}(r::AbstractRange) where {T<:Integer} - throwstart(r) = (@_noinline_meta; throw(ArgumentError("first element must be 1, got $(first(r))"))) - throwstep(r) = (@_noinline_meta; throw(ArgumentError("step must be 1, got $(step(r))"))) + throwstart(r) = (@noinline; throw(ArgumentError("first element must be 1, got $(first(r))"))) + throwstep(r) = (@noinline; throw(ArgumentError("step must be 1, got $(step(r))"))) + throwbool(r) = (@noinline; throw(ArgumentError("invalid index: $r of type Bool"))) first(r) == 1 || throwstart(r) step(r) == 1 || throwstep(r) + T === Bool && throwbool(r) return new(max(zero(T), last(r))) end end OneTo(stop::T) where {T<:Integer} = OneTo{T}(stop) OneTo(r::AbstractRange{T}) where {T<:Integer} = OneTo{T}(r) +oneto(r) = OneTo(r) ## Step ranges parameterized by length """ - StepRangeLen{T,R,S}(ref::R, step::S, len, [offset=1]) where {T,R,S} - StepRangeLen( ref::R, step::S, len, [offset=1]) where { R,S} + StepRangeLen( ref::R, step::S, len, [offset=1]) where { R,S} + StepRangeLen{T,R,S}( ref::R, step::S, len, [offset=1]) where {T,R,S} + StepRangeLen{T,R,S,L}(ref::R, step::S, len, [offset=1]) where {T,R,S,L} -A range `r` where `r[i]` produces values of type `T` (in the second +A range `r` where `r[i]` produces values of type `T` (in the first form, `T` is deduced automatically), parameterized by a `ref`erence value, a `step`, and the `len`gth. By default `ref` is the starting value `r[1]`, but alternatively you can supply it as the value of @@ -344,45 +469,53 @@ value `r[1]`, but alternatively you can supply it as the value of with `TwicePrecision` this can be used to implement ranges that are free of roundoff error. """ -struct StepRangeLen{T,R,S} <: AbstractRange{T} +struct StepRangeLen{T,R,S,L<:Integer} <: AbstractRange{T} ref::R # reference value (might be smallest-magnitude value in the range) step::S # step value - len::Int # length of the range - offset::Int # the index of ref + len::L # length of the range + offset::L # the index of ref - function StepRangeLen{T,R,S}(ref::R, step::S, len::Integer, offset::Integer = 1) where {T,R,S} - len >= 0 || throw(ArgumentError("length cannot be negative, got $len")) - 1 <= offset <= max(1,len) || throw(ArgumentError("StepRangeLen: offset must be in [1,$len], got $offset")) - new(ref, step, len, offset) + function StepRangeLen{T,R,S,L}(ref::R, step::S, len::Integer, offset::Integer = 1) where {T,R,S,L} + if T <: Integer && !isinteger(ref + step) + throw(ArgumentError("StepRangeLen{<:Integer} cannot have non-integer step")) + end + len = convert(L, len) + len >= zero(len) || throw(ArgumentError("length cannot be negative, got $len")) + offset = convert(L, offset) + L1 = oneunit(typeof(len)) + L1 <= offset <= max(L1, len) || throw(ArgumentError("StepRangeLen: offset must be in [1,$len], got $offset")) + return new(ref, step, len, offset) end end +StepRangeLen{T,R,S}(ref::R, step::S, len::Integer, offset::Integer = 1) where {T,R,S} = + StepRangeLen{T,R,S,promote_type(Int,typeof(len))}(ref, step, len, offset) StepRangeLen(ref::R, step::S, len::Integer, offset::Integer = 1) where {R,S} = - StepRangeLen{typeof(ref+0*step),R,S}(ref, step, len, offset) + StepRangeLen{typeof(ref+zero(step)),R,S,promote_type(Int,typeof(len))}(ref, step, len, offset) StepRangeLen{T}(ref::R, step::S, len::Integer, offset::Integer = 1) where {T,R,S} = - StepRangeLen{T,R,S}(ref, step, len, offset) + StepRangeLen{T,R,S,promote_type(Int,typeof(len))}(ref, step, len, offset) ## range with computed step """ - LinRange{T} + LinRange{T,L} A range with `len` linearly spaced elements between its `start` and `stop`. The size of the spacing is controlled by `len`, which must -be an `Int`. +be an `Integer`. # Examples ```jldoctest julia> LinRange(1.5, 5.5, 9) -9-element LinRange{Float64}: - 1.5,2.0,2.5,3.0,3.5,4.0,4.5,5.0,5.5 +9-element LinRange{Float64, Int64}: + 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5 ``` Compared to using [`range`](@ref), directly constructing a `LinRange` should have less overhead but won't try to correct for floating point errors: -```julia +```jldoctest julia> collect(range(-0.1, 0.3, length=5)) -5-element Array{Float64,1}: +5-element Vector{Float64}: -0.1 0.0 0.1 @@ -390,7 +523,7 @@ julia> collect(range(-0.1, 0.3, length=5)) 0.3 julia> collect(LinRange(-0.1, 0.3, 5)) -5-element Array{Float64,1}: +5-element Vector{Float64}: -0.1 -1.3877787807814457e-17 0.09999999999999999 @@ -398,45 +531,57 @@ julia> collect(LinRange(-0.1, 0.3, 5)) 0.3 ``` """ -struct LinRange{T} <: AbstractRange{T} +struct LinRange{T,L<:Integer} <: AbstractRange{T} start::T stop::T - len::Int - lendiv::Int + len::L + lendiv::L - function LinRange{T}(start,stop,len) where T + function LinRange{T,L}(start::T, stop::T, len::L) where {T,L<:Integer} len >= 0 || throw(ArgumentError("range($start, stop=$stop, length=$len): negative length")) - if len == 1 + onelen = oneunit(typeof(len)) + if len == onelen start == stop || throw(ArgumentError("range($start, stop=$stop, length=$len): endpoints differ")) - return new(start, stop, 1, 1) + return new(start, stop, len, len) + end + lendiv = max(len - onelen, onelen) + if T <: Integer && !iszero(mod(stop-start, lendiv)) + throw(ArgumentError("LinRange{<:Integer} cannot have non-integer step")) end - new(start,stop,len,max(len-1,1)) + return new(start, stop, len, lendiv) end end +function LinRange{T,L}(start, stop, len::Integer) where {T,L} + LinRange{T,L}(convert(T, start), convert(T, stop), convert(L, len)) +end + +function LinRange{T}(start, stop, len::Integer) where T + LinRange{T,promote_type(Int,typeof(len))}(start, stop, len) +end + function LinRange(start, stop, len::Integer) - T = typeof((stop-start)/len) + T = typeof((zero(stop) - zero(start)) / oneunit(len)) LinRange{T}(start, stop, len) end -function _range(start::T, ::Nothing, stop::S, len::Integer) where {T,S} - a, b = promote(start, stop) - _range(a, nothing, b, len) -end -_range(start::T, ::Nothing, stop::T, len::Integer) where {T<:Real} = LinRange{T}(start, stop, len) -_range(start::T, ::Nothing, stop::T, len::Integer) where {T} = LinRange{T}(start, stop, len) -_range(start::T, ::Nothing, stop::T, len::Integer) where {T<:Integer} = +range_start_stop_length(start, stop, len::Integer) = + range_start_stop_length(promote(start, stop)..., len) +range_start_stop_length(start::T, stop::T, len::Integer) where {T} = LinRange(start, stop, len) +range_start_stop_length(start::T, stop::T, len::Integer) where {T<:Integer} = _linspace(float(T), start, stop, len) ## for Float16, Float32, and Float64 we hit twiceprecision.jl to lift to higher precision StepRangeLen # for all other types we fall back to a plain old LinRange _linspace(::Type{T}, start::Integer, stop::Integer, len::Integer) where T = LinRange{T}(start, stop, len) -function show(io::IO, r::LinRange) - print(io, "range(") +function show(io::IO, r::LinRange{T}) where {T} + print(io, "LinRange{") + show(io, T) + print(io, "}(") show(io, first(r)) - print(io, ", stop=") + print(io, ", ") show(io, last(r)) - print(io, ", length=") + print(io, ", ") show(io, length(r)) print(io, ')') end @@ -447,7 +592,7 @@ as if it were `collect(r)`, dependent on the size of the terminal, and taking into account whether compact numbers should be shown. It figures out the width in characters of each element, and if they end up too wide, it shows the first and last elements separated by a -horizontal ellipsis. Typical output will look like `1.0,2.0,3.0,…,4.0,5.0,6.0`. +horizontal ellipsis. Typical output will look like `1.0, 2.0, …, 5.0, 6.0`. `print_range(io, r, pre, sep, post, hdots)` uses optional parameters `pre` and `post` characters for each printed row, @@ -456,9 +601,9 @@ parameters `pre` and `post` characters for each printed row, """ function print_range(io::IO, r::AbstractRange, pre::AbstractString = " ", - sep::AbstractString = ",", + sep::AbstractString = ", ", post::AbstractString = "", - hdots::AbstractString = ",\u2026,") # horiz ellipsis + hdots::AbstractString = ", \u2026, ") # horiz ellipsis # This function borrows from print_matrix() in show.jl # and should be called by show and display sz = displaysize(io) @@ -477,31 +622,34 @@ function print_range(io::IO, r::AbstractRange, maxpossiblecols = div(screenwidth, 1+sepsize) # assume each element is at least 1 char + 1 separator colsr = n <= maxpossiblecols ? (1:n) : [1:div(maxpossiblecols,2)+1; (n-div(maxpossiblecols,2)):n] rowmatrix = reshape(r[colsr], 1, length(colsr)) # treat the range as a one-row matrix for print_matrix_row - A = alignment(io, rowmatrix, 1:m, 1:length(rowmatrix), screenwidth, screenwidth, sepsize) # how much space range takes + nrow, idxlast = size(rowmatrix, 2), last(axes(rowmatrix, 2)) + A = alignment(io, rowmatrix, 1:m, 1:length(rowmatrix), screenwidth, screenwidth, sepsize, nrow) # how much space range takes if n <= length(A) # cols fit screen, so print out all elements print(io, pre) # put in pre chars - print_matrix_row(io,rowmatrix,A,1,1:n,sep) # the entire range + print_matrix_row(io,rowmatrix,A,1,1:n,sep,idxlast) # the entire range print(io, post) # add the post characters else # cols don't fit so put horiz ellipsis in the middle # how many chars left after dividing width of screen in half # and accounting for the horiz ellipsis c = div(screenwidth-length(hdots)+1,2)+1 # chars remaining for each side of rowmatrix - alignR = reverse(alignment(io, rowmatrix, 1:m, length(rowmatrix):-1:1, c, c, sepsize)) # which cols of rowmatrix to put on the right + alignR = reverse(alignment(io, rowmatrix, 1:m, length(rowmatrix):-1:1, c, c, sepsize, nrow)) # which cols of rowmatrix to put on the right c = screenwidth - sum(map(sum,alignR)) - (length(alignR)-1)*sepsize - length(hdots) - alignL = alignment(io, rowmatrix, 1:m, 1:length(rowmatrix), c, c, sepsize) # which cols of rowmatrix to put on the left + alignL = alignment(io, rowmatrix, 1:m, 1:length(rowmatrix), c, c, sepsize, nrow) # which cols of rowmatrix to put on the left print(io, pre) # put in pre chars - print_matrix_row(io, rowmatrix,alignL,1,1:length(alignL),sep) # left part of range + print_matrix_row(io, rowmatrix,alignL,1,1:length(alignL),sep,idxlast) # left part of range print(io, hdots) # horizontal ellipsis - print_matrix_row(io, rowmatrix,alignR,1,length(rowmatrix)-length(alignR)+1:length(rowmatrix),sep) # right part of range + print_matrix_row(io, rowmatrix,alignR,1,length(rowmatrix)-length(alignR)+1:length(rowmatrix),sep,idxlast) # right part of range print(io, post) # post chars end end ## interface implementations +length(r::AbstractRange) = error("length implementation missing") # catch mistakes size(r::AbstractRange) = (length(r),) isempty(r::StepRange) = + # steprange_last(r.start, r.step, r.stop) == r.stop (r.start != r.stop) & ((r.step > zero(r.step)) != (r.stop > r.start)) isempty(r::AbstractUnitRange) = first(r) > last(r) isempty(r::StepRangeLen) = length(r) == 0 @@ -528,68 +676,141 @@ julia> step(range(2.5, stop=10.9, length=85)) ``` """ step(r::StepRange) = r.step -step(r::AbstractUnitRange{T}) where{T} = oneunit(T) - zero(T) +step(r::AbstractUnitRange{T}) where {T} = oneunit(T) - zero(T) step(r::StepRangeLen) = r.step step(r::StepRangeLen{T}) where {T<:AbstractFloat} = T(r.step) step(r::LinRange) = (last(r)-first(r))/r.lendiv +# high-precision step step_hp(r::StepRangeLen) = r.step step_hp(r::AbstractRange) = step(r) -unsafe_length(r::AbstractRange) = length(r) # generic fallback - -function unsafe_length(r::StepRange) - n = Integer(div((r.stop - r.start) + r.step, r.step)) - isempty(r) ? zero(n) : n -end -length(r::StepRange) = unsafe_length(r) -unsafe_length(r::AbstractUnitRange) = Integer(last(r) - first(r) + step(r)) -unsafe_length(r::OneTo) = Integer(r.stop - zero(r.stop)) -length(r::AbstractUnitRange) = unsafe_length(r) -length(r::OneTo) = unsafe_length(r) -length(r::StepRangeLen) = r.len -length(r::LinRange) = r.len +axes(r::AbstractRange) = (oneto(length(r)),) # Needed to fold the `firstindex` call in SimdLoop.simd_index firstindex(::UnitRange) = 1 firstindex(::StepRange) = 1 firstindex(::LinRange) = 1 -function length(r::StepRange{T}) where T<:Union{Int,UInt,Int64,UInt64,Int128,UInt128} - isempty(r) && return zero(T) - if r.step > 1 - return checked_add(convert(T, div(unsigned(r.stop - r.start), r.step)), one(T)) - elseif r.step < -1 - return checked_add(convert(T, div(unsigned(r.start - r.stop), -r.step)), one(T)) - elseif r.step > 0 - return checked_add(div(checked_sub(r.stop, r.start), r.step), one(T)) +# n.b. checked_length for these is defined iff checked_add and checked_sub are +# defined between the relevant types +function checked_length(r::OrdinalRange{T}) where T + s = step(r) + start = first(r) + if isempty(r) + return Integer(div(start - start, oneunit(s))) + end + stop = last(r) + if isless(s, zero(s)) + diff = checked_sub(start, stop) + s = -s else - return checked_add(div(checked_sub(r.start, r.stop), -r.step), one(T)) + diff = checked_sub(stop, start) end + a = div(diff, s) + return Integer(checked_add(a, oneunit(a))) end -function length(r::AbstractUnitRange{T}) where T<:Union{Int,Int64,Int128} - @_inline_meta - checked_add(checked_sub(last(r), first(r)), one(T)) +function checked_length(r::AbstractUnitRange{T}) where T + # compiler optimization: remove dead cases from above + if isempty(r) + return Integer(first(r) - first(r)) + end + a = checked_sub(last(r), first(r)) + return Integer(checked_add(a, oneunit(a))) end -length(r::OneTo{T}) where {T<:Union{Int,Int64}} = T(r.stop) -length(r::AbstractUnitRange{T}) where {T<:Union{UInt,UInt64,UInt128}} = - r.stop < r.start ? zero(T) : checked_add(last(r) - first(r), one(T)) +function length(r::OrdinalRange{T}) where T + s = step(r) + start = first(r) + if isempty(r) + return Integer(div(start - start, oneunit(s))) + end + stop = last(r) + if isless(s, zero(s)) + diff = start - stop + s = -s + else + diff = stop - start + end + a = div(diff, s) + return Integer(a + oneunit(a)) +end -# some special cases to favor default Int type -let smallint = (Int === Int64 ? - Union{Int8,UInt8,Int16,UInt16,Int32,UInt32} : - Union{Int8,UInt8,Int16,UInt16}) - global length - - function length(r::StepRange{<:smallint}) - isempty(r) && return Int(0) - div(Int(r.stop)+Int(r.step) - Int(r.start), Int(r.step)) +function length(r::AbstractUnitRange{T}) where T + @inline + start, stop = first(r), last(r) + a = oneunit(zero(stop) - zero(start)) + if a isa Signed || stop >= start + a += stop - start # Signed are allowed to go negative + else + a = zero(a) # Unsigned don't necessarily underflow + end + return Integer(a) +end + +length(r::OneTo) = Integer(r.stop - zero(r.stop)) +length(r::StepRangeLen) = r.len +length(r::LinRange) = r.len + +let bigints = Union{Int, UInt, Int64, UInt64, Int128, UInt128} + global length, checked_length + # compile optimization for which promote_type(T, Int) == T + length(r::OneTo{T}) where {T<:bigints} = r.stop + # slightly more accurate length and checked_length in extreme cases + # (near typemax) for types with known `unsigned` functions + function length(r::OrdinalRange{T}) where T<:bigints + s = step(r) + isempty(r) && return zero(T) + diff = last(r) - first(r) + # if |s| > 1, diff might have overflowed, but unsigned(diff)÷s should + # therefore still be valid (if the result is representable at all) + # n.b. !(s isa T) + if s isa Unsigned || -1 <= s <= 1 || s == -s + a = div(diff, s) + elseif s < 0 + a = div(unsigned(-diff), -s) % typeof(diff) + else + a = div(unsigned(diff), s) % typeof(diff) + end + return Integer(a) + oneunit(a) end + function checked_length(r::OrdinalRange{T}) where T<:bigints + s = step(r) + isempty(r) && return zero(T) + stop, start = last(r), first(r) + # n.b. !(s isa T) + if s > 1 + diff = stop - start + a = convert(T, div(unsigned(diff), s)) + elseif s < -1 + diff = start - stop + a = convert(T, div(unsigned(diff), -s)) + elseif s > 0 + a = div(checked_sub(stop, start), s) + else + a = div(checked_sub(start, stop), -s) + end + return checked_add(a, oneunit(a)) + end +end - length(r::AbstractUnitRange{<:smallint}) = Int(last(r)) - Int(first(r)) + 1 - length(r::OneTo{<:smallint}) = Int(r.stop) +# some special cases to favor default Int type +let smallints = (Int === Int64 ? + Union{Int8, UInt8, Int16, UInt16, Int32, UInt32} : + Union{Int8, UInt8, Int16, UInt16}) + global length, checked_length + # n.b. !(step isa T) + function length(r::OrdinalRange{<:smallints}) + s = step(r) + isempty(r) && return 0 + return div(Int(last(r)) - Int(first(r)), s) + 1 + end + length(r::AbstractUnitRange{<:smallints}) = Int(last(r)) - Int(first(r)) + 1 + length(r::OneTo{<:smallints}) = Int(r.stop) + checked_length(r::OrdinalRange{<:smallints}) = length(r) + checked_length(r::AbstractUnitRange{<:smallints}) = length(r) + checked_length(r::OneTo{<:smallints}) = length(r) end first(r::OrdinalRange{T}) where {T} = convert(T, r.start) @@ -597,7 +818,7 @@ first(r::OneTo{T}) where {T} = oneunit(T) first(r::StepRangeLen) = unsafe_getindex(r, 1) first(r::LinRange) = r.start -last(r::OrdinalRange{T}) where {T} = convert(T, r.stop) +last(r::OrdinalRange{T}) where {T} = convert(T, r.stop) # via steprange_last last(r::StepRangeLen) = unsafe_getindex(r, length(r)) last(r::LinRange) = r.stop @@ -606,6 +827,40 @@ maximum(r::AbstractUnitRange) = isempty(r) ? throw(ArgumentError("range must be minimum(r::AbstractRange) = isempty(r) ? throw(ArgumentError("range must be non-empty")) : min(first(r), last(r)) maximum(r::AbstractRange) = isempty(r) ? throw(ArgumentError("range must be non-empty")) : max(first(r), last(r)) +""" + argmin(r::AbstractRange) + +Ranges can have multiple minimal elements. In that case +`argmin` will return a minimal index, but not necessarily the +first one. +""" +function argmin(r::AbstractRange) + if isempty(r) + throw(ArgumentError("range must be non-empty")) + elseif step(r) > 0 + firstindex(r) + else + lastindex(r) + end +end + +""" + argmax(r::AbstractRange) + +Ranges can have multiple maximal elements. In that case +`argmax` will return a maximal index, but not necessarily the +first one. +""" +function argmax(r::AbstractRange) + if isempty(r) + throw(ArgumentError("range must be non-empty")) + elseif step(r) > 0 + lastindex(r) + else + firstindex(r) + end +end + extrema(r::AbstractRange) = (minimum(r), maximum(r)) # Ranges are immutable @@ -614,16 +869,17 @@ copy(r::AbstractRange) = r ## iteration -function iterate(r::Union{LinRange,StepRangeLen}, i::Int=1) - @_inline_meta +function iterate(r::Union{StepRangeLen,LinRange}, i::Integer=zero(length(r))) + @inline + i += oneunit(i) length(r) < i && return nothing - unsafe_getindex(r, i), i + 1 + unsafe_getindex(r, i), i end iterate(r::OrdinalRange) = isempty(r) ? nothing : (first(r), first(r)) function iterate(r::OrdinalRange{T}, i) where {T} - @_inline_meta + @inline i == last(r) && return nothing next = convert(T, i + step(r)) (next, next) @@ -634,8 +890,9 @@ end _in_unit_range(v::UnitRange, val, i::Integer) = i > 0 && val <= v.stop && val >= v.start function getindex(v::UnitRange{T}, i::Integer) where T - @_inline_meta - val = convert(T, v.start + (i - 1)) + @inline + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) + val = convert(T, v.start + (i - oneunit(i))) @boundscheck _in_unit_range(v, val, i) || throw_boundserror(v, i) val end @@ -644,21 +901,24 @@ const OverflowSafe = Union{Bool,Int8,Int16,Int32,Int64,Int128, UInt8,UInt16,UInt32,UInt64,UInt128} function getindex(v::UnitRange{T}, i::Integer) where {T<:OverflowSafe} - @_inline_meta - val = v.start + (i - 1) + @inline + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) + val = v.start + (i - oneunit(i)) @boundscheck _in_unit_range(v, val, i) || throw_boundserror(v, i) val % T end function getindex(v::OneTo{T}, i::Integer) where T - @_inline_meta + @inline + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) @boundscheck ((i > 0) & (i <= v.stop)) || throw_boundserror(v, i) convert(T, i) end function getindex(v::AbstractRange{T}, i::Integer) where T - @_inline_meta - ret = convert(T, first(v) + (i - 1)*step_hp(v)) + @inline + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) + ret = convert(T, first(v) + (i - oneunit(i))*step_hp(v)) ok = ifelse(step(v) > zero(step(v)), (ret <= last(v)) & (ret >= first(v)), (ret <= first(v)) & (ret >= last(v))) @@ -667,83 +927,171 @@ function getindex(v::AbstractRange{T}, i::Integer) where T end function getindex(r::Union{StepRangeLen,LinRange}, i::Integer) - @_inline_meta + @inline + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) @boundscheck checkbounds(r, i) unsafe_getindex(r, i) end # This is separate to make it useful even when running with --check-bounds=yes function unsafe_getindex(r::StepRangeLen{T}, i::Integer) where T + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) u = i - r.offset T(r.ref + u*r.step) end function _getindex_hiprec(r::StepRangeLen, i::Integer) # without rounding by T + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) u = i - r.offset r.ref + u*r.step end function unsafe_getindex(r::LinRange, i::Integer) - lerpi(i-1, r.lendiv, r.start, r.stop) + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) + lerpi(i-oneunit(i), r.lendiv, r.start, r.stop) end function lerpi(j::Integer, d::Integer, a::T, b::T) where T - @_inline_meta - t = j/d - T((1-t)*a + t*b) + @inline + t = j/d # ∈ [0,1] + # compute approximately fma(t, b, -fma(t, a, a)) + return T((1-t)*a + t*b) end getindex(r::AbstractRange, ::Colon) = copy(r) -function getindex(r::AbstractUnitRange, s::AbstractUnitRange{<:Integer}) - @_inline_meta +function getindex(r::AbstractUnitRange, s::AbstractUnitRange{T}) where {T<:Integer} + @inline @boundscheck checkbounds(r, s) - f = first(r) - st = oftype(f, f + first(s)-1) - range(st, length=length(s)) + + if T === Bool + return range(first(s) ? first(r) : last(r), length = last(s)) + else + f = first(r) + start = oftype(f, f + first(s) - firstindex(r)) + len = length(s) + stop = oftype(f, start + (len - oneunit(len))) + return range(start, stop) + end end function getindex(r::OneTo{T}, s::OneTo) where T - @_inline_meta + @inline @boundscheck checkbounds(r, s) - OneTo(T(s.stop)) + return OneTo(T(s.stop)) end -function getindex(r::AbstractUnitRange, s::StepRange{<:Integer}) - @_inline_meta +function getindex(r::AbstractUnitRange, s::StepRange{T}) where {T<:Integer} + @inline @boundscheck checkbounds(r, s) - st = oftype(first(r), first(r) + s.start-1) - range(st, step=step(s), length=length(s)) + + if T === Bool + return range(first(s) ? first(r) : last(r), step=oneunit(eltype(r)), length=last(s)) + else + f = first(r) + start = oftype(f, f + s.start - firstindex(r)) + st = step(s) + len = length(s) + stop = oftype(f, start + (len - oneunit(len)) * st) + return range(start, stop; step=st) + end end -function getindex(r::StepRange, s::AbstractRange{<:Integer}) - @_inline_meta +function getindex(r::StepRange, s::AbstractRange{T}) where {T<:Integer} + @inline @boundscheck checkbounds(r, s) - st = oftype(r.start, r.start + (first(s)-1)*step(r)) - range(st, step=step(r)*step(s), length=length(s)) + + if T === Bool + if length(s) == 0 + start, len = first(r), 0 + elseif length(s) == 1 + if first(s) + start, len = first(r), 1 + else + start, len = first(r), 0 + end + else # length(s) == 2 + start, len = last(r), 1 + end + return range(start, step=step(r); length=len) + else + f = r.start + fs = first(s) + st = r.step + start = oftype(f, f + (fs - oneunit(fs)) * st) + st = st * step(s) + len = length(s) + stop = oftype(f, start + (len - oneunit(len)) * st) + return range(start, stop; step=st) + end end -function getindex(r::StepRangeLen{T}, s::OrdinalRange{<:Integer}) where {T} - @_inline_meta +function getindex(r::StepRangeLen{T}, s::OrdinalRange{S}) where {T, S<:Integer} + @inline @boundscheck checkbounds(r, s) - # Find closest approach to offset by s - ind = LinearIndices(s) - offset = max(min(1 + round(Int, (r.offset - first(s))/step(s)), last(ind)), first(ind)) - ref = _getindex_hiprec(r, first(s) + (offset-1)*step(s)) - return StepRangeLen{T}(ref, r.step*step(s), length(s), offset) + + len = length(s) + sstep = step_hp(s) + rstep = step_hp(r) + L = typeof(len) + if S === Bool + rstep *= one(sstep) + if len == 0 + return StepRangeLen{T}(first(r), rstep, zero(L), oneunit(L)) + elseif len == 1 + if first(s) + return StepRangeLen{T}(first(r), rstep, oneunit(L), oneunit(L)) + else + return StepRangeLen{T}(first(r), rstep, zero(L), oneunit(L)) + end + else # len == 2 + return StepRangeLen{T}(last(r), rstep, oneunit(L), oneunit(L)) + end + else + # Find closest approach to offset by s + ind = LinearIndices(s) + offset = L(max(min(1 + round(L, (r.offset - first(s))/sstep), last(ind)), first(ind))) + ref = _getindex_hiprec(r, first(s) + (offset - oneunit(offset)) * sstep) + return StepRangeLen{T}(ref, rstep*sstep, len, offset) + end end -function getindex(r::LinRange{T}, s::OrdinalRange{<:Integer}) where {T} - @_inline_meta +function getindex(r::LinRange{T}, s::OrdinalRange{S}) where {T, S<:Integer} + @inline @boundscheck checkbounds(r, s) - vfirst = unsafe_getindex(r, first(s)) - vlast = unsafe_getindex(r, last(s)) - return LinRange{T}(vfirst, vlast, length(s)) + + len = length(s) + L = typeof(len) + if S === Bool + if len == 0 + return LinRange{T}(first(r), first(r), len) + elseif len == 1 + if first(s) + return LinRange{T}(first(r), first(r), len) + else + return LinRange{T}(first(r), first(r), zero(L)) + end + else # length(s) == 2 + return LinRange{T}(last(r), last(r), oneunit(L)) + end + else + vfirst = unsafe_getindex(r, first(s)) + vlast = unsafe_getindex(r, last(s)) + return LinRange{T}(vfirst, vlast, len) + end end show(io::IO, r::AbstractRange) = print(io, repr(first(r)), ':', repr(step(r)), ':', repr(last(r))) show(io::IO, r::UnitRange) = print(io, repr(first(r)), ':', repr(last(r))) show(io::IO, r::OneTo) = print(io, "Base.OneTo(", r.stop, ")") +function show(io::IO, r::StepRangeLen) + if step(r) != 0 + print(io, repr(first(r)), ':', repr(step(r)), ':', repr(last(r))) + else + # ugly temporary printing, to avoid 0:0:0 etc. + print(io, "StepRangeLen(", repr(first(r)), ", ", repr(step(r)), ", ", repr(length(r)), ")") + end +end function ==(r::T, s::T) where {T<:AbstractRange} isempty(r) && return isempty(s) @@ -757,6 +1105,11 @@ function ==(r::OrdinalRange, s::OrdinalRange) (first(r) == first(s)) & (step(r) == step(s)) & (last(r) == last(s)) end +==(r::AbstractUnitRange, s::AbstractUnitRange) = + (isempty(r) & isempty(s)) | ((first(r) == first(s)) & (last(r) == last(s))) + +==(r::OneTo, s::OneTo) = last(r) == last(s) + ==(r::T, s::T) where {T<:Union{StepRangeLen,LinRange}} = (isempty(r) & isempty(s)) | ((first(r) == first(s)) & (length(r) == length(s)) & (last(r) == last(s))) @@ -855,6 +1208,16 @@ function intersect(r::StepRange, s::StepRange) step(r) < zero(step(r)) ? StepRange{T,S}(n, -a, m) : StepRange{T,S}(m, a, n) end +function intersect(r1::AbstractRange, r2::AbstractRange) + # To iterate over the shorter range + length(r1) > length(r2) && return intersect(r2, r1) + + r1 = unique(r1) + T = promote_eltype(r1, r2) + + return T[x for x in r1 if x ∈ r2] +end + function intersect(r1::AbstractRange, r2::AbstractRange, r3::AbstractRange, r::AbstractRange...) i = intersect(intersect(r1, r2), r3) for t in r @@ -888,32 +1251,36 @@ end issubset(r::OneTo, s::OneTo) = r.stop <= s.stop issubset(r::AbstractUnitRange{<:Integer}, s::AbstractUnitRange{<:Integer}) = - isempty(r) || first(r) >= first(s) && last(r) <= last(s) + isempty(r) || (first(r) >= first(s) && last(r) <= last(s)) ## linear operations on ranges ## --(r::OrdinalRange) = range(-first(r), step=-step(r), length=length(r)) --(r::StepRangeLen{T,R,S}) where {T,R,S} = - StepRangeLen{T,R,S}(-r.ref, -r.step, length(r), r.offset) +-(r::OrdinalRange) = range(-first(r), step=negate(step(r)), length=length(r)) +-(r::StepRangeLen{T,R,S,L}) where {T,R,S,L} = + StepRangeLen{T,R,S,L}(-r.ref, -r.step, r.len, r.offset) function -(r::LinRange) start = -r.start LinRange{typeof(start)}(start, -r.stop, length(r)) end - # promote eltype if at least one container wouldn't change, otherwise join container types. -el_same(::Type{T}, a::Type{<:AbstractArray{T,n}}, b::Type{<:AbstractArray{T,n}}) where {T,n} = a +el_same(::Type{T}, a::Type{<:AbstractArray{T,n}}, b::Type{<:AbstractArray{T,n}}) where {T,n} = a # we assume a === b el_same(::Type{T}, a::Type{<:AbstractArray{T,n}}, b::Type{<:AbstractArray{S,n}}) where {T,S,n} = a el_same(::Type{T}, a::Type{<:AbstractArray{S,n}}, b::Type{<:AbstractArray{T,n}}) where {T,S,n} = b el_same(::Type, a, b) = promote_typejoin(a, b) +promote_result(::Type{<:AbstractArray}, ::Type{<:AbstractArray}, ::Type{T}, ::Type{S}) where {T,S} = (@inline; promote_type(T,S)) +promote_result(::Type{T}, ::Type{S}, ::Type{Bottom}, ::Type{Bottom}) where {T<:AbstractArray,S<:AbstractArray} = (@inline; promote_typejoin(T,S)) +# If no promote_rule is defined, both directions give Bottom. In that case use typejoin on the eltypes instead and give Array as the container. +promote_result(::Type{<:AbstractArray{T,n}}, ::Type{<:AbstractArray{S,n}}, ::Type{Bottom}, ::Type{Bottom}) where {T,S,n} = (@inline; Array{promote_type(T,S),n}) + promote_rule(a::Type{UnitRange{T1}}, b::Type{UnitRange{T2}}) where {T1,T2} = - el_same(promote_type(T1,T2), a, b) + el_same(promote_type(T1, T2), a, b) UnitRange{T}(r::UnitRange{T}) where {T<:Real} = r UnitRange{T}(r::UnitRange) where {T<:Real} = UnitRange{T}(r.start, r.stop) promote_rule(a::Type{OneTo{T1}}, b::Type{OneTo{T2}}) where {T1,T2} = - el_same(promote_type(T1,T2), a, b) + el_same(promote_type(T1, T2), a, b) OneTo{T}(r::OneTo{T}) where {T<:Integer} = r OneTo{T}(r::OneTo) where {T<:Integer} = OneTo{T}(r.stop) @@ -926,11 +1293,14 @@ AbstractUnitRange{T}(r::AbstractUnitRange{T}) where {T} = r AbstractUnitRange{T}(r::UnitRange) where {T} = UnitRange{T}(r) AbstractUnitRange{T}(r::OneTo) where {T} = OneTo{T}(r) -promote_rule(::Type{StepRange{T1a,T1b}}, ::Type{StepRange{T2a,T2b}}) where {T1a,T1b,T2a,T2b} = - el_same(promote_type(T1a,T2a), - # el_same only operates on array element type, so just promote second type parameter - StepRange{T1a, promote_type(T1b,T2b)}, - StepRange{T2a, promote_type(T1b,T2b)}) +OrdinalRange{T, S}(r::OrdinalRange) where {T, S} = StepRange{T, S}(r) +OrdinalRange{T, T}(r::AbstractUnitRange) where {T} = AbstractUnitRange{T}(r) + +function promote_rule(::Type{StepRange{T1a,T1b}}, ::Type{StepRange{T2a,T2b}}) where {T1a,T1b,T2a,T2b} + Tb = promote_type(T1b, T2b) + # el_same only operates on array element type, so just promote second type parameter + el_same(promote_type(T1a, T2a), StepRange{T1a,Tb}, StepRange{T2a,Tb}) +end StepRange{T1,T2}(r::StepRange{T1,T2}) where {T1,T2} = r promote_rule(a::Type{StepRange{T1a,T1b}}, ::Type{UR}) where {T1a,T1b,UR<:AbstractUnitRange} = @@ -941,35 +1311,38 @@ StepRange(r::AbstractUnitRange{T}) where {T} = StepRange{T,T}(first(r), step(r), last(r)) (StepRange{T1,T2} where T1)(r::AbstractRange) where {T2} = StepRange{eltype(r),T2}(r) -promote_rule(::Type{StepRangeLen{T1,R1,S1}},::Type{StepRangeLen{T2,R2,S2}}) where {T1,T2,R1,R2,S1,S2} = - el_same(promote_type(T1,T2), - StepRangeLen{T1,promote_type(R1,R2),promote_type(S1,S2)}, - StepRangeLen{T2,promote_type(R1,R2),promote_type(S1,S2)}) -StepRangeLen{T,R,S}(r::StepRangeLen{T,R,S}) where {T,R,S} = r -StepRangeLen{T,R,S}(r::StepRangeLen) where {T,R,S} = - StepRangeLen{T,R,S}(convert(R, r.ref), convert(S, r.step), length(r), r.offset) +function promote_rule(::Type{StepRangeLen{T1,R1,S1,L1}},::Type{StepRangeLen{T2,R2,S2,L2}}) where {T1,T2,R1,R2,S1,S2,L1,L2} + R, S, L = promote_type(R1, R2), promote_type(S1, S2), promote_type(L1, L2) + el_same(promote_type(T1, T2), StepRangeLen{T1,R,S,L}, StepRangeLen{T2,R,S,L}) +end +StepRangeLen{T,R,S,L}(r::StepRangeLen{T,R,S,L}) where {T,R,S,L} = r +StepRangeLen{T,R,S,L}(r::StepRangeLen) where {T,R,S,L} = + StepRangeLen{T,R,S,L}(convert(R, r.ref), convert(S, r.step), convert(L, r.len), convert(L, r.offset)) StepRangeLen{T}(r::StepRangeLen) where {T} = - StepRangeLen(convert(T, r.ref), convert(T, r.step), length(r), r.offset) + StepRangeLen(convert(T, r.ref), convert(T, r.step), r.len, r.offset) -promote_rule(a::Type{StepRangeLen{T,R,S}}, ::Type{OR}) where {T,R,S,OR<:AbstractRange} = - promote_rule(a, StepRangeLen{eltype(OR), eltype(OR), eltype(OR)}) -StepRangeLen{T,R,S}(r::AbstractRange) where {T,R,S} = - StepRangeLen{T,R,S}(R(first(r)), S(step(r)), length(r)) +promote_rule(a::Type{StepRangeLen{T,R,S,L}}, ::Type{OR}) where {T,R,S,L,OR<:AbstractRange} = + promote_rule(a, StepRangeLen{eltype(OR), eltype(OR), eltype(OR), Int}) +StepRangeLen{T,R,S,L}(r::AbstractRange) where {T,R,S,L} = + StepRangeLen{T,R,S,L}(R(first(r)), S(step(r)), length(r)) StepRangeLen{T}(r::AbstractRange) where {T} = StepRangeLen(T(first(r)), T(step(r)), length(r)) StepRangeLen(r::AbstractRange) = StepRangeLen{eltype(r)}(r) -promote_rule(a::Type{LinRange{T1}}, b::Type{LinRange{T2}}) where {T1,T2} = - el_same(promote_type(T1,T2), a, b) -LinRange{T}(r::LinRange{T}) where {T} = r -LinRange{T}(r::AbstractRange) where {T} = LinRange{T}(first(r), last(r), length(r)) +function promote_rule(a::Type{LinRange{T1,L1}}, b::Type{LinRange{T2,L2}}) where {T1,T2,L1,L2} + L = promote_type(L1, L2) + el_same(promote_type(T1, T2), LinRange{T1,L}, LinRange{T2,L}) +end +LinRange{T,L}(r::LinRange{T,L}) where {T,L} = r +LinRange{T,L}(r::AbstractRange) where {T,L} = LinRange{T,L}(first(r), last(r), length(r)) +LinRange{T}(r::AbstractRange) where {T} = LinRange{T,typeof(length(r))}(first(r), last(r), length(r)) LinRange(r::AbstractRange{T}) where {T} = LinRange{T}(r) -promote_rule(a::Type{LinRange{T}}, ::Type{OR}) where {T,OR<:OrdinalRange} = - promote_rule(a, LinRange{eltype(OR)}) +promote_rule(a::Type{LinRange{T,L}}, ::Type{OR}) where {T,L,OR<:OrdinalRange} = + promote_rule(a, LinRange{eltype(OR),L}) -promote_rule(::Type{LinRange{L}}, b::Type{StepRangeLen{T,R,S}}) where {L,T,R,S} = - promote_rule(StepRangeLen{L,L,L}, b) +promote_rule(::Type{LinRange{A,L}}, b::Type{StepRangeLen{T2,R2,S2,L2}}) where {A,L,T2,R2,S2,L2} = + promote_rule(StepRangeLen{A,A,A,L}, b) ## concatenation ## @@ -990,15 +1363,15 @@ end Array{T,1}(r::AbstractRange{T}) where {T} = vcat(r) collect(r::AbstractRange) = vcat(r) -_reverse(r::OrdinalRange, ::Colon) = (:)(last(r), -step(r), first(r)) +_reverse(r::OrdinalRange, ::Colon) = (:)(last(r), negate(step(r)), first(r)) function _reverse(r::StepRangeLen, ::Colon) # If `r` is empty, `length(r) - r.offset + 1 will be nonpositive hence # invalid. As `reverse(r)` is also empty, any offset would work so we keep # `r.offset` offset = isempty(r) ? r.offset : length(r)-r.offset+1 - StepRangeLen(r.ref, -r.step, length(r), offset) + return typeof(r)(r.ref, negate(r.step), length(r), offset) end -_reverse(r::LinRange{T}, ::Colon) where {T} = LinRange{T}(r.stop, r.start, length(r)) +_reverse(r::LinRange{T}, ::Colon) where {T} = typeof(r)(r.stop, r.start, length(r)) ## sorting ## @@ -1021,7 +1394,9 @@ function sum(r::AbstractRange{<:Real}) end function _in_range(x, r::AbstractRange) - if step(r) == 0 + if !isfinite(x) + return false + elseif iszero(step(r)) return !isempty(r) && first(r) == x else n = round(Integer, (x - first(r)) / step(r)) + 1 @@ -1036,11 +1411,13 @@ in(x::T, r::AbstractRange{T}) where {T} = _in_range(x, r) in(x::Integer, r::AbstractUnitRange{<:Integer}) = (first(r) <= x) & (x <= last(r)) in(x::Real, r::AbstractRange{T}) where {T<:Integer} = - isinteger(x) && !isempty(r) && x >= minimum(r) && x <= maximum(r) && - (mod(convert(T,x),step(r))-mod(first(r),step(r)) == 0) + isinteger(x) && !isempty(r) && + (iszero(step(r)) ? x == first(r) : (x >= minimum(r) && x <= maximum(r) && + (mod(convert(T,x),step(r))-mod(first(r),step(r)) == 0))) in(x::AbstractChar, r::AbstractRange{<:AbstractChar}) = - !isempty(r) && x >= minimum(r) && x <= maximum(r) && - (mod(Int(x) - Int(first(r)), step(r)) == 0) + !isempty(r) && + (iszero(step(r)) ? x == first(r) : (x >= minimum(r) && x <= maximum(r) && + (mod(Int(x) - Int(first(r)), step(r)) == 0))) # Addition/subtraction of ranges @@ -1050,7 +1427,7 @@ function _define_range_op(@nospecialize f) r1l = length(r1) (r1l == length(r2) || throw(DimensionMismatch("argument dimensions must match: length of r1 is $r1l, length of r2 is $(length(r2))"))) - range($f(first(r1), first(r2)), step=$f(step(r1), step(r2)), length=r1l) + StepRangeLen($f(first(r1), first(r2)), $f(step(r1), step(r2)), r1l) end function $f(r1::LinRange{T}, r2::LinRange{T}) where T @@ -1086,14 +1463,14 @@ end Find `y` in the range `r` such that ``x ≡ y (mod n)``, where `n = length(r)`, i.e. `y = mod(x - first(r), n) + first(r)`. -See also: [`mod1`](@ref). +See also [`mod1`](@ref). # Examples ```jldoctest -julia> mod(0, Base.OneTo(3)) +julia> mod(0, Base.OneTo(3)) # mod1(0, 3) 3 -julia> mod(3, 0:2) +julia> mod(3, 0:2) # mod(3, 3) 0 ``` diff --git a/base/rational.jl b/base/rational.jl index 1f7b0bea79ca41..9e887bdaefa91a 100644 --- a/base/rational.jl +++ b/base/rational.jl @@ -18,21 +18,22 @@ unsafe_rational(num::T, den::T) where {T<:Integer} = unsafe_rational(T, num, den unsafe_rational(num::Integer, den::Integer) = unsafe_rational(promote(num, den)...) @noinline __throw_rational_argerror_typemin(T) = throw(ArgumentError("invalid rational: denominator can't be typemin($T)")) -function checked_den(num::T, den::T) where T<:Integer +function checked_den(::Type{T}, num::T, den::T) where T<:Integer if signbit(den) den = -den - signbit(den) && __throw_rational_argerror_typemin(T) + signbit(den) && __throw_rational_argerror_typemin(typeof(den)) num = -num end return unsafe_rational(T, num, den) end +checked_den(num::T, den::T) where T<:Integer = checked_den(T, num, den) checked_den(num::Integer, den::Integer) = checked_den(promote(num, den)...) @noinline __throw_rational_argerror_zero(T) = throw(ArgumentError("invalid rational: zero($T)//zero($T)")) function Rational{T}(num::Integer, den::Integer) where T<:Integer iszero(den) && iszero(num) && __throw_rational_argerror_zero(T) num, den = divgcd(num, den) - return checked_den(T(num), T(den)) + return checked_den(T, T(num), T(den)) end Rational(n::T, d::T) where {T<:Integer} = Rational{T}(n, d) @@ -215,6 +216,8 @@ function rationalize(::Type{T}, x::AbstractFloat, tol::Real) where T<:Integer end rationalize(::Type{T}, x::AbstractFloat; tol::Real = eps(x)) where {T<:Integer} = rationalize(T, x, tol)::Rational{T} rationalize(x::AbstractFloat; kvs...) = rationalize(Int, x; kvs...) +rationalize(::Type{T}, x::Complex; kvs...) where {T<:Integer} = Complex(rationalize(T, x.re, kvs...)::Rational{T}, rationalize(T, x.im, kvs...)::Rational{T}) +rationalize(x::Complex; kvs...) = Complex(rationalize(Int, x.re, kvs...), rationalize(Int, x.im, kvs...)) """ numerator(x) @@ -262,6 +265,7 @@ typemin(::Type{Rational{T}}) where {T<:Integer} = unsafe_rational(T, zero(T), on typemax(::Type{Rational{T}}) where {T<:Integer} = unsafe_rational(T, one(T), zero(T)) isinteger(x::Rational) = x.den == 1 +ispow2(x::Rational) = ispow2(x.num) & ispow2(x.den) +(x::Rational) = unsafe_rational(+x.num, x.den) -(x::Rational) = unsafe_rational(-x.num, x.den) @@ -277,13 +281,35 @@ function -(x::Rational{T}) where T<:Unsigned x end -for (op,chop) in ((:+,:checked_add), (:-,:checked_sub), (:rem,:rem), (:mod,:mod)) +function +(x::Rational, y::Rational) + xp, yp = promote(x, y)::NTuple{2,Rational} + if isinf(x) && x == y + return xp + end + xd, yd = divgcd(promote(x.den, y.den)...) + Rational(checked_add(checked_mul(x.num,yd), checked_mul(y.num,xd)), checked_mul(x.den,yd)) +end + +function -(x::Rational, y::Rational) + xp, yp = promote(x, y)::NTuple{2,Rational} + if isinf(x) && x == -y + return xp + end + xd, yd = divgcd(promote(x.den, y.den)...) + Rational(checked_sub(checked_mul(x.num,yd), checked_mul(y.num,xd)), checked_mul(x.den,yd)) +end + +for (op,chop) in ((:rem,:rem), (:mod,:mod)) @eval begin function ($op)(x::Rational, y::Rational) xd, yd = divgcd(promote(x.den, y.den)...) Rational(($chop)(checked_mul(x.num,yd), checked_mul(y.num,xd)), checked_mul(x.den,yd)) end + end +end +for (op,chop) in ((:+,:checked_add), (:-,:checked_sub), (:rem,:rem), (:mod,:mod)) + @eval begin function ($op)(x::Rational, y::Integer) unsafe_rational(($chop)(x.num, checked_mul(x.den, y)), x.den) end @@ -485,3 +511,45 @@ function gcdx(x::Rational, y::Rational) end c, a, b end + +## streamlined hashing for smallish rational types ## + +decompose(x::Rational) = numerator(x), 0, denominator(x) +function hash(x::Rational{<:BitInteger64}, h::UInt) + num, den = Base.numerator(x), Base.denominator(x) + den == 1 && return hash(num, h) + den == 0 && return hash(ifelse(num > 0, Inf, -Inf), h) + if isodd(den) + pow = trailing_zeros(num) + num >>= pow + else + pow = trailing_zeros(den) + den >>= pow + pow = -pow + if den == 1 && abs(num) < 9007199254740992 + return hash(ldexp(Float64(num),pow),h) + end + end + h = hash_integer(den, h) + h = hash_integer(pow, h) + h = hash_integer(num, h) + return h +end + +# These methods are only needed for performance. Since `first(r)` and `last(r)` have the +# same denominator (because their difference is an integer), `length(r)` can be calulated +# without calling `gcd`. +function length(r::AbstractUnitRange{T}) where T<:Rational + @inline + f = first(r) + l = last(r) + return div(l.num - f.num + f.den, f.den) +end +function checked_length(r::AbstractUnitRange{T}) where T<:Rational + f = first(r) + l = last(r) + if isempty(r) + return f.num - f.num + end + return div(checked_add(checked_sub(l.num, f.num), f.den), f.den) +end diff --git a/base/reduce.jl b/base/reduce.jl index 4a6f27a5d947a0..1f59c61ea5d5b0 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -88,6 +88,8 @@ Create a mapping reducing function `rf′(acc, x) = rf(acc, f(x))`. struct MappingRF{F, T} f::F rf::T + MappingRF(f::F, rf::T) where {F,T} = new{F,T}(f, rf) + MappingRF(::Type{f}, rf::T) where {f,T} = new{Type{f},T}(f, rf) end @inline (op::MappingRF)(acc, x) = op.rf(acc, op.f(x)) @@ -166,6 +168,8 @@ Like [`reduce`](@ref), but with guaranteed left associativity. If provided, the argument `init` will be used exactly once. In general, it will be necessary to provide `init` to work with empty collections. +See also [`mapfoldl`](@ref), [`foldr`](@ref), [`accumulate`](@ref). + # Examples ```jldoctest julia> foldl(=>, 1:4) @@ -173,6 +177,9 @@ julia> foldl(=>, 1:4) julia> foldl(=>, 1:4; init=0) (((0 => 1) => 2) => 3) => 4 + +julia> accumulate(=>, (1,2,3,4)) +(1, 1 => 2, (1 => 2) => 3, ((1 => 2) => 3) => 4) ``` """ foldl(op, itr; kw...) = mapfoldl(identity, op, itr; kw...) @@ -235,7 +242,7 @@ foldr(op, itr; kw...) = mapfoldr(identity, op, itr; kw...) if ifirst == ilast @inbounds a1 = A[ifirst] return mapreduce_first(f, op, a1) - elseif ifirst + blksize > ilast + elseif ilast - ifirst < blksize # sequential portion @inbounds a1 = A[ifirst] @inbounds a2 = A[ifirst+1] @@ -247,7 +254,7 @@ foldr(op, itr; kw...) = mapfoldr(identity, op, itr; kw...) return v else # pairwise portion - imid = (ifirst + ilast) >> 1 + imid = ifirst + (ilast - ifirst) >> 1 v1 = mapreduce_impl(f, op, A, ifirst, imid, blksize) v2 = mapreduce_impl(f, op, A, imid+1, ilast, blksize) return op(v1, v2) @@ -297,6 +304,9 @@ pairwise_blocksize(::typeof(abs2), ::typeof(+)) = 4096 # handling empty arrays _empty_reduce_error() = throw(ArgumentError("reducing over an empty collection is not allowed")) +_empty_reduce_error(@nospecialize(f), @nospecialize(T::Type)) = throw(ArgumentError(""" + reducing with $f over an empty collection of element type $T is not allowed. + You may be able to prevent this error by supplying an `init` value to the reducer.""")) """ Base.reduce_empty(op, T) @@ -304,23 +314,32 @@ _empty_reduce_error() = throw(ArgumentError("reducing over an empty collection i The value to be returned when calling [`reduce`](@ref), [`foldl`](@ref) or [`foldr`](@ref) with reduction `op` over an empty array with element type of `T`. -If not defined, this will throw an `ArgumentError`. +This should only be defined in unambiguous cases; for example, + +```julia +Base.reduce_empty(::typeof(+), ::Type{T}) where T = zero(T) +``` + +is justified (the sum of zero elements is zero), whereas +`reduce_empty(::typeof(max), ::Type{Any})` is not (the maximum value of an empty collection +is generally ambiguous, and especially so when the element type is unknown). + +As an alternative, consider supplying an `init` value to the reducer. """ -reduce_empty(op, ::Type{T}) where {T} = _empty_reduce_error() -reduce_empty(::typeof(+), ::Type{Union{}}) = _empty_reduce_error() +reduce_empty(::typeof(+), ::Type{Union{}}) = _empty_reduce_error(+, Union{}) reduce_empty(::typeof(+), ::Type{T}) where {T} = zero(T) reduce_empty(::typeof(+), ::Type{Bool}) = zero(Int) -reduce_empty(::typeof(*), ::Type{Union{}}) = _empty_reduce_error() +reduce_empty(::typeof(*), ::Type{Union{}}) = _empty_reduce_error(*, Union{}) reduce_empty(::typeof(*), ::Type{T}) where {T} = one(T) reduce_empty(::typeof(*), ::Type{<:AbstractChar}) = "" reduce_empty(::typeof(&), ::Type{Bool}) = true reduce_empty(::typeof(|), ::Type{Bool}) = false -reduce_empty(::typeof(add_sum), ::Type{Union{}}) = _empty_reduce_error() +reduce_empty(::typeof(add_sum), ::Type{Union{}}) = _empty_reduce_error(add_sum, Union{}) reduce_empty(::typeof(add_sum), ::Type{T}) where {T} = reduce_empty(+, T) reduce_empty(::typeof(add_sum), ::Type{T}) where {T<:SmallSigned} = zero(Int) reduce_empty(::typeof(add_sum), ::Type{T}) where {T<:SmallUnsigned} = zero(UInt) -reduce_empty(::typeof(mul_prod), ::Type{Union{}}) = _empty_reduce_error() +reduce_empty(::typeof(mul_prod), ::Type{Union{}}) = _empty_reduce_error(mul_prod, Union{}) reduce_empty(::typeof(mul_prod), ::Type{T}) where {T} = reduce_empty(*, T) reduce_empty(::typeof(mul_prod), ::Type{T}) where {T<:SmallSigned} = one(Int) reduce_empty(::typeof(mul_prod), ::Type{T}) where {T<:SmallUnsigned} = one(UInt) @@ -335,11 +354,8 @@ reduce_empty(op::FlipArgs, ::Type{T}) where {T} = reduce_empty(op.f, T) The value to be returned when calling [`mapreduce`](@ref), [`mapfoldl`](@ref`) or [`mapfoldr`](@ref) with map `f` and reduction `op` over an empty array with element type -of `T`. - -If not defined, this will throw an `ArgumentError`. +of `T`. See [`Base.reduce_empty`](@ref) for more information. """ -mapreduce_empty(f, op, T) = _empty_reduce_error() mapreduce_empty(::typeof(identity), op, T) = reduce_empty(op, T) mapreduce_empty(::typeof(abs), op, T) = abs(reduce_empty(op, T)) mapreduce_empty(::typeof(abs2), op, T) = abs2(reduce_empty(op, T)) @@ -353,7 +369,10 @@ mapreduce_empty_iter(f, op, itr, ItrEltype) = @inline reduce_empty_iter(op, itr) = reduce_empty_iter(op, itr, IteratorEltype(itr)) @inline reduce_empty_iter(op, itr, ::HasEltype) = reduce_empty(op, eltype(itr)) -reduce_empty_iter(op, itr, ::EltypeUnknown) = _empty_reduce_error() +reduce_empty_iter(op, itr, ::EltypeUnknown) = throw(ArgumentError(""" + reducing over an empty collection of unknown element type is not allowed. + You may be able to prevent this error by supplying an `init` value to the reducer.""")) + # handling of single-element iterators """ @@ -516,6 +535,8 @@ for non-empty collections. !!! compat "Julia 1.6" Keyword argument `init` requires Julia 1.6 or later. +See also: [`reduce`](@ref), [`mapreduce`](@ref), [`count`](@ref), [`union`](@ref). + # Examples ```jldoctest julia> sum(1:20) @@ -527,7 +548,7 @@ julia> sum(1:20; init = 0.0) """ sum(a; kw...) = sum(identity, a; kw...) sum(a::AbstractArray{Bool}; kw...) = - kw.data === NamedTuple() ? count(a) : reduce(add_sum, a; kw...) + isempty(kw) ? count(a) : reduce(add_sum, a; kw...) ## prod """ @@ -570,6 +591,8 @@ for non-empty collections. !!! compat "Julia 1.6" Keyword argument `init` requires Julia 1.6 or later. +See also: [`reduce`](@ref), [`cumprod`](@ref), [`any`](@ref). + # Examples ```jldoctest julia> prod(1:5) @@ -581,7 +604,7 @@ julia> prod(1:5; init = 1.0) """ prod(a; kw...) = mapreduce(identity, mul_prod, a; kw...) -## maximum & minimum +## maximum, minimum, & extrema _fast(::typeof(min),x,y) = min(x,y) _fast(::typeof(max),x,y) = max(x,y) function _fast(::typeof(max), x::AbstractFloat, y::AbstractFloat) @@ -611,11 +634,6 @@ function mapreduce_impl(f, op::Union{typeof(max), typeof(min)}, start = first + 1 simdstop = start + chunk_len - 4 while simdstop <= last - 3 - # short circuit in case of NaN or missing - (v1 == v1) === true || return v1 - (v2 == v2) === true || return v2 - (v3 == v3) === true || return v3 - (v4 == v4) === true || return v4 @inbounds for i in start:4:simdstop v1 = _fast(op, v1, f(A[i+0])) v2 = _fast(op, v2, f(A[i+1])) @@ -720,7 +738,7 @@ julia> maximum([1,2,3]) 3 julia> maximum(()) -ERROR: ArgumentError: reducing over an empty collection is not allowed +ERROR: MethodError: reducing over an empty collection is not allowed; consider supplying `init` to the reducer Stacktrace: [...] @@ -752,7 +770,7 @@ julia> minimum([1,2,3]) 1 julia> minimum([]) -ERROR: ArgumentError: reducing over an empty collection is not allowed +ERROR: MethodError: reducing over an empty collection is not allowed; consider supplying `init` to the reducer Stacktrace: [...] @@ -762,18 +780,317 @@ Inf """ minimum(a; kw...) = mapreduce(identity, min, a; kw...) +""" + extrema(itr; [init]) -> (mn, mx) + +Compute both the minimum `mn` and maximum `mx` element in a single pass, and return them +as a 2-tuple. + +The value returned for empty `itr` can be specified by `init`. It must be a 2-tuple whose +first and second elements are neutral elements for `min` and `max` respectively +(i.e. which are greater/less than or equal to any other element). As a consequence, when +`itr` is empty the returned `(mn, mx)` tuple will satisfy `mn ≥ mx`. When `init` is +specified it may be used even for non-empty `itr`. + +!!! compat "Julia 1.8" + Keyword argument `init` requires Julia 1.8 or later. + +# Examples +```jldoctest +julia> extrema(2:10) +(2, 10) + +julia> extrema([9,pi,4.5]) +(3.141592653589793, 9.0) + +julia> extrema([]; init = (Inf, -Inf)) +(Inf, -Inf) +``` +""" +extrema(itr; kw...) = extrema(identity, itr; kw...) + +""" + extrema(f, itr; [init]) -> (mn, mx) + +Compute both the minimum `mn` and maximum `mx` of `f` applied to each element in `itr` and +return them as a 2-tuple. Only one pass is made over `itr`. + +The value returned for empty `itr` can be specified by `init`. It must be a 2-tuple whose +first and second elements are neutral elements for `min` and `max` respectively +(i.e. which are greater/less than or equal to any other element). It is used for non-empty +collections. Note: it implies that, for empty `itr`, the returned value `(mn, mx)` satisfies +`mn ≥ mx` even though for non-empty `itr` it satisfies `mn ≤ mx`. This is a "paradoxical" +but yet expected result. + +!!! compat "Julia 1.2" + This method requires Julia 1.2 or later. + +!!! compat "Julia 1.8" + Keyword argument `init` requires Julia 1.8 or later. + +# Examples +```jldoctest +julia> extrema(sin, 0:π) +(0.0, 0.9092974268256817) + +julia> extrema(sin, Real[]; init = (1.0, -1.0)) # good, since -1 ≤ sin(::Real) ≤ 1 +(1.0, -1.0) +``` +""" +extrema(f, itr; kw...) = mapreduce(ExtremaMap(f), _extrema_rf, itr; kw...) + +# Not using closure since `extrema(type, itr)` is a very likely use-case and it's better +# to avoid type-instability (#23618). +struct ExtremaMap{F} <: Function + f::F +end +ExtremaMap(::Type{T}) where {T} = ExtremaMap{Type{T}}(T) +@inline (f::ExtremaMap)(x) = (y = f.f(x); (y, y)) + +# TODO: optimize for inputs <: AbstractFloat +@inline _extrema_rf((min1, max1), (min2, max2)) = (min(min1, min2), max(max1, max2)) + +## findmax, findmin, argmax & argmin + +""" + findmax(f, domain) -> (f(x), index) + +Returns a pair of a value in the codomain (outputs of `f`) and the index of +the corresponding value in the `domain` (inputs to `f`) such that `f(x)` is maximised. +If there are multiple maximal points, then the first one will be returned. + +`domain` must be a non-empty iterable. + +Values are compared with `isless`. + +!!! compat "Julia 1.7" + This method requires Julia 1.7 or later. + +# Examples + +```jldoctest +julia> findmax(identity, 5:9) +(9, 5) + +julia> findmax(-, 1:10) +(-1, 1) + +julia> findmax(first, [(1, :a), (3, :b), (3, :c)]) +(3, 2) + +julia> findmax(cos, 0:π/2:2π) +(1.0, 1) +``` +""" +findmax(f, domain) = mapfoldl( ((k, v),) -> (f(v), k), _rf_findmax, pairs(domain) ) +_rf_findmax((fm, im), (fx, ix)) = isless(fm, fx) ? (fx, ix) : (fm, im) + +""" + findmax(itr) -> (x, index) + +Return the maximal element of the collection `itr` and its index or key. +If there are multiple maximal elements, then the first one will be returned. +Values are compared with `isless`. + +See also: [`findmin`](@ref), [`argmax`](@ref), [`maximum`](@ref). + +# Examples + +```jldoctest +julia> findmax([8, 0.1, -9, pi]) +(8.0, 1) + +julia> findmax([1, 7, 7, 6]) +(7, 2) + +julia> findmax([1, 7, 7, NaN]) +(NaN, 4) +``` +""" +findmax(itr) = _findmax(itr, :) +_findmax(a, ::Colon) = findmax(identity, a) + +""" + findmin(f, domain) -> (f(x), index) + +Returns a pair of a value in the codomain (outputs of `f`) and the index of +the corresponding value in the `domain` (inputs to `f`) such that `f(x)` is minimised. +If there are multiple minimal points, then the first one will be returned. + +`domain` must be a non-empty iterable. + +`NaN` is treated as less than all other values except `missing`. + +!!! compat "Julia 1.7" + This method requires Julia 1.7 or later. + +# Examples + +```jldoctest +julia> findmin(identity, 5:9) +(5, 1) + +julia> findmin(-, 1:10) +(-10, 10) + +julia> findmin(first, [(2, :a), (2, :b), (3, :c)]) +(2, 1) + +julia> findmin(cos, 0:π/2:2π) +(-1.0, 3) +``` + +""" +findmin(f, domain) = mapfoldl( ((k, v),) -> (f(v), k), _rf_findmin, pairs(domain) ) +_rf_findmin((fm, im), (fx, ix)) = isgreater(fm, fx) ? (fx, ix) : (fm, im) + +""" + findmin(itr) -> (x, index) + +Return the minimal element of the collection `itr` and its index or key. +If there are multiple minimal elements, then the first one will be returned. +`NaN` is treated as less than all other values except `missing`. + +See also: [`findmax`](@ref), [`argmin`](@ref), [`minimum`](@ref). + +# Examples + +```jldoctest +julia> findmin([8, 0.1, -9, pi]) +(-9.0, 3) + +julia> findmin([1, 7, 7, 6]) +(1, 1) + +julia> findmin([1, 7, 7, NaN]) +(NaN, 4) +``` +""" +findmin(itr) = _findmin(itr, :) +_findmin(a, ::Colon) = findmin(identity, a) + +""" + argmax(f, domain) + +Return a value `x` in the domain of `f` for which `f(x)` is maximised. +If there are multiple maximal values for `f(x)` then the first one will be found. + +`domain` must be a non-empty iterable. + +Values are compared with `isless`. + +!!! compat "Julia 1.7" + This method requires Julia 1.7 or later. + +See also [`argmin`](@ref), [`findmax`](@ref). + +# Examples +```jldoctest +julia> argmax(abs, -10:5) +-10 + +julia> argmax(cos, 0:π/2:2π) +0.0 +``` +""" +argmax(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmax, domain)[2] + +""" + argmax(itr) + +Return the index or key of the maximal element in a collection. +If there are multiple maximal elements, then the first one will be returned. + +The collection must not be empty. + +Values are compared with `isless`. + +See also: [`argmin`](@ref), [`findmax`](@ref). + +# Examples +```jldoctest +julia> argmax([8, 0.1, -9, pi]) +1 + +julia> argmax([1, 7, 7, 6]) +2 + +julia> argmax([1, 7, 7, NaN]) +4 +``` +""" +argmax(itr) = findmax(itr)[2] + +""" + argmin(f, domain) + +Return a value `x` in the domain of `f` for which `f(x)` is minimised. +If there are multiple minimal values for `f(x)` then the first one will be found. + +`domain` must be a non-empty iterable. + +`NaN` is treated as less than all other values except `missing`. + +!!! compat "Julia 1.7" + This method requires Julia 1.7 or later. + +See also [`argmax`](@ref), [`findmin`](@ref). + +# Examples +```jldoctest +julia> argmin(sign, -10:5) +-10 + +julia> argmin(x -> -x^3 + x^2 - 10, -5:5) +5 + +julia> argmin(acos, 0:0.1:1) +1.0 +``` +""" +argmin(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmin, domain)[2] + +""" + argmin(itr) + +Return the index or key of the minimal element in a collection. +If there are multiple minimal elements, then the first one will be returned. + +The collection must not be empty. + +`NaN` is treated as less than all other values except `missing`. + +See also: [`argmax`](@ref), [`findmin`](@ref). + +# Examples +```jldoctest +julia> argmin([8, 0.1, -9, pi]) +3 + +julia> argmin([7, 1, 1, 6]) +2 + +julia> argmin([7, 1, 1, NaN]) +4 +``` +""" +argmin(itr) = findmin(itr)[2] + ## all & any """ any(itr) -> Bool Test whether any elements of a boolean collection are `true`, returning `true` as -soon as the first `true` value in `itr` is encountered (short-circuiting). +soon as the first `true` value in `itr` is encountered (short-circuiting). To +short-circuit on `false`, use [`all`](@ref). If the input contains [`missing`](@ref) values, return `missing` if all non-missing values are `false` (or equivalently, if the input contains no `true` value), following [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic). +See also: [`all`](@ref), [`count`](@ref), [`sum`](@ref), [`|`](@ref), , [`||`](@ref). + # Examples ```jldoctest julia> a = [true,false,false,true] @@ -803,12 +1120,15 @@ any(itr) = any(identity, itr) all(itr) -> Bool Test whether all elements of a boolean collection are `true`, returning `false` as -soon as the first `false` value in `itr` is encountered (short-circuiting). +soon as the first `false` value in `itr` is encountered (short-circuiting). To +short-circuit on `true`, use [`any`](@ref). If the input contains [`missing`](@ref) values, return `missing` if all non-missing values are `true` (or equivalently, if the input contains no `false` value), following [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic). +See also: [`all!`](@ref), [`any`](@ref), [`count`](@ref), [`&`](@ref), , [`&&`](@ref), [`allunique`](@ref). + # Examples ```jldoctest julia> a = [true,false,false,true] @@ -840,7 +1160,7 @@ all(itr) = all(identity, itr) Determine whether predicate `p` returns `true` for any elements of `itr`, returning `true` as soon as the first item in `itr` for which `p` returns `true` is encountered -(short-circuiting). +(short-circuiting). To short-circuit on `false`, use [`all`](@ref). If the input contains [`missing`](@ref) values, return `missing` if all non-missing values are `false` (or equivalently, if the input contains no `true` value), following @@ -883,12 +1203,33 @@ function _any(f, itr, ::Colon) return anymissing ? missing : false end +# Specialized versions of any(f, ::Tuple), avoiding type instabilities for small tuples +# containing mixed types. +# We fall back to the for loop implementation all elements have the same type or +# if the tuple is too large. +any(f, itr::NTuple) = _any(f, itr, :) # case of homogeneous tuple +function any(f, itr::Tuple) # case of tuple with mixed types + length(itr) > 32 && return _any(f, itr, :) + _any_tuple(f, false, itr...) +end + +@inline function _any_tuple(f, anymissing, x, rest...) + v = f(x) + if ismissing(v) + anymissing = true + elseif v + return true + end + return _any_tuple(f, anymissing, rest...) +end +@inline _any_tuple(f, anymissing) = anymissing ? missing : false + """ all(p, itr) -> Bool Determine whether predicate `p` returns `true` for all elements of `itr`, returning `false` as soon as the first item in `itr` for which `p` returns `false` is encountered -(short-circuiting). +(short-circuiting). To short-circuit on `true`, use [`any`](@ref). If the input contains [`missing`](@ref) values, return `missing` if all non-missing values are `true` (or equivalently, if the input contains no `false` value), following @@ -933,17 +1274,44 @@ function _all(f, itr, ::Colon) return anymissing ? missing : true end +# Specialized versions of all(f, ::Tuple), avoiding type instabilities for small tuples +# containing mixed types. This is similar to any(f, ::Tuple) defined above. +all(f, itr::NTuple) = _all(f, itr, :) +function all(f, itr::Tuple) + length(itr) > 32 && return _all(f, itr, :) + _all_tuple(f, false, itr...) +end + +@inline function _all_tuple(f, anymissing, x, rest...) + v = f(x) + if ismissing(v) + anymissing = true + # this syntax allows throwing a TypeError for non-Bool, for consistency with any + elseif v + nothing + else + return false + end + return _all_tuple(f, anymissing, rest...) +end +@inline _all_tuple(f, anymissing) = anymissing ? missing : true + ## count _bool(f) = x->f(x)::Bool """ - count(p, itr) -> Integer - count(itr) -> Integer + count([f=identity,] itr; init=0) -> Integer + +Count the number of elements in `itr` for which the function `f` returns `true`. +If `f` is omitted, count the number of `true` elements in `itr` (which +should be a collection of boolean values). `init` optionally specifies the value +to start counting from and therefore also determines the output type. -Count the number of elements in `itr` for which predicate `p` returns `true`. -If `p` is omitted, counts the number of `true` elements in `itr` (which -should be a collection of boolean values). +!!! compat "Julia 1.6" + `init` keyword was added in Julia 1.6. + +See also: [`any`](@ref), [`sum`](@ref). # Examples ```jldoctest @@ -952,32 +1320,37 @@ julia> count(i->(4<=i<=6), [2,3,4,5,6]) julia> count([true, false, true, true]) 3 + +julia> count(>(3), 1:7, init=0x03) +0x07 ``` """ -count(itr) = count(identity, itr) +count(itr; init=0) = count(identity, itr; init) -count(f, itr) = _simple_count(f, itr) +count(f, itr; init=0) = _simple_count(f, itr, init) -function _simple_count(pred, itr) - n = 0 - for x in itr - n += pred(x)::Bool +_simple_count(pred, itr, init) = _simple_count_helper(Generator(pred, itr), init) + +function _simple_count_helper(g, init::T) where {T} + n::T = init + for x in g + n += x::Bool end return n end -function count(::typeof(identity), x::Array{Bool}) - n = 0 +function _simple_count(::typeof(identity), x::Array{Bool}, init::T=0) where {T} + n::T = init chunks = length(x) ÷ sizeof(UInt) mask = 0x0101010101010101 % UInt GC.@preserve x begin ptr = Ptr{UInt}(pointer(x)) for i in 1:chunks - n += count_ones(unsafe_load(ptr, i) & mask) + n = (n + count_ones(unsafe_load(ptr, i) & mask)) % T end end for i in sizeof(UInt)*chunks+1:length(x) - n += x[i] + n = (n + x[i]) % T end return n end diff --git a/base/reducedim.jl b/base/reducedim.jl index c889392ececd70..4ccf826df5865e 100644 --- a/base/reducedim.jl +++ b/base/reducedim.jl @@ -3,7 +3,7 @@ ## Functions to compute the reduced shape # for reductions that expand 0 dims to 1 -reduced_index(i::OneTo) = OneTo(1) +reduced_index(i::OneTo{T}) where {T} = OneTo(one(T)) reduced_index(i::Union{Slice, IdentityUnitRange}) = oftype(i, first(i):first(i)) reduced_index(i::AbstractUnitRange) = throw(ArgumentError( @@ -44,7 +44,7 @@ function reduced_indices0(inds::Indices{N}, d::Int) where N end function reduced_indices(inds::Indices{N}, region) where N - rinds = [inds...] + rinds = collect(inds) for i in region isa(i, Integer) || throw(ArgumentError("reduced dimension(s) must be integers")) d = Int(i) @@ -58,7 +58,7 @@ function reduced_indices(inds::Indices{N}, region) where N end function reduced_indices0(inds::Indices{N}, region) where N - rinds = [inds...] + rinds = collect(inds) for i in region isa(i, Integer) || throw(ArgumentError("reduced dimension(s) must be integers")) d = Int(i) @@ -77,15 +77,14 @@ end ## initialization # initarray! is only called by sum!, prod!, etc. for (Op, initfun) in ((:(typeof(add_sum)), :zero), (:(typeof(mul_prod)), :one)) - @eval initarray!(a::AbstractArray{T}, ::$(Op), init::Bool, src::AbstractArray) where {T} = (init && fill!(a, $(initfun)(T)); a) + @eval initarray!(a::AbstractArray{T}, ::Any, ::$(Op), init::Bool, src::AbstractArray) where {T} = (init && fill!(a, $(initfun)(T)); a) end -for Op in (:(typeof(max)), :(typeof(min))) - @eval initarray!(a::AbstractArray{T}, ::$(Op), init::Bool, src::AbstractArray) where {T} = (init && copyfirst!(a, src); a) -end +initarray!(a::AbstractArray{T}, f, ::Union{typeof(min),typeof(max),typeof(_extrema_rf)}, + init::Bool, src::AbstractArray) where {T} = (init && mapfirst!(f, a, src); a) for (Op, initval) in ((:(typeof(&)), true), (:(typeof(|)), false)) - @eval initarray!(a::AbstractArray, ::$(Op), init::Bool, src::AbstractArray) = (init && fill!(a, $initval); a) + @eval initarray!(a::AbstractArray, ::Any, ::$(Op), init::Bool, src::AbstractArray) = (init && fill!(a, $initval); a) end # reducedim_initarray is called by @@ -125,7 +124,7 @@ function _reducedim_init(f, op, fv, fop, A, region) end # initialization when computing minima and maxima requires a little care -for (f1, f2, initval) in ((:min, :max, :Inf), (:max, :min, :(-Inf))) +for (f1, f2, initval, typeextreme) in ((:min, :max, :Inf, :typemax), (:max, :min, :(-Inf), :typemin)) @eval function reducedim_init(f, op::typeof($f1), A::AbstractArray, region) # First compute the reduce indices. This will throw an ArgumentError # if any region is invalid @@ -139,20 +138,68 @@ for (f1, f2, initval) in ((:min, :max, :Inf), (:max, :min, :(-Inf))) if isempty(A1) # If the slice is empty just return non-view version as the initial array - return copy(A1) + return map(f, A1) else # otherwise use the min/max of the first slice as initial value v0 = mapreduce(f, $f2, A1) - # but NaNs need to be avoided as initial values - v0 = v0 != v0 ? typeof(v0)($initval) : v0 - T = _realtype(f, promote_union(eltype(A))) Tr = v0 isa T ? T : typeof(v0) + + # but NaNs and missing need to be avoided as initial values + if v0 isa Number && isnan(v0) + # v0 is NaN + v0 = oftype(v0, $initval) + elseif isunordered(v0) + # v0 is missing or a third-party unordered value + Tnm = nonmissingtype(Tr) + # TODO: Some types, like BigInt, don't support typemin/typemax. + # So a Matrix{Union{BigInt, Missing}} can still error here. + v0 = $typeextreme(Tnm) + end + # v0 may have changed type. + Tr = v0 isa T ? T : typeof(v0) + return reducedim_initarray(A, region, v0, Tr) end end end + +function reducedim_init(f::ExtremaMap, op::typeof(_extrema_rf), A::AbstractArray, region) + # First compute the reduce indices. This will throw an ArgumentError + # if any region is invalid + ri = reduced_indices(A, region) + + # Next, throw if reduction is over a region with length zero + any(i -> isempty(axes(A, i)), region) && _empty_reduce_error() + + # Make a view of the first slice of the region + A1 = view(A, ri...) + + isempty(A1) && return map(f, A1) + # use the max/min of the first slice as initial value for non-empty cases + v0 = reverse(mapreduce(f, op, A1)) # turn minmax to maxmin + + T = _realtype(f.f, promote_union(eltype(A))) + Tmin = v0[1] isa T ? T : typeof(v0[1]) + Tmax = v0[2] isa T ? T : typeof(v0[2]) + + # but NaNs and missing need to be avoided as initial values + if v0[1] isa Number && isnan(v0[1]) + v0 = oftype(v0[1], Inf), oftype(v0[2], -Inf) + elseif isunordered(v0[1]) + # v0 is missing or a third-party unordered value + # TODO: Some types, like BigInt, don't support typemin/typemax. + # So a Matrix{Union{BigInt, Missing}} can still error here. + v0 = typemax(nonmissingtype(Tmin)), typemin(nonmissingtype(Tmax)) + end + # v0 may have changed type. + Tmin = v0[1] isa T ? T : typeof(v0[1]) + Tmax = v0[2] isa T ? T : typeof(v0[2]) + + return reducedim_initarray(A, region, v0, Tuple{Tmin,Tmax}) +end + reducedim_init(f::Union{typeof(abs),typeof(abs2)}, op::typeof(max), A::AbstractArray{T}, region) where {T} = reducedim_initarray(A, region, zero(f(zero(T))), _realtype(f, T)) @@ -181,7 +228,7 @@ end has_fast_linear_indexing(a::AbstractArrayOrBroadcasted) = false has_fast_linear_indexing(a::Array) = true -has_fast_linear_indexing(::Number) = true # for Broadcasted +has_fast_linear_indexing(::Union{Number,Ref,AbstractChar}) = true # 0d objects, for Broadcasted has_fast_linear_indexing(bc::Broadcast.Broadcasted) = all(has_fast_linear_indexing, bc.args) @@ -283,7 +330,7 @@ reducedim!(op, R::AbstractArray{RT}, A::AbstractArrayOrBroadcasted) where {RT} = """ mapreduce(f, op, A::AbstractArray...; dims=:, [init]) -Evaluates to the same as `reduce(op, map(f, A); dims=dims, init=init)`, but is generally +Evaluates to the same as `reduce(op, map(f, A...); dims=dims, init=init)`, but is generally faster because the intermediate array is avoided. !!! compat "Julia 1.2" @@ -369,6 +416,9 @@ dimensions. !!! compat "Julia 1.5" `dims` keyword was added in Julia 1.5. +!!! compat "Julia 1.6" + `init` keyword was added in Julia 1.6. + # Examples ```jldoctest julia> A = [1 2; 3 4] @@ -386,11 +436,11 @@ julia> count(<=(2), A, dims=2) 0 ``` """ -count(A::AbstractArrayOrBroadcasted; dims=:) = count(identity, A, dims=dims) -count(f, A::AbstractArrayOrBroadcasted; dims=:) = _count(f, A, dims) +count(A::AbstractArrayOrBroadcasted; dims=:, init=0) = count(identity, A; dims, init) +count(f, A::AbstractArrayOrBroadcasted; dims=:, init=0) = _count(f, A, dims, init) -_count(f, A::AbstractArrayOrBroadcasted, dims::Colon) = _simple_count(f, A) -_count(f, A::AbstractArrayOrBroadcasted, dims) = mapreduce(_bool(f), add_sum, A, dims=dims, init=0) +_count(f, A::AbstractArrayOrBroadcasted, dims::Colon, init) = _simple_count(f, A, init) +_count(f, A::AbstractArrayOrBroadcasted, dims, init) = mapreduce(_bool(f), add_sum, A; dims, init) """ count!([f=identity,] r, A) @@ -420,7 +470,7 @@ julia> count!(<=(2), [1; 1], A) """ count!(r::AbstractArray, A::AbstractArrayOrBroadcasted; init::Bool=true) = count!(identity, r, A; init=init) count!(f, r::AbstractArray, A::AbstractArrayOrBroadcasted; init::Bool=true) = - mapreducedim!(_bool(f), add_sum, initarray!(r, add_sum, init, A), A) + mapreducedim!(_bool(f), add_sum, initarray!(r, f, add_sum, init, A), A) """ sum(A::AbstractArray; dims) @@ -575,6 +625,8 @@ Compute the maximum value of an array over the given dimensions. See also the [`max(a,b)`](@ref) function to take the maximum of two or more arguments, which can be applied elementwise to arrays via `max.(a,b)`. +See also: [`maximum!`](@ref), [`extrema`](@ref), [`findmax`](@ref), [`argmax`](@ref). + # Examples ```jldoctest julia> A = [1 2; 3 4] @@ -597,7 +649,7 @@ maximum(A::AbstractArray; dims) """ maximum(f, A::AbstractArray; dims) -Compute the maximum value from of calling the function `f` on each element of an array over the given +Compute the maximum value by calling the function `f` on each element of an array over the given dimensions. # Examples @@ -650,6 +702,8 @@ Compute the minimum value of an array over the given dimensions. See also the [`min(a,b)`](@ref) function to take the minimum of two or more arguments, which can be applied elementwise to arrays via `min.(a,b)`. +See also: [`minimum!`](@ref), [`extrema`](@ref), [`findmin`](@ref), [`argmin`](@ref). + # Examples ```jldoctest julia> A = [1 2; 3 4] @@ -672,7 +726,7 @@ minimum(A::AbstractArray; dims) """ minimum(f, A::AbstractArray; dims) -Compute the minimum value from of calling the function `f` on each element of an array over the given +Compute the minimum value by calling the function `f` on each element of an array over the given dimensions. # Examples @@ -718,6 +772,74 @@ julia> minimum!([1 1], A) """ minimum!(r, A) +""" + extrema(A::AbstractArray; dims) -> Array{Tuple} + +Compute the minimum and maximum elements of an array over the given dimensions. + +See also: [`minimum`](@ref), [`maximum`](@ref), [`extrema!`](@ref). + +# Examples +```jldoctest +julia> A = reshape(Vector(1:2:16), (2,2,2)) +2×2×2 Array{Int64, 3}: +[:, :, 1] = + 1 5 + 3 7 + +[:, :, 2] = + 9 13 + 11 15 + +julia> extrema(A, dims = (1,2)) +1×1×2 Array{Tuple{Int64, Int64}, 3}: +[:, :, 1] = + (1, 7) + +[:, :, 2] = + (9, 15) +``` +""" +extrema(A::AbstractArray; dims) + +""" + extrema(f, A::AbstractArray; dims) -> Array{Tuple} + +Compute the minimum and maximum of `f` applied to each element in the given dimensions +of `A`. + +!!! compat "Julia 1.2" + This method requires Julia 1.2 or later. +""" +extrema(f, A::AbstractArray; dims) + +""" + extrema!(r, A) + +Compute the minimum and maximum value of `A` over the singleton dimensions of `r`, and write results to `r`. + +!!! compat "Julia 1.8" + This method requires Julia 1.8 or later. + +# Examples +```jldoctest +julia> A = [1 2; 3 4] +2×2 Matrix{Int64}: + 1 2 + 3 4 + +julia> extrema!([(1, 1); (1, 1)], A) +2-element Vector{Tuple{Int64, Int64}}: + (1, 2) + (3, 4) + +julia> extrema!([(1, 1);; (1, 1)], A) +1×2 Matrix{Tuple{Int64, Int64}}: + (1, 3) (2, 4) +``` +""" +extrema!(r, A) + """ all(A; dims) @@ -745,7 +867,7 @@ all(A::AbstractArray; dims) """ all(p, A; dims) -Determine whether predicate p returns true for all elements along the given dimensions of an array. +Determine whether predicate `p` returns `true` for all elements along the given dimensions of an array. # Examples ```jldoctest @@ -817,7 +939,7 @@ any(::AbstractArray; dims) """ any(p, A; dims) -Determine whether predicate p returns true for any elements along the given dimensions of an array. +Determine whether predicate `p` returns `true` for any elements along the given dimensions of an array. # Examples ```jldoctest @@ -864,7 +986,9 @@ julia> any!([1 1], A) any!(r, A) for (fname, _fname, op) in [(:sum, :_sum, :add_sum), (:prod, :_prod, :mul_prod), - (:maximum, :_maximum, :max), (:minimum, :_minimum, :min)] + (:maximum, :_maximum, :max), (:minimum, :_minimum, :min), + (:extrema, :_extrema, :_extrema_rf)] + mapf = fname === :extrema ? :(ExtremaMap(f)) : :f @eval begin # User-facing methods with keyword arguments @inline ($fname)(a::AbstractArray; dims=:, kw...) = ($_fname)(a, dims; kw...) @@ -872,7 +996,7 @@ for (fname, _fname, op) in [(:sum, :_sum, :add_sum), (:prod, :_prod, # Underlying implementations using dispatch ($_fname)(a, ::Colon; kw...) = ($_fname)(identity, a, :; kw...) - ($_fname)(f, a, ::Colon; kw...) = mapreduce(f, $op, a; kw...) + ($_fname)(f, a, ::Colon; kw...) = mapreduce($mapf, $op, a; kw...) end end @@ -885,16 +1009,18 @@ _all(a, ::Colon) = _all(identity, a, :) for (fname, op) in [(:sum, :add_sum), (:prod, :mul_prod), (:maximum, :max), (:minimum, :min), - (:all, :&), (:any, :|)] + (:all, :&), (:any, :|), + (:extrema, :_extrema_rf)] fname! = Symbol(fname, '!') _fname = Symbol('_', fname) + mapf = fname === :extrema ? :(ExtremaMap(f)) : :f @eval begin $(fname!)(f::Function, r::AbstractArray, A::AbstractArray; init::Bool=true) = - mapreducedim!(f, $(op), initarray!(r, $(op), init, A), A) + mapreducedim!($mapf, $(op), initarray!(r, $mapf, $(op), init, A), A) $(fname!)(r::AbstractArray, A::AbstractArray; init::Bool=true) = $(fname!)(identity, r, A; init=init) $(_fname)(A, dims; kw...) = $(_fname)(identity, A, dims; kw...) - $(_fname)(f, A, dims; kw...) = mapreduce(f, $(op), A; dims=dims, kw...) + $(_fname)(f, A, dims; kw...) = mapreduce($mapf, $(op), A; dims=dims, kw...) end end @@ -923,7 +1049,7 @@ function findminmax!(f, Rval, Rind, A::AbstractArray{T,N}) where {T,N} for i in axes(A,1) k, kss = y::Tuple tmpAv = A[i,IA] - if tmpRi == zi || (tmpRv == tmpRv && (tmpAv != tmpAv || f(tmpAv, tmpRv))) + if tmpRi == zi || f(tmpRv, tmpAv) tmpRv = tmpAv tmpRi = k end @@ -940,7 +1066,7 @@ function findminmax!(f, Rval, Rind, A::AbstractArray{T,N}) where {T,N} tmpAv = A[i,IA] tmpRv = Rval[i,IR] tmpRi = Rind[i,IR] - if tmpRi == zi || (tmpRv == tmpRv && (tmpAv != tmpAv || f(tmpAv, tmpRv))) + if tmpRi == zi || f(tmpRv, tmpAv) Rval[i,IR] = tmpAv Rind[i,IR] = k end @@ -956,18 +1082,18 @@ end Find the minimum of `A` and the corresponding linear index along singleton dimensions of `rval` and `rind`, and store the results in `rval` and `rind`. -`NaN` is treated as less than all other values. +`NaN` is treated as less than all other values except `missing`. """ function findmin!(rval::AbstractArray, rind::AbstractArray, A::AbstractArray; init::Bool=true) - findminmax!(isless, init && !isempty(A) ? fill!(rval, first(A)) : rval, fill!(rind,zero(eltype(keys(A)))), A) + findminmax!(isgreater, init && !isempty(A) ? fill!(rval, first(A)) : rval, fill!(rind,zero(eltype(keys(A)))), A) end """ findmin(A; dims) -> (minval, index) For an array input, returns the value and index of the minimum over the given dimensions. -`NaN` is treated as less than all other values. +`NaN` is treated as less than all other values except `missing`. # Examples ```jldoctest @@ -980,7 +1106,7 @@ julia> findmin(A, dims=1) ([1.0 2.0], CartesianIndex{2}[CartesianIndex(1, 1) CartesianIndex(1, 2)]) julia> findmin(A, dims=2) -([1.0; 3.0], CartesianIndex{2}[CartesianIndex(1, 1); CartesianIndex(2, 1)]) +([1.0; 3.0;;], CartesianIndex{2}[CartesianIndex(1, 1); CartesianIndex(2, 1);;]) ``` """ findmin(A::AbstractArray; dims=:) = _findmin(A, dims) @@ -993,30 +1119,28 @@ function _findmin(A, region) end (similar(A, ri), zeros(eltype(keys(A)), ri)) else - findminmax!(isless, fill!(similar(A, ri), first(A)), + findminmax!(isgreater, fill!(similar(A, ri), first(A)), zeros(eltype(keys(A)), ri), A) end end -isgreater(a, b) = isless(b,a) - """ findmax!(rval, rind, A) -> (maxval, index) Find the maximum of `A` and the corresponding linear index along singleton dimensions of `rval` and `rind`, and store the results in `rval` and `rind`. -`NaN` is treated as greater than all other values. +`NaN` is treated as greater than all other values except `missing`. """ function findmax!(rval::AbstractArray, rind::AbstractArray, A::AbstractArray; init::Bool=true) - findminmax!(isgreater, init && !isempty(A) ? fill!(rval, first(A)) : rval, fill!(rind,zero(eltype(keys(A)))), A) + findminmax!(isless, init && !isempty(A) ? fill!(rval, first(A)) : rval, fill!(rind,zero(eltype(keys(A)))), A) end """ findmax(A; dims) -> (maxval, index) For an array input, returns the value and index of the maximum over the given dimensions. -`NaN` is treated as greater than all other values. +`NaN` is treated as greater than all other values except `missing`. # Examples ```jldoctest @@ -1029,7 +1153,7 @@ julia> findmax(A, dims=1) ([3.0 4.0], CartesianIndex{2}[CartesianIndex(2, 1) CartesianIndex(2, 2)]) julia> findmax(A, dims=2) -([2.0; 4.0], CartesianIndex{2}[CartesianIndex(1, 2); CartesianIndex(2, 2)]) +([2.0; 4.0;;], CartesianIndex{2}[CartesianIndex(1, 2); CartesianIndex(2, 2);;]) ``` """ findmax(A::AbstractArray; dims=:) = _findmax(A, dims) @@ -1042,7 +1166,7 @@ function _findmax(A, region) end similar(A, ri), zeros(eltype(keys(A)), ri) else - findminmax!(isgreater, fill!(similar(A, ri), first(A)), + findminmax!(isless, fill!(similar(A, ri), first(A)), zeros(eltype(keys(A)), ri), A) end end @@ -1053,7 +1177,7 @@ reducedim1(R, A) = length(axes1(R)) == 1 argmin(A; dims) -> indices For an array input, return the indices of the minimum elements over the given dimensions. -`NaN` is treated as less than all other values. +`NaN` is treated as less than all other values except `missing`. # Examples ```jldoctest @@ -1078,7 +1202,7 @@ argmin(A::AbstractArray; dims=:) = findmin(A; dims=dims)[2] argmax(A; dims) -> indices For an array input, return the indices of the maximum elements over the given dimensions. -`NaN` is treated as greater than all other values. +`NaN` is treated as greater than all other values except `missing`. # Examples ```jldoctest diff --git a/base/reflection.jl b/base/reflection.jl index 031426531b9e10..7e0003c0e651b6 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -20,6 +20,8 @@ nameof(m::Module) = ccall(:jl_module_name, Ref{Symbol}, (Any,), m) Get a module's enclosing `Module`. `Main` is its own parent. +See also: [`names`](@ref), [`nameof`](@ref), [`fullname`](@ref), [`@__MODULE__`](@ref). + # Examples ```jldoctest julia> parentmodule(Main) @@ -94,6 +96,8 @@ are also included. As a special case, all names defined in `Main` are considered \"exported\", since it is not idiomatic to explicitly export names from `Main`. + +See also: [`@locals`](@ref Base.@locals), [`@__MODULE__`](@ref). """ names(m::Module; all::Bool = false, imported::Bool = false) = sort!(ccall(:jl_module_names, Array{Symbol,1}, (Any, Cint, Cint), m, all, imported)) @@ -125,7 +129,7 @@ function _fieldnames(@nospecialize t) throw(ArgumentError("type does not have definite field names")) end end - isdefined(t, :names) ? t.names : t.name.names + return t.name.names end """ @@ -143,15 +147,19 @@ julia> fieldname(Rational, 2) ``` """ function fieldname(t::DataType, i::Integer) - if t.abstract - throw(ArgumentError("type does not have definite field names")) + throw_not_def_field() = throw(ArgumentError("type does not have definite field names")) + function throw_field_access(t, i, n_fields) + field_label = n_fields == 1 ? "field" : "fields" + throw(ArgumentError("Cannot access field $i since type $t only has $n_fields $field_label.")) end + throw_need_pos_int(i) = throw(ArgumentError("Field numbers must be positive integers. $i is invalid.")) + + isabstracttype(t) && throw_not_def_field() names = _fieldnames(t) n_fields = length(names)::Int - field_label = n_fields == 1 ? "field" : "fields" - i > n_fields && throw(ArgumentError("Cannot access field $i since type $t only has $n_fields $field_label.")) - i < 1 && throw(ArgumentError("Field numbers must be positive integers. $i is invalid.")) - return names[i]::Symbol + i > n_fields && throw_field_access(t, i, n_fields) + i < 1 && throw_need_pos_int(i) + return @inbounds names[i]::Symbol end fieldname(t::UnionAll, i::Integer) = fieldname(unwrap_unionall(t), i) @@ -163,10 +171,15 @@ fieldname(t::Type{<:Tuple}, i::Integer) = Get a tuple with the names of the fields of a `DataType`. +See also [`propertynames`](@ref), [`hasfield`](@ref). + # Examples ```jldoctest julia> fieldnames(Rational) (:num, :den) + +julia> fieldnames(typeof(1+im)) +(:re, :im) ``` """ fieldnames(t::DataType) = (fieldcount(t); # error check to make sure type is specific enough @@ -181,13 +194,25 @@ fieldnames(t::Type{<:Tuple}) = ntuple(identity, fieldcount(t)) Return a boolean indicating whether `T` has `name` as one of its own fields. +See also [`fieldnames`](@ref), [`fieldcount`](@ref), [`hasproperty`](@ref). + !!! compat "Julia 1.2" This function requires at least Julia 1.2. + +# Examples +```jldoctest +julia> struct Foo + bar::Int + end + +julia> hasfield(Foo, :bar) +true + +julia> hasfield(Foo, :x) +false +``` """ -function hasfield(T::Type, name::Symbol) - @_pure_meta - return fieldindex(T, name, false) > 0 -end +hasfield(T::Type, name::Symbol) = fieldindex(T, name, false) > 0 """ nameof(t::DataType) -> Symbol @@ -235,11 +260,34 @@ parentmodule(t::UnionAll) = parentmodule(unwrap_unionall(t)) """ isconst(m::Module, s::Symbol) -> Bool -Determine whether a global is declared `const` in a given `Module`. +Determine whether a global is declared `const` in a given module `m`. """ isconst(m::Module, s::Symbol) = ccall(:jl_is_const, Cint, (Any, Any), m, s) != 0 +""" + isconst(t::DataType, s::Union{Int,Symbol}) -> Bool + +Determine whether a field `s` is declared `const` in a given type `t`. +""" +function isconst(@nospecialize(t::Type), s::Symbol) + t = unwrap_unionall(t) + isa(t, DataType) || return false + return isconst(t, fieldindex(t, s, false)) +end +function isconst(@nospecialize(t::Type), s::Int) + t = unwrap_unionall(t) + # TODO: what to do for `Union`? + isa(t, DataType) || return false # uncertain + ismutabletype(t) || return true # immutable structs are always const + 1 <= s <= length(t.name.names) || return true # OOB reads are "const" since they always throw + constfields = t.name.constfields + constfields === C_NULL && return false + s -= 1 + return unsafe_load(Ptr{UInt32}(constfields), 1 + s÷32) & (1 << (s%32)) != 0 +end + + """ @locals() @@ -280,15 +328,17 @@ macro locals() end """ - objectid(x) + objectid(x) -> UInt Get a hash value for `x` based on object identity. `objectid(x)==objectid(y)` if `x === y`. + +See also [`hash`](@ref), [`IdDict`](@ref). """ objectid(@nospecialize(x)) = ccall(:jl_object_id, UInt, (Any,), x) # concrete datatype predicates -datatype_fieldtypes(x::DataType) = ccall(:jl_get_fieldtypes, Any, (Any,), x) +datatype_fieldtypes(x::DataType) = ccall(:jl_get_fieldtypes, Core.SimpleVector, (Any,), x) struct DataTypeLayout nfields::UInt32 @@ -307,28 +357,30 @@ Memory allocation minimum alignment for instances of this type. Can be called on any `isconcretetype`. """ function datatype_alignment(dt::DataType) - @_pure_meta + @_total_may_throw_meta dt.layout == C_NULL && throw(UndefRefError()) alignment = unsafe_load(convert(Ptr{DataTypeLayout}, dt.layout)).alignment return Int(alignment) end -function uniontype_layout(T::Type) +function uniontype_layout(@nospecialize T::Type) sz = RefValue{Csize_t}(0) algn = RefValue{Csize_t}(0) isinline = ccall(:jl_islayout_inline, Cint, (Any, Ptr{Csize_t}, Ptr{Csize_t}), T, sz, algn) != 0 - (isinline, sz[], algn[]) + (isinline, Int(sz[]), Int(algn[])) end +LLT_ALIGN(x, sz) = (x + sz - 1) & -sz + # amount of total space taken by T when stored in a container -function aligned_sizeof(T::Type) - @_pure_meta +function aligned_sizeof(@nospecialize T::Type) + @_total_may_throw_meta if isbitsunion(T) _, sz, al = uniontype_layout(T) - return (sz + al - 1) & -al + return LLT_ALIGN(sz, al) elseif allocatedinline(T) al = datatype_alignment(T) - return (Core.sizeof(T) + al - 1) & -al + return LLT_ALIGN(Core.sizeof(T), al) else return Core.sizeof(Ptr{Cvoid}) end @@ -345,7 +397,7 @@ with no intervening padding bytes. Can be called on any `isconcretetype`. """ function datatype_haspadding(dt::DataType) - @_pure_meta + @_total_may_throw_meta dt.layout == C_NULL && throw(UndefRefError()) flags = unsafe_load(convert(Ptr{DataTypeLayout}, dt.layout)).flags return flags & 1 == 1 @@ -358,12 +410,11 @@ Return the number of fields known to this datatype's layout. Can be called on any `isconcretetype`. """ function datatype_nfields(dt::DataType) - @_pure_meta + @_total_may_throw_meta dt.layout == C_NULL && throw(UndefRefError()) return unsafe_load(convert(Ptr{DataTypeLayout}, dt.layout)).nfields end - """ Base.datatype_pointerfree(dt::DataType) -> Bool @@ -371,7 +422,7 @@ Return whether instances of this type can contain references to gc-managed memor Can be called on any `isconcretetype`. """ function datatype_pointerfree(dt::DataType) - @_pure_meta + @_total_may_throw_meta dt.layout == C_NULL && throw(UndefRefError()) npointers = unsafe_load(convert(Ptr{DataTypeLayout}, dt.layout)).npointers return npointers == 0 @@ -387,19 +438,64 @@ Can be called on any `isconcretetype`. See also [`fieldoffset`](@ref). """ function datatype_fielddesc_type(dt::DataType) - @_pure_meta + @_total_may_throw_meta dt.layout == C_NULL && throw(UndefRefError()) flags = unsafe_load(convert(Ptr{DataTypeLayout}, dt.layout)).flags return (flags >> 1) & 3 end +# For type stability, we only expose a single struct that describes everything +struct FieldDesc + isforeign::Bool + isptr::Bool + size::UInt32 + offset::UInt32 +end + +struct FieldDescStorage{T} + ptrsize::T + offset::T +end +FieldDesc(fd::FieldDescStorage{T}) where {T} = + FieldDesc(false, fd.ptrsize & 1 != 0, + fd.ptrsize >> 1, fd.offset) + +struct DataTypeFieldDesc + dt::DataType + function DataTypeFieldDesc(dt::DataType) + dt.layout == C_NULL && throw(UndefRefError()) + new(dt) + end +end + +function getindex(dtfd::DataTypeFieldDesc, i::Int) + layout_ptr = convert(Ptr{DataTypeLayout}, dtfd.dt.layout) + fd_ptr = layout_ptr + sizeof(DataTypeLayout) + layout = unsafe_load(layout_ptr) + fielddesc_type = (layout.flags >> 1) & 3 + nfields = layout.nfields + @boundscheck ((1 <= i <= nfields) || throw(BoundsError(dtfd, i))) + if fielddesc_type == 0 + return FieldDesc(unsafe_load(Ptr{FieldDescStorage{UInt8}}(fd_ptr), i)) + elseif fielddesc_type == 1 + return FieldDesc(unsafe_load(Ptr{FieldDescStorage{UInt16}}(fd_ptr), i)) + elseif fielddesc_type == 2 + return FieldDesc(unsafe_load(Ptr{FieldDescStorage{UInt32}}(fd_ptr), i)) + else + # fielddesc_type == 3 + return FieldDesc(true, true, 0, 0) + end +end + """ ismutable(v) -> Bool -Return `true` iff value `v` is mutable. See [Mutable Composite Types](@ref) +Return `true` if and only if value `v` is mutable. See [Mutable Composite Types](@ref) for a discussion of immutability. Note that this function works on values, so if you give it a type, it will tell you that a value of `DataType` is mutable. +See also [`isbits`](@ref), [`isstructtype`](@ref). + # Examples ```jldoctest julia> ismutable(1) @@ -412,7 +508,23 @@ true !!! compat "Julia 1.5" This function requires at least Julia 1.5. """ -ismutable(@nospecialize(x)) = (@_pure_meta; typeof(x).mutable) +ismutable(@nospecialize(x)) = (@_total_meta; typeof(x).name.flags & 0x2 == 0x2) + +""" + ismutabletype(T) -> Bool + +Determine whether type `T` was declared as a mutable type +(i.e. using `mutable struct` keyword). + +!!! compat "Julia 1.7" + This function requires at least Julia 1.7. +""" +function ismutabletype(@nospecialize t) + @_total_meta + t = unwrap_unionall(t) + # TODO: what to do for `Union`? + return isa(t, DataType) && t.name.flags & 0x2 == 0x2 +end """ isstructtype(T) -> Bool @@ -420,13 +532,13 @@ ismutable(@nospecialize(x)) = (@_pure_meta; typeof(x).mutable) Determine whether type `T` was declared as a struct type (i.e. using the `struct` or `mutable struct` keyword). """ -function isstructtype(@nospecialize(t::Type)) - @_pure_meta +function isstructtype(@nospecialize t) + @_total_meta t = unwrap_unionall(t) # TODO: what to do for `Union`? isa(t, DataType) || return false hasfield = !isdefined(t, :types) || !isempty(t.types) - return hasfield || (t.size == 0 && !t.abstract) + return hasfield || (t.size == 0 && !isabstracttype(t)) end """ @@ -435,13 +547,13 @@ end Determine whether type `T` was declared as a primitive type (i.e. using the `primitive` keyword). """ -function isprimitivetype(@nospecialize(t::Type)) - @_pure_meta +function isprimitivetype(@nospecialize t) + @_total_meta t = unwrap_unionall(t) # TODO: what to do for `Union`? isa(t, DataType) || return false hasfield = !isdefined(t, :types) || !isempty(t.types) - return !hasfield && t.size != 0 && !t.abstract + return !hasfield && t.size != 0 && !isabstracttype(t) end """ @@ -456,6 +568,8 @@ This category of types is significant since they are valid as type parameters, may not track [`isdefined`](@ref) / [`isassigned`](@ref) status, and have a defined layout that is compatible with C. +See also [`isbits`](@ref), [`isprimitivetype`](@ref), [`ismutable`](@ref). + # Examples ```jldoctest julia> isbitstype(Complex{Float64}) @@ -465,14 +579,14 @@ julia> isbitstype(Complex) false ``` """ -isbitstype(@nospecialize(t::Type)) = (@_pure_meta; isa(t, DataType) && t.isbitstype) +isbitstype(@nospecialize t) = (@_total_meta; isa(t, DataType) && (t.flags & 0x8) == 0x8) """ isbits(x) -Return `true` if `x` is an instance of an `isbitstype` type. +Return `true` if `x` is an instance of an [`isbitstype`](@ref) type. """ -isbits(@nospecialize x) = (@_pure_meta; typeof(x).isbitstype) +isbits(@nospecialize x) = (@_total_meta; typeof(x).flags & 0x8 == 0x8) """ isdispatchtuple(T) @@ -481,7 +595,7 @@ Determine whether type `T` is a tuple "leaf type", meaning it could appear as a type signature in dispatch and has no subtypes (or supertypes) which could appear in a call. """ -isdispatchtuple(@nospecialize(t)) = (@_pure_meta; isa(t, DataType) && t.isdispatchtuple) +isdispatchtuple(@nospecialize(t)) = (@_total_meta; isa(t, DataType) && (t.flags & 0x4) == 0x4) iskindtype(@nospecialize t) = (t === DataType || t === UnionAll || t === Union || t === typeof(Bottom)) isconcretedispatch(@nospecialize t) = isconcretetype(t) && !iskindtype(t) @@ -501,6 +615,8 @@ end Determine whether type `T` is a concrete type, meaning it could have direct instances (values `x` such that `typeof(x) === T`). +See also: [`isbits`](@ref), [`isabstracttype`](@ref), [`issingletontype`](@ref). + # Examples ```jldoctest julia> isconcretetype(Complex) @@ -522,7 +638,7 @@ julia> isconcretetype(Union{Int,String}) false ``` """ -isconcretetype(@nospecialize(t)) = (@_pure_meta; isa(t, DataType) && t.isconcretetype) +isconcretetype(@nospecialize(t)) = (@_total_meta; isa(t, DataType) && (t.flags & 0x2) == 0x2) """ isabstracttype(T) @@ -540,10 +656,10 @@ false ``` """ function isabstracttype(@nospecialize(t)) - @_pure_meta + @_total_meta t = unwrap_unionall(t) # TODO: what to do for `Union`? - return isa(t, DataType) && t.abstract + return isa(t, DataType) && (t.name.flags & 0x1) == 0x1 end """ @@ -552,17 +668,17 @@ end Determine whether type `T` has exactly one possible instance; for example, a struct type with no fields. """ -issingletontype(@nospecialize(t)) = (@_pure_meta; isa(t, DataType) && isdefined(t, :instance)) +issingletontype(@nospecialize(t)) = (@_total_meta; isa(t, DataType) && isdefined(t, :instance)) """ - typeintersect(T, S) + typeintersect(T::Type, S::Type) Compute a type that contains the intersection of `T` and `S`. Usually this will be the smallest such type or one close to it. """ -typeintersect(@nospecialize(a), @nospecialize(b)) = (@_pure_meta; ccall(:jl_type_intersection, Any, (Any, Any), a, b)) +typeintersect(@nospecialize(a), @nospecialize(b)) = (@_total_meta; ccall(:jl_type_intersection, Any, (Any, Any), a::Type, b::Type)) -morespecific(@nospecialize(a), @nospecialize(b)) = ccall(:jl_type_morespecific, Cint, (Any, Any), a, b) != 0 +morespecific(@nospecialize(a), @nospecialize(b)) = (@_total_meta; ccall(:jl_type_morespecific, Cint, (Any, Any), a::Type, b::Type) != 0) """ fieldoffset(type, i) @@ -574,22 +690,23 @@ use it in the following manner to summarize information about a struct: julia> structinfo(T) = [(fieldoffset(T,i), fieldname(T,i), fieldtype(T,i)) for i = 1:fieldcount(T)]; julia> structinfo(Base.Filesystem.StatStruct) -12-element Vector{Tuple{UInt64, Symbol, DataType}}: - (0x0000000000000000, :device, UInt64) - (0x0000000000000008, :inode, UInt64) - (0x0000000000000010, :mode, UInt64) - (0x0000000000000018, :nlink, Int64) - (0x0000000000000020, :uid, UInt64) - (0x0000000000000028, :gid, UInt64) - (0x0000000000000030, :rdev, UInt64) - (0x0000000000000038, :size, Int64) - (0x0000000000000040, :blksize, Int64) - (0x0000000000000048, :blocks, Int64) - (0x0000000000000050, :mtime, Float64) - (0x0000000000000058, :ctime, Float64) +13-element Vector{Tuple{UInt64, Symbol, Type}}: + (0x0000000000000000, :desc, Union{RawFD, String}) + (0x0000000000000008, :device, UInt64) + (0x0000000000000010, :inode, UInt64) + (0x0000000000000018, :mode, UInt64) + (0x0000000000000020, :nlink, Int64) + (0x0000000000000028, :uid, UInt64) + (0x0000000000000030, :gid, UInt64) + (0x0000000000000038, :rdev, UInt64) + (0x0000000000000040, :size, Int64) + (0x0000000000000048, :blksize, Int64) + (0x0000000000000050, :blocks, Int64) + (0x0000000000000058, :mtime, Float64) + (0x0000000000000060, :ctime, Float64) ``` """ -fieldoffset(x::DataType, idx::Integer) = (@_pure_meta; ccall(:jl_get_field_offset, Csize_t, (Any, Cint), x, idx)) +fieldoffset(x::DataType, idx::Integer) = (@_total_may_throw_meta; ccall(:jl_get_field_offset, Csize_t, (Any, Cint), x, idx)) """ fieldtype(T, name::Symbol | index::Int) @@ -635,12 +752,22 @@ julia> Base.fieldindex(Foo, :z, false) ``` """ function fieldindex(T::DataType, name::Symbol, err::Bool=true) + @_total_may_throw_meta return Int(ccall(:jl_field_index, Cint, (Any, Any, Cint), T, name, err)+1) end -fieldindex(t::UnionAll, name::Symbol, err::Bool=true) = fieldindex(something(argument_datatype(t)), name, err) +function fieldindex(t::UnionAll, name::Symbol, err::Bool=true) + t = argument_datatype(t) + if t === nothing + throw(ArgumentError("type does not have definite fields")) + end + return fieldindex(t, name, err) +end -argument_datatype(@nospecialize t) = ccall(:jl_argument_datatype, Any, (Any,), t) +function argument_datatype(@nospecialize t) + @_total_meta + return ccall(:jl_argument_datatype, Any, (Any,), t)::Union{Nothing,DataType} +end """ fieldcount(t::Type) @@ -649,20 +776,20 @@ Get the number of fields that an instance of the given type would have. An error is thrown if the type is too abstract to determine this. """ function fieldcount(@nospecialize t) + @_total_may_throw_meta if t isa UnionAll || t isa Union t = argument_datatype(t) if t === nothing throw(ArgumentError("type does not have a definite number of fields")) end - t = t::DataType - elseif t == Union{} + elseif t === Union{} throw(ArgumentError("The empty type does not have a well-defined number of fields since it does not have instances.")) end if !(t isa DataType) throw(TypeError(:fieldcount, DataType, t)) end if t.name === NamedTuple_typename - names, types = t.parameters + names, types = t.parameters[1], t.parameters[2] if names isa Tuple return length(names) end @@ -671,7 +798,7 @@ function fieldcount(@nospecialize t) end abstr = true else - abstr = t.abstract || (t.name === Tuple.name && isvatuple(t)) + abstr = isabstracttype(t) || (t.name === Tuple.name && isvatuple(t)) end if abstr throw(ArgumentError("type does not have a definite number of fields")) @@ -701,7 +828,7 @@ julia> fieldtypes(Foo) (Int64, String) ``` """ -fieldtypes(T::Type) = ntupleany(i -> fieldtype(T, i), fieldcount(T)) +fieldtypes(T::Type) = (@_total_may_throw_meta; ntupleany(i -> fieldtype(T, i), fieldcount(T))) # return all instances, for types that can be enumerated @@ -727,6 +854,9 @@ function to_tuple_type(@nospecialize(t)) end if isa(t, Type) && t <: Tuple for p in unwrap_unionall(t).parameters + if isa(p, Core.TypeofVararg) + p = p.T + end if !(isa(p, Type) || isa(p, TypeVar)) error("argument tuple type must contain only types") end @@ -773,7 +903,7 @@ function code_lowered(@nospecialize(f), @nospecialize(t=Tuple); generated::Bool= throw(ArgumentError("'debuginfo' must be either :source or :none")) end return map(method_instances(f, t)) do m - if generated && isgenerated(m) + if generated && hasgenerator(m) if may_invoke_generator(m) return ccall(:jl_code_for_staged, Any, (Any,), m)::CodeInfo else @@ -788,8 +918,8 @@ function code_lowered(@nospecialize(f), @nospecialize(t=Tuple); generated::Bool= end end -isgenerated(m::Method) = isdefined(m, :generator) -isgenerated(m::Core.MethodInstance) = isgenerated(m.def) +hasgenerator(m::Method) = isdefined(m, :generator) +hasgenerator(m::Core.MethodInstance) = hasgenerator(m.def::Method) # low-level method lookup functions used by the compiler @@ -806,27 +936,25 @@ function _methods(@nospecialize(f), @nospecialize(t), lim::Int, world::UInt) end function _methods_by_ftype(@nospecialize(t), lim::Int, world::UInt) - return _methods_by_ftype(t, lim, world, false, UInt[typemin(UInt)], UInt[typemax(UInt)], Cint[0]) + return _methods_by_ftype(t, nothing, lim, world) end -function _methods_by_ftype(@nospecialize(t), lim::Int, world::UInt, ambig::Bool, min::Array{UInt,1}, max::Array{UInt,1}, has_ambig::Array{Int32,1}) - return ccall(:jl_matching_methods, Any, (Any, Cint, Cint, UInt, Ptr{UInt}, Ptr{UInt}, Ptr{Int32}), t, lim, ambig, world, min, max, has_ambig)::Union{Array{Any,1}, Bool} +function _methods_by_ftype(@nospecialize(t), mt::Union{Core.MethodTable, Nothing}, lim::Int, world::UInt) + return _methods_by_ftype(t, mt, lim, world, false, RefValue{UInt}(typemin(UInt)), RefValue{UInt}(typemax(UInt)), Ptr{Int32}(C_NULL)) end -function _methods_by_ftype(@nospecialize(t), lim::Int, world::UInt, ambig::Bool, min::Ref{UInt}, max::Ref{UInt}, has_ambig::Ref{Int32}) - return ccall(:jl_matching_methods, Any, (Any, Cint, Cint, UInt, Ptr{UInt}, Ptr{UInt}, Ptr{Int32}), t, lim, ambig, world, min, max, has_ambig)::Union{Array{Any,1}, Bool} +function _methods_by_ftype(@nospecialize(t), mt::Union{Core.MethodTable, Nothing}, lim::Int, world::UInt, ambig::Bool, min::Ref{UInt}, max::Ref{UInt}, has_ambig::Ref{Int32}) + return ccall(:jl_matching_methods, Any, (Any, Any, Cint, Cint, UInt, Ptr{UInt}, Ptr{UInt}, Ptr{Int32}), t, mt, lim, ambig, world, min, max, has_ambig)::Union{Array{Any,1}, Bool} end # high-level, more convenient method lookup functions # type for reflecting and pretty-printing a subset of methods -mutable struct MethodList +mutable struct MethodList <: AbstractArray{Method,1} ms::Array{Method,1} mt::Core.MethodTable end -length(m::MethodList) = length(m.ms) -isempty(m::MethodList) = isempty(m.ms) -iterate(m::MethodList, s...) = iterate(m.ms, s...) -eltype(::Type{MethodList}) = Method +size(m::MethodList) = size(m.ms) +getindex(m::MethodList, i::Integer) = m.ms[i] function MethodList(mt::Core.MethodTable) ms = Method[] @@ -847,39 +975,34 @@ A list of modules can also be specified as an array. !!! compat "Julia 1.4" At least Julia 1.4 is required for specifying a module. + +See also: [`which`](@ref) and `@which`. """ function methods(@nospecialize(f), @nospecialize(t), - @nospecialize(mod::Union{Tuple{Module},AbstractArray{Module},Nothing}=nothing)) - if isa(f, Core.Builtin) - throw(ArgumentError("argument is not a generic function")) - end + mod::Union{Tuple{Module},AbstractArray{Module},Nothing}=nothing) t = to_tuple_type(t) - world = typemax(UInt) + world = get_world_counter() # Lack of specialization => a comprehension triggers too many invalidations via _collect, so collect the methods manually ms = Method[] - for m in _methods(f, t, -1, world) - m::Core.MethodMatch + for m in _methods(f, t, -1, world)::Vector + m = m::Core.MethodMatch (mod === nothing || m.method.module ∈ mod) && push!(ms, m.method) end MethodList(ms, typeof(f).name.mt) end methods(@nospecialize(f), @nospecialize(t), mod::Module) = methods(f, t, (mod,)) -methods(f::Core.Builtin) = MethodList(Method[], typeof(f).name.mt) - function methods_including_ambiguous(@nospecialize(f), @nospecialize(t)) tt = signature_type(f, t) - world = typemax(UInt) - min = UInt[typemin(UInt)] - max = UInt[typemax(UInt)] - has_ambig = Int32[0] - ms = _methods_by_ftype(tt, -1, world, true, min, max, has_ambig) - isa(ms, Bool) && return ms + world = get_world_counter() + min = RefValue{UInt}(typemin(UInt)) + max = RefValue{UInt}(typemax(UInt)) + ms = _methods_by_ftype(tt, nothing, -1, world, true, min, max, Ptr{Int32}(C_NULL))::Vector return MethodList(Method[(m::Core.MethodMatch).method for m in ms], typeof(f).name.mt) end function methods(@nospecialize(f), - @nospecialize(mod::Union{Module,AbstractArray{Module},Nothing}=nothing)) + mod::Union{Module,AbstractArray{Module},Nothing}=nothing) # return all matches return methods(f, Tuple{Vararg{Any}}, mod) end @@ -944,12 +1067,11 @@ _uncompressed_ir(ci::Core.CodeInstance, s::Array{UInt8,1}) = ccall(:jl_uncompres const uncompressed_ast = uncompressed_ir const _uncompressed_ast = _uncompressed_ir -function method_instances(@nospecialize(f), @nospecialize(t), world::UInt = typemax(UInt)) +function method_instances(@nospecialize(f), @nospecialize(t), world::UInt=get_world_counter()) tt = signature_type(f, t) results = Core.MethodInstance[] - for match in _methods_by_ftype(tt, -1, world) - instance = ccall(:jl_specializations_get_linfo, Ref{MethodInstance}, - (Any, Any, Any), match.method, match.spec_types, match.sparams) + for match in _methods_by_ftype(tt, -1, world)::Vector + instance = Core.Compiler.specialize_method(match) push!(results, instance) end return results @@ -986,10 +1108,10 @@ const SLOT_USED = 0x8 ast_slotflag(@nospecialize(code), i) = ccall(:jl_ir_slotflag, UInt8, (Any, Csize_t), code, i - 1) """ - may_invoke_generator(method, atypes, sparams) + may_invoke_generator(method, atype, sparams) Computes whether or not we may invoke the generator for the given `method` on -the given atypes and sparams. For correctness, all generated function are +the given atype and sparams. For correctness, all generated function are required to return monotonic answers. However, since we don't expect users to be able to successfully implement this criterion, we only call generated functions on concrete types. The one exception to this is that we allow calling @@ -1003,9 +1125,9 @@ in some cases, but this may still allow inference not to fall over in some limit function may_invoke_generator(method::MethodInstance) return may_invoke_generator(method.def::Method, method.specTypes, method.sparam_vals) end -function may_invoke_generator(method::Method, @nospecialize(atypes), sparams::SimpleVector) +function may_invoke_generator(method::Method, @nospecialize(atype), sparams::SimpleVector) # If we have complete information, we may always call the generator - isdispatchtuple(atypes) && return true + isdispatchtuple(atype) && return true # We don't have complete information, but it is possible that the generator # syntactically doesn't make use of the information we don't have. Check @@ -1023,7 +1145,7 @@ function may_invoke_generator(method::Method, @nospecialize(atypes), sparams::Si isdefined(generator_method, :source) || return false code = generator_method.source nslots = ccall(:jl_ir_nslots, Int, (Any,), code) - at = unwrap_unionall(atypes)::DataType + at = unwrap_unionall(atype)::DataType (nslots >= 1 + length(sparams) + length(at.parameters)) || return false for i = 1:nsparams @@ -1053,21 +1175,47 @@ function func_for_method_checked(m::Method, @nospecialize(types), sparams::Simpl end """ - code_typed(f, types; optimize=true, debuginfo=:default) + code_typed(f, types; kw...) Returns an array of type-inferred lowered form (IR) for the methods matching the given -generic function and type signature. The keyword argument `optimize` controls whether -additional optimizations, such as inlining, are also applied. -The keyword `debuginfo` controls the amount of code metadata present in the output, +generic function and type signature. + +# Keyword Arguments + +- `optimize=true`: controls whether additional optimizations, such as inlining, are also applied. +- `debuginfo=:default`: controls the amount of code metadata present in the output, possible options are `:source` or `:none`. + +# Internal Keyword Arguments + +This section should be considered internal, and is only for who understands Julia compiler +internals. + +- `world=Base.get_world_counter()`: optional, controls the world age to use when looking up methods, +use current world age if not specified. +- `interp=Core.Compiler.NativeInterpreter(world)`: optional, controls the interpreter to use, +use the native interpreter Julia uses if not specified. + +# Example + +One can put the argument types in a tuple to get the corresponding `code_typed`. + +```julia +julia> code_typed(+, (Float64, Float64)) +1-element Vector{Any}: + CodeInfo( +1 ─ %1 = Base.add_float(x, y)::Float64 +└── return %1 +) => Float64 +``` """ -function code_typed(@nospecialize(f), @nospecialize(types=Tuple); +function code_typed(@nospecialize(f), @nospecialize(types=default_tt(f)); optimize=true, debuginfo::Symbol=:default, world = get_world_counter(), interp = Core.Compiler.NativeInterpreter(world)) - if isa(f, Core.Builtin) - throw(ArgumentError("argument is not a generic function")) + if isa(f, Core.OpaqueClosure) + return code_typed_opaque_closure(f; optimize, debuginfo, interp) end ft = Core.Typeof(f) if isa(types, Type) @@ -1079,6 +1227,18 @@ function code_typed(@nospecialize(f), @nospecialize(types=Tuple); return code_typed_by_type(tt; optimize, debuginfo, world, interp) end +# returns argument tuple type which is supposed to be used for `code_typed` and its family; +# if there is a single method this functions returns the method argument signature, +# otherwise returns `Tuple` that doesn't match with any signature +function default_tt(@nospecialize(f)) + ms = methods(f) + if length(ms) == 1 + return tuple_type_tail(only(ms).sig) + else + return Tuple + end +end + """ code_typed_by_type(types::Type{<:Tuple}; ...) @@ -1100,38 +1260,85 @@ function code_typed_by_type(@nospecialize(tt::Type); throw(ArgumentError("'debuginfo' must be either :source or :none")) end tt = to_tuple_type(tt) - matches = _methods_by_ftype(tt, -1, world) - if matches === false - error("signature does not correspond to a generic function") - end + matches = _methods_by_ftype(tt, -1, world)::Vector asts = [] for match in matches + match = match::Core.MethodMatch meth = func_for_method_checked(match.method, tt, match.sparams) (code, ty) = Core.Compiler.typeinf_code(interp, meth, match.spec_types, match.sparams, optimize) - code === nothing && error("inference not successful") # inference disabled? - debuginfo === :none && remove_linenums!(code) - push!(asts, code => ty) + if code === nothing + push!(asts, meth => Any) + else + debuginfo === :none && remove_linenums!(code) + push!(asts, code => ty) + end end return asts end -function return_types(@nospecialize(f), @nospecialize(types=Tuple), interp=Core.Compiler.NativeInterpreter()) +function code_typed_opaque_closure(@nospecialize(oc::Core.OpaqueClosure); + debuginfo::Symbol=:default, __...) ccall(:jl_is_in_pure_context, Bool, ()) && error("code reflection cannot be used from generated functions") - if isa(f, Core.Builtin) - throw(ArgumentError("argument is not a generic function")) + m = oc.source + if isa(m, Method) + code = _uncompressed_ir(m, m.source) + debuginfo === :none && remove_linenums!(code) + # intersect the declared return type and the inferred return type (if available) + rt = typeintersect(code.rettype, typeof(oc).parameters[2]) + return Any[code => rt] + else + error("encountered invalid Core.OpaqueClosure object") + end +end + +function return_types(@nospecialize(f), @nospecialize(types=default_tt(f)); + world = get_world_counter(), + interp = Core.Compiler.NativeInterpreter(world)) + ccall(:jl_is_in_pure_context, Bool, ()) && error("code reflection cannot be used from generated functions") + if isa(f, Core.OpaqueClosure) + _, rt = only(code_typed_opaque_closure(f)) + return Any[rt] end types = to_tuple_type(types) rt = [] - world = get_world_counter() - for match in _methods(f, types, -1, world) + for match in _methods(f, types, -1, world)::Vector + match = match::Core.MethodMatch meth = func_for_method_checked(match.method, types, match.sparams) ty = Core.Compiler.typeinf_type(interp, meth, match.spec_types, match.sparams) - ty === nothing && error("inference not successful") # inference disabled? - push!(rt, ty) + push!(rt, something(ty, Any)) end return rt end +function infer_effects(@nospecialize(f), @nospecialize(types=default_tt(f)); + world = get_world_counter(), + interp = Core.Compiler.NativeInterpreter(world)) + ccall(:jl_is_in_pure_context, Bool, ()) && error("code reflection cannot be used from generated functions") + types = to_tuple_type(types) + if isa(f, Core.Builtin) + args = Any[types.parameters...] + rt = Core.Compiler.builtin_tfunction(interp, f, args, nothing) + return Core.Compiler.builtin_effects(f, args, rt) + else + effects = Core.Compiler.EFFECTS_TOTAL + matches = _methods(f, types, -1, world)::Vector + if isempty(matches) + # although this call is known to throw MethodError (thus `nothrow=ALWAYS_FALSE`), + # still mark it `TRISTATE_UNKNOWN` just in order to be consistent with a result + # derived by the effect analysis, which can't prove guaranteed throwness at this moment + return Core.Compiler.Effects(effects; nothrow=Core.Compiler.TRISTATE_UNKNOWN) + end + for match in matches + match = match::Core.MethodMatch + frame = Core.Compiler.typeinf_frame(interp, + match.method, match.spec_types, match.sparams, #=run_optimizer=#false) + frame === nothing && return Core.Compiler.Effects() + effects = Core.Compiler.tristate_merge(effects, frame.ipo_effects) + end + return effects + end +end + """ print_statement_costs(io::IO, f, types) @@ -1139,9 +1346,6 @@ Print type-inferred and optimized code for `f` given argument types `types`, prepending each line with its cost as estimated by the compiler's inlining engine. """ function print_statement_costs(io::IO, @nospecialize(f), @nospecialize(t); kwargs...) - if isa(f, Core.Builtin) - throw(ArgumentError("argument is not a generic function")) - end tt = signature_type(f, t) print_statement_costs(io, tt; kwargs...) end @@ -1149,39 +1353,51 @@ end function print_statement_costs(io::IO, @nospecialize(tt::Type); world = get_world_counter(), interp = Core.Compiler.NativeInterpreter(world)) - matches = _methods_by_ftype(tt, -1, world) - if matches === false - error("signature does not correspond to a generic function") - end + matches = _methods_by_ftype(tt, -1, world)::Vector params = Core.Compiler.OptimizationParams(interp) cst = Int[] for match in matches + match = match::Core.MethodMatch meth = func_for_method_checked(match.method, tt, match.sparams) - (code, ty) = Core.Compiler.typeinf_code(interp, meth, match.spec_types, match.sparams, true) - code === nothing && error("inference not successful") # inference disabled? - empty!(cst) - resize!(cst, length(code.code)) - maxcost = Core.Compiler.statement_costs!(cst, code.code, code, Any[match.sparams...], false, params) - nd = ndigits(maxcost) println(io, meth) - IRShow.show_ir(io, code, (io, linestart, idx) -> (print(io, idx > 0 ? lpad(cst[idx], nd+1) : " "^(nd+1), " "); return "")) - println() + (code, ty) = Core.Compiler.typeinf_code(interp, meth, match.spec_types, match.sparams, true) + if code === nothing + println(io, " inference not successful") + else + empty!(cst) + resize!(cst, length(code.code)) + maxcost = Core.Compiler.statement_costs!(cst, code.code, code, Any[match.sparams...], false, params) + nd = ndigits(maxcost) + irshow_config = IRShow.IRShowConfig() do io, linestart, idx + print(io, idx > 0 ? lpad(cst[idx], nd+1) : " "^(nd+1), " ") + return "" + end + IRShow.show_ir(io, code, irshow_config) + end + println(io) end end print_statement_costs(args...; kwargs...) = print_statement_costs(stdout, args...; kwargs...) +function _which(@nospecialize(tt::Type), world=get_world_counter()) + match, _ = Core.Compiler._findsup(tt, nothing, world) + if match === nothing + error("no unique matching method found for the specified argument types") + end + return match +end + """ which(f, types) Returns the method of `f` (a `Method` object) that would be called for arguments of the given `types`. If `types` is an abstract type, then the method that would be called by `invoke` is returned. + +See also: [`parentmodule`](@ref), and `@which` and `@edit` in [`InteractiveUtils`](@ref man-interactive-utils). """ function which(@nospecialize(f), @nospecialize(t)) - if isa(f, Core.Builtin) - throw(ArgumentError("argument is not a generic function")) - end t = to_tuple_type(t) tt = signature_type(f, t) return which(tt) @@ -1192,12 +1408,8 @@ end Returns the method that would be called by the given type signature (as a tuple type). """ -function which(@nospecialize(tt::Type)) - m = ccall(:jl_gf_invoke_lookup, Any, (Any, UInt), tt, typemax(UInt)) - if m === nothing - error("no unique matching method found for the specified argument types") - end - return m::Method +function which(@nospecialize(tt#=::Type=#)) + return _which(tt).method end """ @@ -1258,7 +1470,7 @@ function parentmodule(@nospecialize(f), @nospecialize(types)) end """ - hasmethod(f, t::Type{<:Tuple}[, kwnames]; world=typemax(UInt)) -> Bool + hasmethod(f, t::Type{<:Tuple}[, kwnames]; world=get_world_counter()) -> Bool Determine whether the given generic function has a method matching the given `Tuple` of argument types with the upper bound of world age given by `world`. @@ -1293,13 +1505,13 @@ julia> hasmethod(g, Tuple{}, (:a, :b, :c, :d)) # g accepts arbitrary kwargs true ``` """ -function hasmethod(@nospecialize(f), @nospecialize(t); world=typemax(UInt)) +function hasmethod(@nospecialize(f), @nospecialize(t); world::UInt=get_world_counter()) t = to_tuple_type(t) t = signature_type(f, t) - return ccall(:jl_gf_invoke_lookup, Any, (Any, UInt), t, world) !== nothing + return ccall(:jl_gf_invoke_lookup, Any, (Any, Any, UInt), t, nothing, world) !== nothing end -function hasmethod(@nospecialize(f), @nospecialize(t), kwnames::Tuple{Vararg{Symbol}}; world=typemax(UInt)) +function hasmethod(@nospecialize(f), @nospecialize(t), kwnames::Tuple{Vararg{Symbol}}; world::UInt=get_world_counter()) # TODO: this appears to be doing the wrong queries hasmethod(f, t, world=world) || return false isempty(kwnames) && return true @@ -1330,7 +1542,7 @@ function bodyfunction(basemethod::Method) # %1 = mkw(kwvalues..., #self#, args...) # return %1 # where `mkw` is the name of the "active" keyword body-function. - ast = Base.uncompressed_ast(basemethod) + ast = uncompressed_ast(basemethod) f = nothing if isa(ast, Core.CodeInfo) && length(ast.code) >= 2 callexpr = ast.code[end-1] @@ -1363,9 +1575,11 @@ Determine whether two methods `m1` and `m2` may be ambiguous for some call signature. This test is performed in the context of other methods of the same function; in isolation, `m1` and `m2` might be ambiguous, but if a third method resolving the ambiguity has been defined, this returns `false`. +Alternatively, in isolation `m1` and `m2` might be ordered, but if a third +method cannot be sorted with them, they may cause an ambiguity together. For parametric types, the `ambiguous_bottom` keyword argument controls whether -`Union{}` counts as an ambiguous intersection of type parameters – when `true`, +`Union{}` counts as an ambiguous intersection of type parameters – when `true`, it is considered ambiguous, when `false` it is not. # Examples @@ -1389,27 +1603,89 @@ false ``` """ function isambiguous(m1::Method, m2::Method; ambiguous_bottom::Bool=false) - # TODO: eagerly returning `morespecific` is wrong, and fails to consider - # the possibility of an ambiguity caused by a third method: - # see the precise algorithm in ml_matches for a more correct computation - if m1 === m2 || morespecific(m1.sig, m2.sig) || morespecific(m2.sig, m1.sig) - return false - end + m1 === m2 && return false ti = typeintersect(m1.sig, m2.sig) - (ti <: m1.sig && ti <: m2.sig) || return false # XXX: completely wrong, obviously ti === Bottom && return false - if !ambiguous_bottom - has_bottom_parameter(ti) && return false - end - matches = _methods_by_ftype(ti, -1, typemax(UInt)) - for match in matches - m = match.method - m === m1 && continue - m === m2 && continue - if ti <: m.sig && morespecific(m.sig, m1.sig) && morespecific(m.sig, m2.sig) + function inner(ti) + ti === Bottom && return false + if !ambiguous_bottom + has_bottom_parameter(ti) && return false + end + world = get_world_counter() + min = Ref{UInt}(typemin(UInt)) + max = Ref{UInt}(typemax(UInt)) + has_ambig = Ref{Int32}(0) + ms = _methods_by_ftype(ti, nothing, -1, world, true, min, max, has_ambig)::Vector + has_ambig[] == 0 && return false + if !ambiguous_bottom + filter!(ms) do m::Core.MethodMatch + return !has_bottom_parameter(m.spec_types) + end + end + # if ml-matches reported the existence of an ambiguity over their + # intersection, see if both m1 and m2 may be involved in it + have_m1 = have_m2 = false + for match in ms + match = match::Core.MethodMatch + m = match.method + m === m1 && (have_m1 = true) + m === m2 && (have_m2 = true) + end + if !have_m1 || !have_m2 + # ml-matches did not need both methods to expose the reported ambiguity + return false + end + if !ambiguous_bottom + # since we're intentionally ignoring certain ambiguities (via the + # filter call above), see if we can now declare the intersection fully + # covered even though it is partially ambiguous over Union{} as a type + # parameter somewhere + minmax = nothing + for match in ms + m = match.method + match.fully_covers || continue + if minmax === nothing || morespecific(m.sig, minmax.sig) + minmax = m + end + end + if minmax === nothing + return true + end + for match in ms + m = match.method + m === minmax && continue + if match.fully_covers + if !morespecific(minmax.sig, m.sig) + return true + end + else + if morespecific(m.sig, minmax.sig) + return true + end + end + end return false end + return true + end + if !(ti <: m1.sig && ti <: m2.sig) + # When type-intersection fails, it's often also not commutative. Thus + # checking the reverse may allow detecting ambiguity solutions + # correctly in more cases (and faster). + ti2 = typeintersect(m2.sig, m1.sig) + if ti2 <: m1.sig && ti2 <: m2.sig + ti = ti2 + elseif ti != ti2 + # TODO: this would be the correct way to handle this case, but + # people complained so we don't do it + # inner(ti2) || return false + return false # report that the type system failed to decide if it was ambiguous by saying they definitely aren't + else + return false # report that the type system failed to decide if it was ambiguous by saying they definitely aren't + end end + inner(ti) || return false + # otherwise type-intersection reported an ambiguity we couldn't solve return true end @@ -1449,7 +1725,6 @@ min_world(m::Core.CodeInfo) = m.min_world max_world(m::Core.CodeInfo) = m.max_world get_world_counter() = ccall(:jl_get_world_counter, UInt, ()) - """ propertynames(x, private=false) @@ -1462,10 +1737,12 @@ as well to get the properties of an instance of the type. of the documented interface of `x`. If you want it to also return "private" fieldnames intended for internal use, pass `true` for the optional second argument. REPL tab completion on `x.` shows only the `private=false` properties. + +See also: [`hasproperty`](@ref), [`hasfield`](@ref). """ propertynames(x) = fieldnames(typeof(x)) propertynames(m::Module) = names(m) -propertynames(x, private) = propertynames(x) # ignore private flag by default +propertynames(x, private::Bool) = propertynames(x) # ignore private flag by default """ hasproperty(x, s::Symbol) @@ -1474,5 +1751,70 @@ Return a boolean indicating whether the object `x` has `s` as one of its own pro !!! compat "Julia 1.2" This function requires at least Julia 1.2. + +See also: [`propertynames`](@ref), [`hasfield`](@ref). """ hasproperty(x, s::Symbol) = s in propertynames(x) + +""" + @invoke f(arg::T, ...; kwargs...) + +Provides a convenient way to call [`invoke`](@ref); +`@invoke f(arg1::T1, arg2::T2; kwargs...)` will be expanded into `invoke(f, Tuple{T1,T2}, arg1, arg2; kwargs...)`. +When an argument's type annotation is omitted, it's specified as `Any` argument, e.g. +`@invoke f(arg1::T, arg2)` will be expanded into `invoke(f, Tuple{T,Any}, arg1, arg2)`. + +!!! compat "Julia 1.7" + This macro requires Julia 1.7 or later. +""" +macro invoke(ex) + f, args, kwargs = destructure_callex(ex) + newargs, newargtypes = Any[], Any[] + for i = 1:length(args) + x = args[i] + if isexpr(x, :(::)) + a = x.args[1] + t = x.args[2] + else + a = x + t = GlobalRef(Core, :Any) + end + push!(newargs, a) + push!(newargtypes, t) + end + return esc(:($(GlobalRef(Core, :invoke))($(f), Tuple{$(newargtypes...)}, $(newargs...); $(kwargs...)))) +end + +""" + @invokelatest f(args...; kwargs...) + +Provides a convenient way to call [`Base.invokelatest`](@ref). +`@invokelatest f(args...; kwargs...)` will simply be expanded into +`Base.invokelatest(f, args...; kwargs...)`. + +!!! compat "Julia 1.7" + This macro requires Julia 1.7 or later. +""" +macro invokelatest(ex) + f, args, kwargs = destructure_callex(ex) + return esc(:($(GlobalRef(@__MODULE__, :invokelatest))($(f), $(args...); $(kwargs...)))) +end + +function destructure_callex(ex) + isexpr(ex, :call) || throw(ArgumentError("a call expression f(args...; kwargs...) should be given")) + + f = first(ex.args) + args = [] + kwargs = [] + for x in ex.args[2:end] + if isexpr(x, :parameters) + append!(kwargs, x.args) + elseif isexpr(x, :kw) + push!(kwargs, x) + else + push!(args, x) + end + end + + return f, args, kwargs +end diff --git a/base/refpointer.jl b/base/refpointer.jl index 8ce30c6bded3a0..cd179c87b30d5d 100644 --- a/base/refpointer.jl +++ b/base/refpointer.jl @@ -24,6 +24,15 @@ If `T` is a bitstype, `isassigned(Ref{T}())` will always be true. When passed as a `ccall` argument (either as a `Ptr` or `Ref` type), a `Ref` object will be converted to a native pointer to the data it references. +For most `T`, or when converted to a `Ptr{Cvoid}`, this is a pointer to the +object data. When `T` is an `isbits` type, this value may be safely mutated, +otherwise mutation is strictly undefined behavior. + +As a special case, setting `T = Any` will instead cause the creation of a +pointer to the reference itself when converted to a `Ptr{Any}` +(a `jl_value_t const* const*` if T is immutable, else a `jl_value_t *const *`). +When converted to a `Ptr{Cvoid}`, it will still return a pointer to the data +region as for any other `T`. A `C_NULL` instance of `Ptr` can be passed to a `ccall` `Ref` argument to initialize it. @@ -105,21 +114,20 @@ RefArray(x::AbstractArray{T}, i::Int, roots::Any) where {T} = RefArray{T,typeof( RefArray(x::AbstractArray{T}, i::Int=1, roots::Nothing=nothing) where {T} = RefArray{T,typeof(x),Nothing}(x, i, nothing) convert(::Type{Ref{T}}, x::AbstractArray{T}) where {T} = RefArray(x, 1) -function unsafe_convert(P::Type{Ptr{T}}, b::RefArray{T}) where T +function unsafe_convert(P::Union{Type{Ptr{T}},Type{Ptr{Cvoid}}}, b::RefArray{T})::P where T if allocatedinline(T) p = pointer(b.x, b.i) - elseif isconcretetype(T) && T.mutable + elseif isconcretetype(T) && ismutabletype(T) p = pointer_from_objref(b.x[b.i]) else # see comment on equivalent branch for RefValue p = pointerref(Ptr{Ptr{Cvoid}}(pointer(b.x, b.i)), 1, Core.sizeof(Ptr{Cvoid})) end - return convert(P, p) + return p end -function unsafe_convert(P::Type{Ptr{Any}}, b::RefArray{Any}) - return convert(P, pointer(b.x, b.i)) +function unsafe_convert(::Type{Ptr{Any}}, b::RefArray{Any})::Ptr{Any} + return pointer(b.x, b.i) end -unsafe_convert(::Type{Ptr{Cvoid}}, b::RefArray{T}) where {T} = convert(Ptr{Cvoid}, unsafe_convert(Ptr{T}, b)) ### if is_primary_base_module diff --git a/base/refvalue.jl b/base/refvalue.jl index 47659ddd31723e..7cbb651d41aee7 100644 --- a/base/refvalue.jl +++ b/base/refvalue.jl @@ -35,10 +35,10 @@ true """ isassigned(x::RefValue) = isdefined(x, :x) -function unsafe_convert(P::Type{Ptr{T}}, b::RefValue{T}) where T +function unsafe_convert(P::Union{Type{Ptr{T}},Type{Ptr{Cvoid}}}, b::RefValue{T})::P where T if allocatedinline(T) p = pointer_from_objref(b) - elseif isconcretetype(T) && T.mutable + elseif isconcretetype(T) && ismutabletype(T) p = pointer_from_objref(b.x) else # If the slot is not leaf type, it could be either immutable or not. @@ -47,12 +47,11 @@ function unsafe_convert(P::Type{Ptr{T}}, b::RefValue{T}) where T # which also ensures this returns same pointer as the one rooted in the `RefValue` object. p = pointerref(Ptr{Ptr{Cvoid}}(pointer_from_objref(b)), 1, Core.sizeof(Ptr{Cvoid})) end - return convert(P, p)::Ptr{T} + return p end -function unsafe_convert(P::Type{Ptr{Any}}, b::RefValue{Any}) - return convert(P, pointer_from_objref(b))::Ptr{Any} +function unsafe_convert(::Type{Ptr{Any}}, b::RefValue{Any})::Ptr{Any} + return pointer_from_objref(b) end -unsafe_convert(::Type{Ptr{Cvoid}}, b::RefValue{T}) where {T} = convert(Ptr{Cvoid}, unsafe_convert(Ptr{T}, b))::Ptr{Cvoid} getindex(b::RefValue) = b.x setindex!(b::RefValue, x) = (b.x = x; b) diff --git a/base/regex.jl b/base/regex.jl index 75c3777fd681a0..6433eab40006de 100644 --- a/base/regex.jl +++ b/base/regex.jl @@ -4,9 +4,16 @@ include("pcre.jl") -const DEFAULT_COMPILER_OPTS = PCRE.UTF | PCRE.NO_UTF_CHECK | PCRE.ALT_BSUX | PCRE.UCP +const DEFAULT_COMPILER_OPTS = PCRE.UTF | PCRE.MATCH_INVALID_UTF | PCRE.ALT_BSUX | PCRE.UCP const DEFAULT_MATCH_OPTS = PCRE.NO_UTF_CHECK +""" +An abstract type representing any sort of pattern matching expression +(typically a regular expression). `AbstractPattern` objects can be used to +match strings with [`match`](@ref). +""" +abstract type AbstractPattern end + """ Regex(pattern[, flags]) @@ -16,8 +23,11 @@ with [`match`](@ref). `Regex` objects can be created using the [`@r_str`](@ref) string macro. The `Regex(pattern[, flags])` constructor is usually used if the `pattern` string needs to be interpolated. See the documentation of the string macro for details on flags. + +!!! note + To escape interpolated variables use `\\Q` and `\\E` (e.g. `Regex("\\\\Q\$x\\\\E")`) """ -mutable struct Regex +mutable struct Regex <: AbstractPattern pattern::String compile_options::UInt32 match_options::UInt32 @@ -97,7 +107,7 @@ listed after the ending quote, to change its behaviour: `\\s`, `\\W`, `\\w`, etc. match based on Unicode character properties. With this option, these sequences only match ASCII characters. -See `Regex` if interpolation is needed. +See [`Regex`](@ref) if interpolation is needed. # Examples ```jldoctest @@ -112,8 +122,9 @@ function show(io::IO, re::Regex) imsxa = PCRE.CASELESS|PCRE.MULTILINE|PCRE.DOTALL|PCRE.EXTENDED|PCRE.UCP opts = re.compile_options if (opts & ~imsxa) == (DEFAULT_COMPILER_OPTS & ~imsxa) - print(io, 'r') - print_quoted_literal(io, re.pattern) + print(io, "r\"") + escape_raw_string(io, re.pattern) + print(io, "\"") if (opts & PCRE.CASELESS ) != 0; print(io, 'i'); end if (opts & PCRE.MULTILINE) != 0; print(io, 'm'); end if (opts & PCRE.DOTALL ) != 0; print(io, 's'); end @@ -128,10 +139,54 @@ function show(io::IO, re::Regex) end end -# TODO: map offsets into strings in other encodings back to original indices. -# or maybe it's better to just fail since that would be quite slow +""" +`AbstractMatch` objects are used to represent information about matches found +in a string using an `AbstractPattern`. +""" +abstract type AbstractMatch end + +""" + RegexMatch + +A type representing a single match to a `Regex` found in a string. +Typically created from the [`match`](@ref) function. + +The `match` field stores the substring of the entire matched string. +The `captures` field stores the substrings for each capture group, indexed by number. +To index by capture group name, the entire match object should be indexed instead, +as shown in the examples. +The location of the start of the match is stored in the `offset` field. +The `offsets` field stores the locations of the start of each capture group, +with 0 denoting a group that was not captured. + +This type can be used as an iterator over the capture groups of the `Regex`, +yielding the substrings captured in each group. +Because of this, the captures of a match can be destructured. +If a group was not captured, `nothing` will be yielded instead of a substring. + +Methods that accept a `RegexMatch` object are defined for [`iterate`](@ref), +[`length`](@ref), [`eltype`](@ref), [`keys`](@ref keys(::RegexMatch)), [`haskey`](@ref), and +[`getindex`](@ref), where keys are the the names or numbers of a capture group. +See [`keys`](@ref keys(::RegexMatch)) for more information. + +# Examples +```jldoctest +julia> m = match(r"(?\\d+):(?\\d+)(am|pm)?", "11:30 in the morning") +RegexMatch("11:30", hour="11", minute="30", 3=nothing) + +julia> hr, min, ampm = m; + +julia> hr +"11" -struct RegexMatch +julia> m["minute"] +"30" + +julia> m.match +"11:30" +``` +""" +struct RegexMatch <: AbstractMatch match::SubString{String} captures::Vector{Union{Nothing,SubString{String}}} offset::Int @@ -139,19 +194,46 @@ struct RegexMatch regex::Regex end +""" + keys(m::RegexMatch) -> Vector + +Return a vector of keys for all capture groups of the underlying regex. +A key is included even if the capture group fails to match. +That is, `idx` will be in the return value even if `m[idx] == nothing`. + +Unnamed capture groups will have integer keys corresponding to their index. +Named capture groups will have string keys. + +!!! compat "Julia 1.6" + This method was added in Julia 1.6 + +# Examples +```jldoctest +julia> keys(match(r"(?\\d+):(?\\d+)(am|pm)?", "11:30")) +3-element Vector{Any}: + "hour" + "minute" + 3 +``` +""" +function keys(m::RegexMatch) + idx_to_capture_name = PCRE.capture_names(m.regex.regex) + return map(eachindex(m.captures)) do i + # If the capture group is named, return it's name, else return it's index + get(idx_to_capture_name, i, i) + end +end + function show(io::IO, m::RegexMatch) print(io, "RegexMatch(") show(io, m.match) - idx_to_capture_name = PCRE.capture_names(m.regex.regex) - if !isempty(m.captures) + capture_keys = keys(m) + if !isempty(capture_keys) print(io, ", ") - for i = 1:length(m.captures) - # If the capture group is named, show the name. - # Otherwise show its index. - capture_name = get(idx_to_capture_name, i, i) + for (i, capture_name) in enumerate(capture_keys) print(io, capture_name, "=") show(io, m.captures[i]) - if i < length(m.captures) + if i < length(m) print(io, ", ") end end @@ -175,6 +257,10 @@ function haskey(m::RegexMatch, name::Symbol) end haskey(m::RegexMatch, name::AbstractString) = haskey(m, Symbol(name)) +iterate(m::RegexMatch, args...) = iterate(m.captures, args...) +length(m::RegexMatch) = length(m.captures) +eltype(m::RegexMatch) = eltype(m.captures) + function occursin(r::Regex, s::AbstractString; offset::Integer=0) compile(r) return PCRE.exec_r(r.regex, String(s), offset, r.match_options) @@ -199,7 +285,7 @@ Return `true` if `s` starts with the regex pattern, `prefix`. See also [`occursin`](@ref) and [`endswith`](@ref). !!! compat "Julia 1.2" - This method requires at least Julia 1.2. + This method requires at least Julia 1.2. # Examples ```jldoctest @@ -231,7 +317,7 @@ Return `true` if `s` ends with the regex pattern, `suffix`. See also [`occursin`](@ref) and [`startswith`](@ref). !!! compat "Julia 1.2" - This method requires at least Julia 1.2. + This method requires at least Julia 1.2. # Examples ```jldoctest @@ -249,10 +335,24 @@ function endswith(s::SubString, r::Regex) return PCRE.exec_r(r.regex, s, 0, r.match_options | PCRE.ENDANCHORED) end +function chopprefix(s::AbstractString, prefix::Regex) + m = match(prefix, s, firstindex(s), PCRE.ANCHORED) + m === nothing && return SubString(s) + return SubString(s, ncodeunits(m.match) + 1) +end + +function chopsuffix(s::AbstractString, suffix::Regex) + m = match(suffix, s, firstindex(s), PCRE.ENDANCHORED) + m === nothing && return SubString(s) + isempty(m.match) && return SubString(s) + return SubString(s, firstindex(s), prevind(s, m.offset)) +end + + """ match(r::Regex, s::AbstractString[, idx::Integer[, addopts]]) -Search for the first match of the regular expression `r` in `s` and return a `RegexMatch` +Search for the first match of the regular expression `r` in `s` and return a [`RegexMatch`](@ref) object containing the match, or nothing if the match failed. The matching substring can be retrieved by accessing `m.match` and the captured sequences can be retrieved by accessing `m.captures` The optional `idx` argument specifies an index at which to start the search. @@ -278,7 +378,8 @@ true """ function match end -function match(re::Regex, str::Union{SubString{String}, String}, idx::Integer, add_opts::UInt32=UInt32(0)) +function match(re::Regex, str::Union{SubString{String}, String}, idx::Integer, + add_opts::UInt32=UInt32(0)) compile(re) opts = re.match_options | add_opts matched, data = PCRE.exec_r_data(re.regex, str, idx-1, opts) @@ -336,7 +437,7 @@ findfirst(r::Regex, s::AbstractString) = findnext(r,s,firstindex(s)) """ findall( - pattern::Union{AbstractString,Regex}, + pattern::Union{AbstractString,AbstractPattern}, string::AbstractString; overlap::Bool = false, ) @@ -364,8 +465,11 @@ julia> findall("a", "banana") 4:4 6:6 ``` + +!!! compat "Julia 1.3" + This method requires at least Julia 1.3. """ -function findall(t::Union{AbstractString,Regex}, s::AbstractString; overlap::Bool=false) +function findall(t::Union{AbstractString,AbstractPattern}, s::AbstractString; overlap::Bool=false) found = UnitRange{Int}[] i, e = firstindex(s), lastindex(s) while true @@ -379,9 +483,29 @@ function findall(t::Union{AbstractString,Regex}, s::AbstractString; overlap::Boo return found end +""" + findall(c::AbstractChar, s::AbstractString) + +Return a vector `I` of the indices of `s` where `s[i] == c`. If there are no such +elements in `s`, return an empty array. + +# Examples +```jldoctest +julia> findall('a', "batman") +2-element Vector{Int64}: + 2 + 5 +``` + +!!! compat "Julia 1.7" + This method requires at least Julia 1.7. +""" +findall(c::AbstractChar, s::AbstractString) = findall(isequal(c),s) + + """ count( - pattern::Union{AbstractString,Regex}, + pattern::Union{AbstractChar,AbstractString,AbstractPattern}, string::AbstractString; overlap::Bool = false, ) @@ -391,8 +515,14 @@ calling `length(findall(pattern, string))` but more efficient. If `overlap=true`, the matching sequences are allowed to overlap indices in the original string, otherwise they must be from disjoint character ranges. + +!!! compat "Julia 1.3" + This method requires at least Julia 1.3. + +!!! compat "Julia 1.7" + Using a character as the pattern requires at least Julia 1.7. """ -function count(t::Union{AbstractString,Regex}, s::AbstractString; overlap::Bool=false) +function count(t::Union{AbstractChar,AbstractString,AbstractPattern}, s::AbstractString; overlap::Bool=false) n = 0 i, e = firstindex(s), lastindex(s) while true @@ -412,18 +542,17 @@ end Stores the given string `substr` as a `SubstitutionString`, for use in regular expression substitutions. Most commonly constructed using the [`@s_str`](@ref) macro. +# Examples ```jldoctest julia> SubstitutionString("Hello \\\\g, it's \\\\1") -s"Hello \\\\g, it's \\\\1" +s"Hello \\g, it's \\1" julia> subst = s"Hello \\g, it's \\1" -s"Hello \\\\g, it's \\\\1" +s"Hello \\g, it's \\1" julia> typeof(subst) SubstitutionString{String} - ``` - """ struct SubstitutionString{T<:AbstractString} <: AbstractString string::T @@ -436,8 +565,9 @@ isvalid(s::SubstitutionString, i::Integer) = isvalid(s.string, i)::Bool iterate(s::SubstitutionString, i::Integer...) = iterate(s.string, i...)::Union{Nothing,Tuple{AbstractChar,Int}} function show(io::IO, s::SubstitutionString) - print(io, "s") - show(io, s.string) + print(io, "s\"") + escape_raw_string(io, s.string) + print(io, "\"") end """ @@ -447,6 +577,7 @@ Construct a substitution string, used for regular expression substitutions. Wit string, sequences of the form `\\N` refer to the Nth capture group in the regex, and `\\g` refers to a named capture group with name `groupname`. +# Examples ```jldoctest julia> msg = "#Hello# from Julia"; @@ -472,13 +603,20 @@ _free_pat_replacer(r::RegexAndMatchData) = PCRE.free_match_data(r.match_data) replace_err(repl) = error("Bad replacement string: $repl") -function _write_capture(io, re::RegexAndMatchData, group) +function _write_capture(io::IO, group::Int, str, r, re::RegexAndMatchData) len = PCRE.substring_length_bynumber(re.match_data, group) + # in the case of an optional group that doesn't match, len == 0 + len == 0 && return ensureroom(io, len+1) PCRE.substring_copy_bynumber(re.match_data, group, pointer(io.data, io.ptr), len+1) io.ptr += len io.size = max(io.size, io.ptr - 1) + nothing +end +function _write_capture(io::IO, group::Int, str, r, re) + group == 0 || replace_err("pattern is not a Regex") + return print(io, SubString(str, r)) end @@ -486,7 +624,7 @@ const SUB_CHAR = '\\' const GROUP_CHAR = 'g' const KEEP_ESC = [SUB_CHAR, GROUP_CHAR, '0':'9'...] -function _replace(io, repl_s::SubstitutionString, str, r, re::RegexAndMatchData) +function _replace(io, repl_s::SubstitutionString, str, r, re) LBRACKET = '<' RBRACKET = '>' repl = unescape_string(repl_s.string, KEEP_ESC) @@ -510,7 +648,7 @@ function _replace(io, repl_s::SubstitutionString, str, r, re::RegexAndMatchData) break end end - _write_capture(io, re, group) + _write_capture(io, group, str, r, re) elseif repl[next_i] == GROUP_CHAR i = nextind(repl, next_i) if i > e || repl[i] != LBRACKET @@ -523,15 +661,16 @@ function _replace(io, repl_s::SubstitutionString, str, r, re::RegexAndMatchData) i = nextind(repl, i) i > e && replace_err(repl) end - # TODO: avoid this allocation groupname = SubString(repl, groupstart, prevind(repl, i)) if all(isdigit, groupname) - _write_capture(io, re, parse(Int, groupname)) - else + group = parse(Int, groupname) + elseif re isa RegexAndMatchData group = PCRE.substring_number_from_name(re.re.regex, groupname) group < 0 && replace_err("Group $groupname not found in regex $(re.re)") - _write_capture(io, re, group) + else + group = -1 end + _write_capture(io, group, str, r, re) i = nextind(repl, i) else replace_err(repl) @@ -643,7 +782,7 @@ meaning that the contained characters are devoid of any special meaning (they are quoted with "\\Q" and "\\E"). !!! compat "Julia 1.3" - This method requires at least Julia 1.3. + This method requires at least Julia 1.3. # Examples ```jldoctest @@ -722,7 +861,7 @@ end Repeat a regex `n` times. !!! compat "Julia 1.3" - This method requires at least Julia 1.3. + This method requires at least Julia 1.3. # Examples ```jldoctest diff --git a/base/reinterpretarray.jl b/base/reinterpretarray.jl index 4a11a4d55ae7d3..7dc6607285fd0a 100644 --- a/base/reinterpretarray.jl +++ b/base/reinterpretarray.jl @@ -3,53 +3,143 @@ """ Gives a reinterpreted view (of element type T) of the underlying array (of element type S). If the size of `T` differs from the size of `S`, the array will be compressed/expanded in -the first dimension. +the first dimension. The variant `reinterpret(reshape, T, a)` instead adds or consumes the first dimension +depending on the ratio of element sizes. """ -struct ReinterpretArray{T,N,S,A<:AbstractArray{S, N}} <: AbstractArray{T, N} +struct ReinterpretArray{T,N,S,A<:AbstractArray{S},IsReshaped} <: AbstractArray{T, N} parent::A readable::Bool writable::Bool + + function throwbits(S::Type, T::Type, U::Type) + @noinline + throw(ArgumentError("cannot reinterpret `$(S)` as `$(T)`, type `$(U)` is not a bits type")) + end + function throwsize0(S::Type, T::Type, msg) + @noinline + throw(ArgumentError("cannot reinterpret a zero-dimensional `$(S)` array to `$(T)` which is of a $msg size")) + end + function throwsingleton(S::Type, T::Type, kind) + @noinline + throw(ArgumentError("cannot reinterpret $kind `$(S)` array to `$(T)` which is a singleton type")) + end + global reinterpret function reinterpret(::Type{T}, a::A) where {T,N,S,A<:AbstractArray{S, N}} - function throwbits(::Type{S}, ::Type{T}, ::Type{U}) where {S,T,U} - @_noinline_meta - throw(ArgumentError("cannot reinterpret `$(S)` `$(T)`, type `$(U)` is not a bits type")) - end - function throwsize0(::Type{S}, ::Type{T}) - @_noinline_meta - throw(ArgumentError("cannot reinterpret a zero-dimensional `$(S)` array to `$(T)` which is of a different size")) - end - function thrownonint(::Type{S}, ::Type{T}, dim) - @_noinline_meta + function thrownonint(S::Type, T::Type, dim) + @noinline throw(ArgumentError(""" cannot reinterpret an `$(S)` array to `$(T)` whose first dimension has size `$(dim)`. The resulting array would have non-integral first dimension. """)) end - function throwaxes1(::Type{S}, ::Type{T}, ax1) - @_noinline_meta + function throwaxes1(S::Type, T::Type, ax1) + @noinline throw(ArgumentError("cannot reinterpret a `$(S)` array to `$(T)` when the first axis is $ax1. Try reshaping first.")) end isbitstype(T) || throwbits(S, T, T) isbitstype(S) || throwbits(S, T, S) - (N != 0 || sizeof(T) == sizeof(S)) || throwsize0(S, T) + (N != 0 || sizeof(T) == sizeof(S)) || throwsize0(S, T, "different") if N != 0 && sizeof(S) != sizeof(T) ax1 = axes(a)[1] dim = length(ax1) - rem(dim*sizeof(S),sizeof(T)) == 0 || thrownonint(S, T, dim) + if issingletontype(T) + dim == 0 || throwsingleton(S, T, "a non-empty") + else + rem(dim*sizeof(S),sizeof(T)) == 0 || thrownonint(S, T, dim) + end first(ax1) == 1 || throwaxes1(S, T, ax1) end readable = array_subpadding(T, S) writable = array_subpadding(S, T) - new{T, N, S, A}(a, readable, writable) + new{T, N, S, A, false}(a, readable, writable) + end + reinterpret(::Type{T}, a::AbstractArray{T}) where {T} = a + + # With reshaping + function reinterpret(::typeof(reshape), ::Type{T}, a::A) where {T,S,A<:AbstractArray{S}} + function throwintmult(S::Type, T::Type) + @noinline + throw(ArgumentError("`reinterpret(reshape, T, a)` requires that one of `sizeof(T)` (got $(sizeof(T))) and `sizeof(eltype(a))` (got $(sizeof(S))) be an integer multiple of the other")) + end + function throwsize1(a::AbstractArray, T::Type) + @noinline + throw(ArgumentError("`reinterpret(reshape, $T, a)` where `eltype(a)` is $(eltype(a)) requires that `axes(a, 1)` (got $(axes(a, 1))) be equal to 1:$(sizeof(T) ÷ sizeof(eltype(a))) (from the ratio of element sizes)")) + end + function throwfromsingleton(S, T) + @noinline + throw(ArgumentError("`reinterpret(reshape, $T, a)` where `eltype(a)` is $S requires that $T be a singleton type, since $S is one")) + end + isbitstype(T) || throwbits(S, T, T) + isbitstype(S) || throwbits(S, T, S) + if sizeof(S) == sizeof(T) + N = ndims(a) + elseif sizeof(S) > sizeof(T) + issingletontype(T) && throwsingleton(S, T, "with reshape a") + rem(sizeof(S), sizeof(T)) == 0 || throwintmult(S, T) + N = ndims(a) + 1 + else + issingletontype(S) && throwfromsingleton(S, T) + rem(sizeof(T), sizeof(S)) == 0 || throwintmult(S, T) + N = ndims(a) - 1 + N > -1 || throwsize0(S, T, "larger") + axes(a, 1) == OneTo(sizeof(T) ÷ sizeof(S)) || throwsize1(a, T) + end + readable = array_subpadding(T, S) + writable = array_subpadding(S, T) + new{T, N, S, A, true}(a, readable, writable) end + reinterpret(::typeof(reshape), ::Type{T}, a::AbstractArray{T}) where {T} = a end -reinterpret(::Type{T}, a::ReinterpretArray) where {T} = reinterpret(T, a.parent) +ReshapedReinterpretArray{T,N,S,A<:AbstractArray{S}} = ReinterpretArray{T,N,S,A,true} +NonReshapedReinterpretArray{T,N,S,A<:AbstractArray{S, N}} = ReinterpretArray{T,N,S,A,false} + +""" + reinterpret(reshape, T, A::AbstractArray{S}) -> B + +Change the type-interpretation of `A` while consuming or adding a "channel dimension." + +If `sizeof(T) = n*sizeof(S)` for `n>1`, `A`'s first dimension must be +of size `n` and `B` lacks `A`'s first dimension. Conversely, if `sizeof(S) = n*sizeof(T)` for `n>1`, +`B` gets a new first dimension of size `n`. The dimensionality is unchanged if `sizeof(T) == sizeof(S)`. + +!!! compat "Julia 1.6" + This method requires at least Julia 1.6. + +# Examples + +```jldoctest +julia> A = [1 2; 3 4] +2×2 Matrix{$Int}: + 1 2 + 3 4 + +julia> reinterpret(reshape, Complex{Int}, A) # the result is a vector +2-element reinterpret(reshape, Complex{$Int}, ::Matrix{$Int}) with eltype Complex{$Int}: + 1 + 3im + 2 + 4im + +julia> a = [(1,2,3), (4,5,6)] +2-element Vector{Tuple{$Int, $Int, $Int}}: + (1, 2, 3) + (4, 5, 6) + +julia> reinterpret(reshape, Int, a) # the result is a matrix +3×2 reinterpret(reshape, $Int, ::Vector{Tuple{$Int, $Int, $Int}}) with eltype $Int: + 1 4 + 2 5 + 3 6 +``` +""" +reinterpret(::typeof(reshape), T::Type, a::AbstractArray) + +reinterpret(::Type{T}, a::NonReshapedReinterpretArray) where {T} = reinterpret(T, a.parent) +reinterpret(::typeof(reshape), ::Type{T}, a::ReshapedReinterpretArray) where {T} = reinterpret(reshape, T, a.parent) # Definition of StridedArray StridedFastContiguousSubArray{T,N,A<:DenseArray} = FastContiguousSubArray{T,N,A} -StridedReinterpretArray{T,N,A<:Union{DenseArray,StridedFastContiguousSubArray}} = ReinterpretArray{T,N,S,A} where S +StridedReinterpretArray{T,N,A<:Union{DenseArray,StridedFastContiguousSubArray},IsReshaped} = ReinterpretArray{T,N,S,A,IsReshaped} where S StridedReshapedArray{T,N,A<:Union{DenseArray,StridedFastContiguousSubArray,StridedReinterpretArray}} = ReshapedArray{T,N,A} StridedSubArray{T,N,A<:Union{DenseArray,StridedReshapedArray,StridedReinterpretArray}, I<:Tuple{Vararg{Union{RangeIndex, ReshapedUnitRange, AbstractCartesianIndex}}}} = SubArray{T,N,A,I} @@ -58,33 +148,43 @@ StridedVector{T} = StridedArray{T,1} StridedMatrix{T} = StridedArray{T,2} StridedVecOrMat{T} = Union{StridedVector{T}, StridedMatrix{T}} -# the definition of strides for Array{T,N} is tuple() if N = 0, otherwise it is -# a tuple containing 1 and a cumulative product of the first N-1 sizes -# this definition is also used for StridedReshapedArray and StridedReinterpretedArray -# which have the same memory storage as Array -stride(a::Union{DenseArray,StridedReshapedArray,StridedReinterpretArray}, i::Int) = _stride(a, i) +strides(a::Union{DenseArray,StridedReshapedArray,StridedReinterpretArray}) = size_to_strides(1, size(a)...) +stride(A::Union{DenseArray,StridedReshapedArray,StridedReinterpretArray}, k::Integer) = + k ≤ ndims(A) ? strides(A)[k] : length(A) + +function strides(a::ReshapedReinterpretArray) + ap = parent(a) + els, elp = elsize(a), elsize(ap) + stp = strides(ap) + els == elp && return stp + els < elp && return (1, _checked_strides(stp, els, elp)...) + stp[1] == 1 || throw(ArgumentError("Parent must be contiguous in the 1st dimension!")) + return _checked_strides(tail(stp), els, elp) +end -function stride(a::ReinterpretArray, i::Int) - a.parent isa StridedArray || ArgumentError("Parent must be strided.") |> throw - return _stride(a, i) +function strides(a::NonReshapedReinterpretArray) + ap = parent(a) + els, elp = elsize(a), elsize(ap) + stp = strides(ap) + els == elp && return stp + stp[1] == 1 || throw(ArgumentError("Parent must be contiguous in the 1st dimension!")) + return (1, _checked_strides(tail(stp), els, elp)...) end -function _stride(a, i) - if i > ndims(a) - return length(a) +@inline function _checked_strides(stp::Tuple, els::Integer, elp::Integer) + if elp > els && rem(elp, els) == 0 + N = div(elp, els) + return map(i -> N * i, stp) end - s = 1 - for n = 1:(i-1) - s *= size(a, n) - end - return s + drs = map(i -> divrem(elp * i, els), stp) + all(i->iszero(i[2]), drs) || + throw(ArgumentError("Parent's strides could not be exactly divided!")) + map(first, drs) end -function strides(a::ReinterpretArray) - a.parent isa StridedArray || ArgumentError("Parent must be strided.") |> throw - size_to_strides(1, size(a)...) -end -strides(a::Union{DenseArray,StridedReshapedArray,StridedReinterpretArray}) = size_to_strides(1, size(a)...) +_checkcontiguous(::Type{Bool}, A::ReinterpretArray) = _checkcontiguous(Bool, parent(A)) + +similar(a::ReinterpretArray, T::Type, d::Dims) = similar(a.parent, T, d) function check_readable(a::ReinterpretArray{T, N, S} where N) where {T,S} # See comment in check_writable @@ -105,32 +205,146 @@ function check_writable(a::ReinterpretArray{T, N, S} where N) where {T,S} end end -IndexStyle(a::ReinterpretArray) = IndexStyle(a.parent) +## IndexStyle specializations + +# For `reinterpret(reshape, T, a)` where we're adding a channel dimension and with +# `IndexStyle(a) == IndexLinear()`, it's advantageous to retain pseudo-linear indexing. +struct IndexSCartesian2{K} <: IndexStyle end # K = sizeof(S) ÷ sizeof(T), a static-sized 2d cartesian iterator + +IndexStyle(::Type{ReinterpretArray{T,N,S,A,false}}) where {T,N,S,A<:AbstractArray{S,N}} = IndexStyle(A) +function IndexStyle(::Type{ReinterpretArray{T,N,S,A,true}}) where {T,N,S,A<:AbstractArray{S}} + if sizeof(T) < sizeof(S) + IndexStyle(A) === IndexLinear() && return IndexSCartesian2{sizeof(S) ÷ sizeof(T)}() + return IndexCartesian() + end + return IndexStyle(A) +end +IndexStyle(::IndexSCartesian2{K}, ::IndexSCartesian2{K}) where {K} = IndexSCartesian2{K}() + +struct SCartesianIndex2{K} # can't make <:AbstractCartesianIndex without N, and 2 would be a bit misleading + i::Int + j::Int +end +to_index(i::SCartesianIndex2) = i + +struct SCartesianIndices2{K,R<:AbstractUnitRange{Int}} <: AbstractMatrix{SCartesianIndex2{K}} + indices2::R +end +SCartesianIndices2{K}(indices2::AbstractUnitRange{Int}) where {K} = (@assert K::Int > 1; SCartesianIndices2{K,typeof(indices2)}(indices2)) + +eachindex(::IndexSCartesian2{K}, A::ReshapedReinterpretArray) where {K} = SCartesianIndices2{K}(eachindex(IndexLinear(), parent(A))) +@inline function eachindex(style::IndexSCartesian2{K}, A::AbstractArray, B::AbstractArray...) where {K} + iter = eachindex(style, A) + _all_match_first(C->eachindex(style, C), iter, B...) || throw_eachindex_mismatch_indices(IndexSCartesian2{K}(), axes(A), axes.(B)...) + return iter +end + +size(iter::SCartesianIndices2{K}) where K = (K, length(iter.indices2)) +axes(iter::SCartesianIndices2{K}) where K = (OneTo(K), iter.indices2) + +first(iter::SCartesianIndices2{K}) where {K} = SCartesianIndex2{K}(1, first(iter.indices2)) +last(iter::SCartesianIndices2{K}) where {K} = SCartesianIndex2{K}(K, last(iter.indices2)) + +@inline function getindex(iter::SCartesianIndices2{K}, i::Int, j::Int) where {K} + @boundscheck checkbounds(iter, i, j) + return SCartesianIndex2{K}(i, iter.indices2[j]) +end + +function iterate(iter::SCartesianIndices2{K}) where {K} + ret = iterate(iter.indices2) + ret === nothing && return nothing + item2, state2 = ret + return SCartesianIndex2{K}(1, item2), (1, item2, state2) +end + +function iterate(iter::SCartesianIndices2{K}, (state1, item2, state2)) where {K} + if state1 < K + item1 = state1 + 1 + return SCartesianIndex2{K}(item1, item2), (item1, item2, state2) + end + ret = iterate(iter.indices2, state2) + ret === nothing && return nothing + item2, state2 = ret + return SCartesianIndex2{K}(1, item2), (1, item2, state2) +end + +SimdLoop.simd_outer_range(iter::SCartesianIndices2) = iter.indices2 +SimdLoop.simd_inner_length(::SCartesianIndices2{K}, ::Any) where K = K +@inline function SimdLoop.simd_index(::SCartesianIndices2{K}, Ilast::Int, I1::Int) where {K} + SCartesianIndex2{K}(I1+1, Ilast) +end + +_maybe_reshape(::IndexSCartesian2, A::ReshapedReinterpretArray, I...) = A + +# fallbacks +function _getindex(::IndexSCartesian2, A::AbstractArray{T,N}, I::Vararg{Int, N}) where {T,N} + @_propagate_inbounds_meta + getindex(A, I...) +end +function _setindex!(::IndexSCartesian2, A::AbstractArray{T,N}, v, I::Vararg{Int, N}) where {T,N} + @_propagate_inbounds_meta + setindex!(A, v, I...) +end +# fallbacks for array types that use "pass-through" indexing (e.g., `IndexStyle(A) = IndexStyle(parent(A))`) +# but which don't handle SCartesianIndex2 +function _getindex(::IndexSCartesian2, A::AbstractArray{T,N}, ind::SCartesianIndex2) where {T,N} + @_propagate_inbounds_meta + J = _ind2sub(tail(axes(A)), ind.j) + getindex(A, ind.i, J...) +end +function _setindex!(::IndexSCartesian2, A::AbstractArray{T,N}, v, ind::SCartesianIndex2) where {T,N} + @_propagate_inbounds_meta + J = _ind2sub(tail(axes(A)), ind.j) + setindex!(A, v, ind.i, J...) +end +eachindex(style::IndexSCartesian2, A::AbstractArray) = eachindex(style, parent(A)) + +## AbstractArray interface parent(a::ReinterpretArray) = a.parent dataids(a::ReinterpretArray) = dataids(a.parent) -unaliascopy(a::ReinterpretArray{T}) where {T} = reinterpret(T, unaliascopy(a.parent)) +unaliascopy(a::NonReshapedReinterpretArray{T}) where {T} = reinterpret(T, unaliascopy(a.parent)) +unaliascopy(a::ReshapedReinterpretArray{T}) where {T} = reinterpret(reshape, T, unaliascopy(a.parent)) -function size(a::ReinterpretArray{T,N,S} where {N}) where {T,S} +function size(a::NonReshapedReinterpretArray{T,N,S} where {N}) where {T,S} psize = size(a.parent) - size1 = div(psize[1]*sizeof(S), sizeof(T)) + size1 = issingletontype(T) ? psize[1] : div(psize[1]*sizeof(S), sizeof(T)) tuple(size1, tail(psize)...) end -size(a::ReinterpretArray{T,0}) where {T} = () +function size(a::ReshapedReinterpretArray{T,N,S} where {N}) where {T,S} + psize = size(a.parent) + sizeof(S) > sizeof(T) && return (div(sizeof(S), sizeof(T)), psize...) + sizeof(S) < sizeof(T) && return tail(psize) + return psize +end +size(a::NonReshapedReinterpretArray{T,0}) where {T} = () -function axes(a::ReinterpretArray{T,N,S} where {N}) where {T,S} +function axes(a::NonReshapedReinterpretArray{T,N,S} where {N}) where {T,S} paxs = axes(a.parent) f, l = first(paxs[1]), length(paxs[1]) - size1 = div(l*sizeof(S), sizeof(T)) + size1 = issingletontype(T) ? l : div(l*sizeof(S), sizeof(T)) tuple(oftype(paxs[1], f:f+size1-1), tail(paxs)...) end -axes(a::ReinterpretArray{T,0}) where {T} = () +function axes(a::ReshapedReinterpretArray{T,N,S} where {N}) where {T,S} + paxs = axes(a.parent) + sizeof(S) > sizeof(T) && return (OneTo(div(sizeof(S), sizeof(T))), paxs...) + sizeof(S) < sizeof(T) && return tail(paxs) + return paxs +end +axes(a::NonReshapedReinterpretArray{T,0}) where {T} = () elsize(::Type{<:ReinterpretArray{T}}) where {T} = sizeof(T) unsafe_convert(::Type{Ptr{T}}, a::ReinterpretArray{T,N,S} where N) where {T,S} = Ptr{T}(unsafe_convert(Ptr{S},a.parent)) -@inline @propagate_inbounds getindex(a::ReinterpretArray{T,0}) where {T} = reinterpret(T, a.parent[]) -@inline @propagate_inbounds getindex(a::ReinterpretArray) = a[1] +@inline @propagate_inbounds function getindex(a::NonReshapedReinterpretArray{T,0,S}) where {T,S} + if isprimitivetype(T) && isprimitivetype(S) + reinterpret(T, a.parent[]) + else + a[firstindex(a)] + end +end + +@inline @propagate_inbounds getindex(a::ReinterpretArray) = a[firstindex(a)] @inline @propagate_inbounds function getindex(a::ReinterpretArray{T,N,S}, inds::Vararg{Int, N}) where {T,N,S} check_readable(a) @@ -145,41 +359,62 @@ end # Convert to full indices here, to avoid needing multiple conversions in # the loop in _getindex_ra inds = _to_subscript_indices(a, i) - _getindex_ra(a, inds[1], tail(inds)) + isempty(inds) ? _getindex_ra(a, 1, ()) : _getindex_ra(a, inds[1], tail(inds)) +end + +@inline @propagate_inbounds function getindex(a::ReshapedReinterpretArray{T,N,S}, ind::SCartesianIndex2) where {T,N,S} + check_readable(a) + s = Ref{S}(a.parent[ind.j]) + GC.@preserve s begin + tptr = Ptr{T}(unsafe_convert(Ref{S}, s)) + return unsafe_load(tptr, ind.i) + end end @inline _memcpy!(dst, src, n) = ccall(:memcpy, Cvoid, (Ptr{UInt8}, Ptr{UInt8}, Csize_t), dst, src, n) -@inline @propagate_inbounds function _getindex_ra(a::ReinterpretArray{T,N,S}, i1::Int, tailinds::TT) where {T,N,S,TT} +@inline @propagate_inbounds function _getindex_ra(a::NonReshapedReinterpretArray{T,N,S}, i1::Int, tailinds::TT) where {T,N,S,TT} # Make sure to match the scalar reinterpret if that is applicable if sizeof(T) == sizeof(S) && (fieldcount(T) + fieldcount(S)) == 0 + if issingletontype(T) # singleton types + @boundscheck checkbounds(a, i1, tailinds...) + return T.instance + end return reinterpret(T, a.parent[i1, tailinds...]) else @boundscheck checkbounds(a, i1, tailinds...) ind_start, sidx = divrem((i1-1)*sizeof(T), sizeof(S)) - t = Ref{T}() - s = Ref{S}() - GC.@preserve t s begin - tptr = Ptr{UInt8}(unsafe_convert(Ref{T}, t)) - sptr = Ptr{UInt8}(unsafe_convert(Ref{S}, s)) - # Optimizations that avoid branches - if sizeof(T) % sizeof(S) == 0 - # T is bigger than S and contains an integer number of them - n = sizeof(T) ÷ sizeof(S) + # Optimizations that avoid branches + if sizeof(T) % sizeof(S) == 0 + # T is bigger than S and contains an integer number of them + n = sizeof(T) ÷ sizeof(S) + t = Ref{T}() + GC.@preserve t begin + sptr = Ptr{S}(unsafe_convert(Ref{T}, t)) for i = 1:n - s[] = a.parent[ind_start + i, tailinds...] - _memcpy!(tptr + (i-1)*sizeof(S), sptr, sizeof(S)) + s = a.parent[ind_start + i, tailinds...] + unsafe_store!(sptr, s, i) end - elseif sizeof(S) % sizeof(T) == 0 - # S is bigger than T and contains an integer number of them - s[] = a.parent[ind_start + 1, tailinds...] - _memcpy!(tptr, sptr + sidx, sizeof(T)) - else - i = 1 - nbytes_copied = 0 - # This is a bit complicated to deal with partial elements - # at both the start and the end. LLVM will fold as appropriate, - # once it knows the data layout + end + return t[] + elseif sizeof(S) % sizeof(T) == 0 + # S is bigger than T and contains an integer number of them + s = Ref{S}(a.parent[ind_start + 1, tailinds...]) + GC.@preserve s begin + tptr = Ptr{T}(unsafe_convert(Ref{S}, s)) + return unsafe_load(tptr + sidx) + end + else + i = 1 + nbytes_copied = 0 + # This is a bit complicated to deal with partial elements + # at both the start and the end. LLVM will fold as appropriate, + # once it knows the data layout + s = Ref{S}() + t = Ref{T}() + GC.@preserve s t begin + sptr = Ptr{S}(unsafe_convert(Ref{S}, s)) + tptr = Ptr{T}(unsafe_convert(Ref{T}, t)) while nbytes_copied < sizeof(T) s[] = a.parent[ind_start + i, tailinds...] nb = min(sizeof(S) - sidx, sizeof(T)-nbytes_copied) @@ -189,14 +424,67 @@ end i += 1 end end + return t[] + end + end +end + +@inline @propagate_inbounds function _getindex_ra(a::ReshapedReinterpretArray{T,N,S}, i1::Int, tailinds::TT) where {T,N,S,TT} + # Make sure to match the scalar reinterpret if that is applicable + if sizeof(T) == sizeof(S) && (fieldcount(T) + fieldcount(S)) == 0 + if issingletontype(T) # singleton types + @boundscheck checkbounds(a, i1, tailinds...) + return T.instance + end + return reinterpret(T, a.parent[i1, tailinds...]) + end + @boundscheck checkbounds(a, i1, tailinds...) + if sizeof(T) >= sizeof(S) + t = Ref{T}() + GC.@preserve t begin + sptr = Ptr{S}(unsafe_convert(Ref{T}, t)) + if sizeof(T) > sizeof(S) + # Extra dimension in the parent array + n = sizeof(T) ÷ sizeof(S) + if isempty(tailinds) && IndexStyle(a.parent) === IndexLinear() + offset = n * (i1 - firstindex(a)) + for i = 1:n + s = a.parent[i + offset] + unsafe_store!(sptr, s, i) + end + else + for i = 1:n + s = a.parent[i, i1, tailinds...] + unsafe_store!(sptr, s, i) + end + end + else + # No extra dimension + s = a.parent[i1, tailinds...] + unsafe_store!(sptr, s) + end end return t[] end + # S is bigger than T and contains an integer number of them + # n = sizeof(S) ÷ sizeof(T) + s = Ref{S}() + GC.@preserve s begin + tptr = Ptr{T}(unsafe_convert(Ref{S}, s)) + s[] = a.parent[tailinds...] + return unsafe_load(tptr, i1) + end end +@inline @propagate_inbounds function setindex!(a::NonReshapedReinterpretArray{T,0,S}, v) where {T,S} + if isprimitivetype(S) && isprimitivetype(T) + a.parent[] = reinterpret(S, v) + return a + end + setindex!(a, v, firstindex(a)) +end -@inline @propagate_inbounds setindex!(a::ReinterpretArray{T,0,S} where T, v) where {S} = (a.parent[] = reinterpret(S, v)) -@inline @propagate_inbounds setindex!(a::ReinterpretArray, v) = (a[1] = v) +@inline @propagate_inbounds setindex!(a::ReinterpretArray, v) = setindex!(a, v, firstindex(a)) @inline @propagate_inbounds function setindex!(a::ReinterpretArray{T,N,S}, v, inds::Vararg{Int, N}) where {T,N,S} check_writable(a) @@ -212,33 +500,57 @@ end _setindex_ra!(a, v, inds[1], tail(inds)) end -@inline @propagate_inbounds function _setindex_ra!(a::ReinterpretArray{T,N,S}, v, i1::Int, tailinds::TT) where {T,N,S,TT} +@inline @propagate_inbounds function setindex!(a::ReshapedReinterpretArray{T,N,S}, v, ind::SCartesianIndex2) where {T,N,S} + check_writable(a) + v = convert(T, v)::T + s = Ref{S}(a.parent[ind.j]) + GC.@preserve s begin + tptr = Ptr{T}(unsafe_convert(Ref{S}, s)) + unsafe_store!(tptr, v, ind.i) + end + a.parent[ind.j] = s[] + return a +end + +@inline @propagate_inbounds function _setindex_ra!(a::NonReshapedReinterpretArray{T,N,S}, v, i1::Int, tailinds::TT) where {T,N,S,TT} v = convert(T, v)::T # Make sure to match the scalar reinterpret if that is applicable if sizeof(T) == sizeof(S) && (fieldcount(T) + fieldcount(S)) == 0 - return setindex!(a.parent, reinterpret(S, v), i1, tailinds...) + if issingletontype(T) # singleton types + @boundscheck checkbounds(a, i1, tailinds...) + # setindex! is a noop except for the index check + else + setindex!(a.parent, reinterpret(S, v), i1, tailinds...) + end else @boundscheck checkbounds(a, i1, tailinds...) ind_start, sidx = divrem((i1-1)*sizeof(T), sizeof(S)) - t = Ref{T}(v) - s = Ref{S}() - GC.@preserve t s begin - tptr = Ptr{UInt8}(unsafe_convert(Ref{T}, t)) - sptr = Ptr{UInt8}(unsafe_convert(Ref{S}, s)) - # Optimizations that avoid branches - if sizeof(T) % sizeof(S) == 0 - # T is bigger than S and contains an integer number of them + # Optimizations that avoid branches + if sizeof(T) % sizeof(S) == 0 + # T is bigger than S and contains an integer number of them + t = Ref{T}(v) + GC.@preserve t begin + sptr = Ptr{S}(unsafe_convert(Ref{T}, t)) n = sizeof(T) ÷ sizeof(S) - for i = 0:n-1 - _memcpy!(sptr, tptr + i*sizeof(S), sizeof(S)) - a.parent[ind_start + i + 1, tailinds...] = s[] + for i = 1:n + s = unsafe_load(sptr, i) + a.parent[ind_start + i, tailinds...] = s end - elseif sizeof(S) % sizeof(T) == 0 - # S is bigger than T and contains an integer number of them - s[] = a.parent[ind_start + 1, tailinds...] - _memcpy!(sptr + sidx, tptr, sizeof(T)) + end + elseif sizeof(S) % sizeof(T) == 0 + # S is bigger than T and contains an integer number of them + s = Ref{S}(a.parent[ind_start + 1, tailinds...]) + GC.@preserve s begin + tptr = Ptr{T}(unsafe_convert(Ref{S}, s)) + unsafe_store!(tptr + sidx, v) a.parent[ind_start + 1, tailinds...] = s[] - else + end + else + t = Ref{T}(v) + s = Ref{S}() + GC.@preserve t s begin + tptr = Ptr{UInt8}(unsafe_convert(Ref{T}, t)) + sptr = Ptr{UInt8}(unsafe_convert(Ref{S}, s)) nbytes_copied = 0 i = 1 # Deal with any partial elements at the start. We'll have to copy in the @@ -273,6 +585,56 @@ end return a end +@inline @propagate_inbounds function _setindex_ra!(a::ReshapedReinterpretArray{T,N,S}, v, i1::Int, tailinds::TT) where {T,N,S,TT} + v = convert(T, v)::T + # Make sure to match the scalar reinterpret if that is applicable + if sizeof(T) == sizeof(S) && (fieldcount(T) + fieldcount(S)) == 0 + if issingletontype(T) # singleton types + @boundscheck checkbounds(a, i1, tailinds...) + # setindex! is a noop except for the index check + else + setindex!(a.parent, reinterpret(S, v), i1, tailinds...) + end + end + @boundscheck checkbounds(a, i1, tailinds...) + if sizeof(T) >= sizeof(S) + t = Ref{T}(v) + GC.@preserve t begin + sptr = Ptr{S}(unsafe_convert(Ref{T}, t)) + if sizeof(T) > sizeof(S) + # Extra dimension in the parent array + n = sizeof(T) ÷ sizeof(S) + if isempty(tailinds) && IndexStyle(a.parent) === IndexLinear() + offset = n * (i1 - firstindex(a)) + for i = 1:n + s = unsafe_load(sptr, i) + a.parent[i + offset] = s + end + else + for i = 1:n + s = unsafe_load(sptr, i) + a.parent[i, i1, tailinds...] = s + end + end + else # sizeof(T) == sizeof(S) + # No extra dimension + s = unsafe_load(sptr) + a.parent[i1, tailinds...] = s + end + end + else + # S is bigger than T and contains an integer number of them + s = Ref{S}() + GC.@preserve s begin + tptr = Ptr{T}(unsafe_convert(Ref{S}, s)) + s[] = a.parent[tailinds...] + unsafe_store!(tptr, v, i1) + a.parent[tailinds...] = s[] + end + end + return a +end + # Padding struct Padding offset::Int @@ -345,7 +707,7 @@ function CyclePadding(T::DataType) end using .Iterators: Stateful -@pure function array_subpadding(S, T) +@assume_effects :total function array_subpadding(S, T) checked_size = 0 lcm_size = lcm(sizeof(S), sizeof(T)) s, t = Stateful{<:Any, Any}(CyclePadding(S)), @@ -366,3 +728,46 @@ using .Iterators: Stateful end return true end + +# Reductions with IndexSCartesian2 + +function _mapreduce(f::F, op::OP, style::IndexSCartesian2{K}, A::AbstractArrayOrBroadcasted) where {F,OP,K} + inds = eachindex(style, A) + n = size(inds)[2] + if n == 0 + return mapreduce_empty_iter(f, op, A, IteratorEltype(A)) + else + return mapreduce_impl(f, op, A, first(inds), last(inds)) + end +end + +@noinline function mapreduce_impl(f::F, op::OP, A::AbstractArrayOrBroadcasted, + ifirst::SCI, ilast::SCI, blksize::Int) where {F,OP,SCI<:SCartesianIndex2{K}} where K + if ilast.j - ifirst.j < blksize + # sequential portion + @inbounds a1 = A[ifirst] + @inbounds a2 = A[SCI(2,ifirst.j)] + v = op(f(a1), f(a2)) + @simd for i = ifirst.i + 2 : K + @inbounds ai = A[SCI(i,ifirst.j)] + v = op(v, f(ai)) + end + # Remaining columns + for j = ifirst.j+1 : ilast.j + @simd for i = 1:K + @inbounds ai = A[SCI(i,j)] + v = op(v, f(ai)) + end + end + return v + else + # pairwise portion + jmid = ifirst.j + (ilast.j - ifirst.j) >> 1 + v1 = mapreduce_impl(f, op, A, ifirst, SCI(K,jmid), blksize) + v2 = mapreduce_impl(f, op, A, SCI(1,jmid+1), ilast, blksize) + return op(v1, v2) + end +end + +mapreduce_impl(f::F, op::OP, A::AbstractArrayOrBroadcasted, ifirst::SCartesianIndex2, ilast::SCartesianIndex2) where {F,OP} = + mapreduce_impl(f, op, A, ifirst, ilast, pairwise_blocksize(f, op)) diff --git a/base/reshapedarray.jl b/base/reshapedarray.jl index c137afc06e5e42..82d293249afc6e 100644 --- a/base/reshapedarray.jl +++ b/base/reshapedarray.jl @@ -113,9 +113,10 @@ reshape(parent::AbstractArray, dims::Dims) = _reshape(parent, dims) # Allow missing dimensions with Colon(): reshape(parent::AbstractVector, ::Colon) = parent +reshape(parent::AbstractVector, ::Tuple{Colon}) = parent reshape(parent::AbstractArray, dims::Int...) = reshape(parent, dims) reshape(parent::AbstractArray, dims::Union{Int,Colon}...) = reshape(parent, dims) -reshape(parent::AbstractArray, dims::Tuple{Vararg{Union{Int,Colon}}}) = _reshape(parent, _reshape_uncolon(parent, dims)) +reshape(parent::AbstractArray, dims::Tuple{Vararg{Union{Int,Colon}}}) = reshape(parent, _reshape_uncolon(parent, dims)) @inline function _reshape_uncolon(A, dims) @noinline throw1(dims) = throw(DimensionMismatch(string("new dimensions $(dims) ", "may have at most one omitted dimension specified by `Colon()`"))) @@ -146,14 +147,14 @@ end # product of trailing dims into the last element rdims_trailing(l, inds...) = length(l) * rdims_trailing(inds...) rdims_trailing(l) = length(l) -rdims(out::Val{N}, inds::Tuple) where {N} = rdims(ntuple(i -> OneTo(1), Val(N)), inds) +rdims(out::Val{N}, inds::Tuple) where {N} = rdims(ntuple(Returns(OneTo(1)), Val(N)), inds) rdims(out::Tuple{}, inds::Tuple{}) = () # N == 0, M == 0 rdims(out::Tuple{}, inds::Tuple{Any}) = () rdims(out::Tuple{}, inds::NTuple{M,Any}) where {M} = () rdims(out::Tuple{Any}, inds::Tuple{}) = out # N == 1, M == 0 rdims(out::NTuple{N,Any}, inds::Tuple{}) where {N} = out # N > 1, M == 0 rdims(out::Tuple{Any}, inds::Tuple{Any}) = inds # N == 1, M == 1 -rdims(out::Tuple{Any}, inds::NTuple{M,Any}) where {M} = (OneTo(rdims_trailing(inds...)),) # N == 1, M > 1 +rdims(out::Tuple{Any}, inds::NTuple{M,Any}) where {M} = (oneto(rdims_trailing(inds...)),) # N == 1, M > 1 rdims(out::NTuple{N,Any}, inds::NTuple{N,Any}) where {N} = inds # N > 1, M == N rdims(out::NTuple{N,Any}, inds::NTuple{M,Any}) where {N,M} = (first(inds), rdims(tail(out), tail(inds))...) # N > 1, M > 1, M != N @@ -185,7 +186,7 @@ end _reshape(v::ReshapedArray{<:Any,1}, dims::Dims{1}) = _reshape(v.parent, dims) _reshape(R::ReshapedArray, dims::Dims) = _reshape(R.parent, dims) -function __reshape(p::Tuple{AbstractArray,IndexCartesian}, dims::Dims) +function __reshape(p::Tuple{AbstractArray,IndexStyle}, dims::Dims) parent = p[1] strds = front(size_to_strides(map(length, axes(parent))..., 1)) strds1 = map(s->max(1,Int(s)), strds) # for resizing empty arrays @@ -207,7 +208,7 @@ size(A::ReshapedArray) = A.dims similar(A::ReshapedArray, eltype::Type, dims::Dims) = similar(parent(A), eltype, dims) IndexStyle(::Type{<:ReshapedArrayLF}) = IndexLinear() parent(A::ReshapedArray) = A.parent -parentindices(A::ReshapedArray) = map(OneTo, size(parent(A))) +parentindices(A::ReshapedArray) = map(oneto, size(parent(A))) reinterpret(::Type{T}, A::ReshapedArray, dims::Dims) where {T} = reinterpret(T, parent(A), dims) elsize(::Type{<:ReshapedArray{<:Any,<:Any,P}}) where {P} = elsize(P) @@ -241,7 +242,7 @@ end @inline function _unsafe_getindex(A::ReshapedArray{T,N}, indices::Vararg{Int,N}) where {T,N} axp = axes(A.parent) - i = offset_if_vec(Base._sub2ind(size(A), indices...), axp) + i = offset_if_vec(_sub2ind(size(A), indices...), axp) I = ind2sub_rs(axp, A.mi, i) _unsafe_getindex_rs(parent(A), I) end @@ -265,7 +266,7 @@ end @inline function _unsafe_setindex!(A::ReshapedArray{T,N}, val, indices::Vararg{Int,N}) where {T,N} axp = axes(A.parent) - i = offset_if_vec(Base._sub2ind(size(A), indices...), axp) + i = offset_if_vec(_sub2ind(size(A), indices...), axp) @inbounds parent(A)[ind2sub_rs(axes(A.parent), A.mi, i)...] = val val end @@ -286,8 +287,21 @@ viewindexing(I::Tuple{Slice, ReshapedUnitRange, Vararg{ScalarIndex}}) = IndexLin viewindexing(I::Tuple{ReshapedRange, Vararg{ScalarIndex}}) = IndexLinear() compute_stride1(s, inds, I::Tuple{ReshapedRange, Vararg{Any}}) = s*step(I[1].parent) compute_offset1(parent::AbstractVector, stride1::Integer, I::Tuple{ReshapedRange}) = - (@_inline_meta; first(I[1]) - first(axes1(I[1]))*stride1) + (@inline; first(I[1]) - first(axes1(I[1]))*stride1) substrides(strds::NTuple{N,Int}, I::Tuple{ReshapedUnitRange, Vararg{Any}}) where N = (size_to_strides(strds[1], size(I[1])...)..., substrides(tail(strds), tail(I))...) unsafe_convert(::Type{Ptr{T}}, V::SubArray{T,N,P,<:Tuple{Vararg{Union{RangeIndex,ReshapedUnitRange}}}}) where {T,N,P} = unsafe_convert(Ptr{T}, V.parent) + (first_index(V)-1)*sizeof(T) + + +_checkcontiguous(::Type{Bool}, A::AbstractArray) = size_to_strides(1, size(A)...) == strides(A) +_checkcontiguous(::Type{Bool}, A::Array) = true +_checkcontiguous(::Type{Bool}, A::ReshapedArray) = _checkcontiguous(Bool, parent(A)) +_checkcontiguous(::Type{Bool}, A::FastContiguousSubArray) = _checkcontiguous(Bool, parent(A)) + +function strides(a::ReshapedArray) + # We can handle non-contiguous parent if it's a StridedVector + ndims(parent(a)) == 1 && return size_to_strides(only(strides(parent(a))), size(a)...) + _checkcontiguous(Bool, a) || throw(ArgumentError("Parent must be contiguous.")) + size_to_strides(1, size(a)...) +end diff --git a/base/rounding.jl b/base/rounding.jl index 1628d7a01ec1d4..25cfe2dc09829f 100644 --- a/base/rounding.jl +++ b/base/rounding.jl @@ -37,9 +37,13 @@ Currently supported rounding modes are: - [`RoundNearestTiesAway`](@ref) - [`RoundNearestTiesUp`](@ref) - [`RoundToZero`](@ref) -- [`RoundFromZero`](@ref) ([`BigFloat`](@ref) only) +- [`RoundFromZero`](@ref) - [`RoundUp`](@ref) - [`RoundDown`](@ref) + +!!! compat "Julia 1.9" + `RoundFromZero` requires at least Julia 1.9. Prior versions support + `RoundFromZero` for `BigFloat`s only. """ struct RoundingMode{T} end @@ -76,7 +80,10 @@ const RoundDown = RoundingMode{:Down}() RoundFromZero Rounds away from zero. -This rounding mode may only be used with `T == BigFloat` inputs to [`round`](@ref). + +!!! compat "Julia 1.9" + `RoundFromZero` requires at least Julia 1.9. Prior versions support + `RoundFromZero` for `BigFloat`s only. # Examples ```jldoctest @@ -84,7 +91,7 @@ julia> BigFloat("1.0000000000000001", 5, RoundFromZero) 1.06 ``` """ -const RoundFromZero = RoundingMode{:FromZero}() # mpfr only +const RoundFromZero = RoundingMode{:FromZero}() """ RoundNearestTiesAway @@ -151,8 +158,8 @@ See [`RoundingMode`](@ref) for available modes. """ :rounding -setrounding_raw(::Type{<:Union{Float32,Float64}}, i::Integer) = ccall(:fesetround, Int32, (Int32,), i) -rounding_raw(::Type{<:Union{Float32,Float64}}) = ccall(:fegetround, Int32, ()) +setrounding_raw(::Type{<:Union{Float32,Float64}}, i::Integer) = ccall(:jl_set_fenv_rounding, Int32, (Int32,), i) +rounding_raw(::Type{<:Union{Float32,Float64}}) = ccall(:jl_get_fenv_rounding, Int32, ()) rounding(::Type{T}) where {T<:Union{Float32,Float64}} = from_fenv(rounding_raw(T)) diff --git a/base/ryu/LICENSE.md b/base/ryu/LICENSE.md index 74c718646a08d8..cab89eec22785d 100644 --- a/base/ryu/LICENSE.md +++ b/base/ryu/LICENSE.md @@ -22,4 +22,4 @@ FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. \ No newline at end of file +DEALINGS IN THE SOFTWARE. diff --git a/base/ryu/Ryu.jl b/base/ryu/Ryu.jl index 1d260fe9b3696e..81d1c41f4c19f0 100644 --- a/base/ryu/Ryu.jl +++ b/base/ryu/Ryu.jl @@ -64,7 +64,7 @@ Various options for the output format include: * `hash`: whether the decimal point should be written, even if no additional digits are needed for precision * `precision`: minimum number of significant digits to be included in the decimal string; extra `'0'` characters will be added for padding if necessary * `decchar`: decimal point character to be used - * `trimtrailingzeros`: whether trailing zeros should be removed + * `trimtrailingzeros`: whether trailing zeros of fractional part should be removed """ function writefixed(x::T, precision::Integer, diff --git a/base/ryu/exp.jl b/base/ryu/exp.jl index cf1fe23105b8c9..30291212d014d3 100644 --- a/base/ryu/exp.jl +++ b/base/ryu/exp.jl @@ -1,25 +1,16 @@ -@inline function writeexp(buf, pos, v::T, +function writeexp(buf, pos, v::T, precision=-1, plus=false, space=false, hash=false, expchar=UInt8('e'), decchar=UInt8('.'), trimtrailingzeros=false) where {T <: Base.IEEEFloat} @assert 0 < pos <= length(buf) startpos = pos x = Float64(v) - neg = signbit(x) + pos = append_sign(x, plus, space, buf, pos) + # special cases if x == 0 - if neg - buf[pos] = UInt8('-') - pos += 1 - elseif plus - buf[pos] = UInt8('+') - pos += 1 - elseif space - buf[pos] = UInt8(' ') - pos += 1 - end buf[pos] = UInt8('0') pos += 1 - if precision > 0 + if precision > 0 && !trimtrailingzeros buf[pos] = decchar pos += 1 for _ = 1:precision @@ -41,16 +32,6 @@ buf[pos + 2] = UInt8('N') return pos + 3 elseif !isfinite(x) - if neg - buf[pos] = UInt8('-') - pos += 1 - elseif plus - buf[pos] = UInt8('+') - pos += 1 - elseif space - buf[pos] = UInt8(' ') - pos += 1 - end buf[pos] = UInt8('I') buf[pos + 1] = UInt8('n') buf[pos + 2] = UInt8('f') @@ -70,16 +51,6 @@ end nonzero = false precision += 1 - if neg - buf[pos] = UInt8('-') - pos += 1 - elseif plus - buf[pos] = UInt8('+') - pos += 1 - elseif space - buf[pos] = UInt8(' ') - pos += 1 - end digits = 0 printedDigits = 0 availableDigits = 0 @@ -213,7 +184,7 @@ roundPos = pos while true roundPos -= 1 - if roundPos == (startpos - 1) || buf[roundPos] == UInt8('-') + if roundPos == (startpos - 1) || buf[roundPos] == UInt8('-') || (plus && buf[roundPos] == UInt8('+')) || (space && buf[roundPos] == UInt8(' ')) buf[roundPos + 1] = UInt8('1') e += 1 break diff --git a/base/ryu/fixed.jl b/base/ryu/fixed.jl index 4be1b3741832eb..e0085f5c66dab1 100644 --- a/base/ryu/fixed.jl +++ b/base/ryu/fixed.jl @@ -1,30 +1,18 @@ -@inline function writefixed(buf, pos, v::T, +function writefixed(buf, pos, v::T, precision=-1, plus=false, space=false, hash=false, decchar=UInt8('.'), trimtrailingzeros=false) where {T <: Base.IEEEFloat} @assert 0 < pos <= length(buf) startpos = pos x = Float64(v) - neg = signbit(x) + pos = append_sign(x, plus, space, buf, pos) + # special cases if x == 0 - if neg - buf[pos] = UInt8('-') - pos += 1 - elseif plus - buf[pos] = UInt8('+') - pos += 1 - elseif space - buf[pos] = UInt8(' ') - pos += 1 - end buf[pos] = UInt8('0') pos += 1 - if precision > 0 + if precision > 0 && !trimtrailingzeros buf[pos] = decchar pos += 1 - if trimtrailingzeros - precision = 1 - end for _ = 1:precision buf[pos] = UInt8('0') pos += 1 @@ -40,16 +28,6 @@ buf[pos + 2] = UInt8('N') return pos + 3 elseif !isfinite(x) - if neg - buf[pos] = UInt8('-') - pos += 1 - elseif plus - buf[pos] = UInt8('+') - pos += 1 - elseif space - buf[pos] = UInt8(' ') - pos += 1 - end buf[pos] = UInt8('I') buf[pos + 1] = UInt8('n') buf[pos + 2] = UInt8('f') @@ -68,16 +46,6 @@ m2 = (Int64(1) << 52) | mant end nonzero = false - if neg - buf[pos] = UInt8('-') - pos += 1 - elseif plus - buf[pos] = UInt8('+') - pos += 1 - elseif space - buf[pos] = UInt8(' ') - pos += 1 - end if e2 >= -52 idx = e2 < 0 ? 0 : indexforexp(e2) p10bits = pow10bitsforindex(idx) @@ -101,9 +69,11 @@ buf[pos] = UInt8('0') pos += 1 end + hasfractional = false if precision > 0 || hash buf[pos] = decchar pos += 1 + hasfractional = true end if e2 < 0 idx = div(-e2, 16) @@ -166,11 +136,12 @@ dotPos = 1 while true roundPos -= 1 - if roundPos == (startpos - 1) || (buf[roundPos] == UInt8('-')) + if roundPos == (startpos - 1) || (buf[roundPos] == UInt8('-')) || (plus && buf[roundPos] == UInt8('+')) || (space && buf[roundPos] == UInt8(' ')) buf[roundPos + 1] = UInt8('1') if dotPos > 1 buf[dotPos] = UInt8('0') buf[dotPos + 1] = decchar + hasfractional = true end buf[pos] = UInt8('0') pos += 1 @@ -199,7 +170,7 @@ pos += 1 end end - if trimtrailingzeros + if trimtrailingzeros && hasfractional while buf[pos - 1] == UInt8('0') pos -= 1 end diff --git a/base/ryu/shortest.jl b/base/ryu/shortest.jl index 21ef2e8c02e85a..f95c09d235e6df 100644 --- a/base/ryu/shortest.jl +++ b/base/ryu/shortest.jl @@ -224,25 +224,25 @@ integer. If a `maxsignif` argument is provided, then `b < maxsignif`. return b, e10 end - -@inline function writeshortest(buf::Vector{UInt8}, pos, x::T, - plus=false, space=false, hash=true, - precision=-1, expchar=UInt8('e'), padexp=false, decchar=UInt8('.'), - typed=false, compact=false) where {T} +function writeshortest(buf::Vector{UInt8}, pos, x::T, + plus=false, space=false, hash=true, + precision=-1, expchar=UInt8('e'), padexp=false, decchar=UInt8('.'), + typed=false, compact=false) where {T} @assert 0 < pos <= length(buf) - neg = signbit(x) # special cases if x == 0 - if neg - buf[pos] = UInt8('-') - pos += 1 - elseif plus - buf[pos] = UInt8('+') - pos += 1 - elseif space - buf[pos] = UInt8(' ') - pos += 1 + if typed && x isa Float16 + buf[pos] = UInt8('F') + buf[pos + 1] = UInt8('l') + buf[pos + 2] = UInt8('o') + buf[pos + 3] = UInt8('a') + buf[pos + 4] = UInt8('t') + buf[pos + 5] = UInt8('1') + buf[pos + 6] = UInt8('6') + buf[pos + 7] = UInt8('(') + pos += 8 end + pos = append_sign(x, plus, space, buf, pos) buf[pos] = UInt8('0') pos += 1 if hash @@ -257,6 +257,10 @@ end buf[pos + 1] = UInt8('0') pos += 2 end + if typed && x isa Float16 + buf[pos] = UInt8(')') + pos += 1 + end return pos end while hash && precision > 1 @@ -269,8 +273,13 @@ end buf[pos + 1] = UInt8('0') pos += 2 end + if typed && x isa Float16 + buf[pos] = UInt8(')') + pos += 1 + end return pos elseif isnan(x) + pos = append_sign(x, plus, space, buf, pos) buf[pos] = UInt8('N') buf[pos + 1] = UInt8('a') buf[pos + 2] = UInt8('N') @@ -285,22 +294,20 @@ end end return pos + 3 + (typed && x isa Union{Float32, Float16} ? 2 : 0) elseif !isfinite(x) - if neg - buf[pos] = UInt8('-') - end - buf[pos + neg] = UInt8('I') - buf[pos + neg + 1] = UInt8('n') - buf[pos + neg + 2] = UInt8('f') + pos = append_sign(x, plus, space, buf, pos) + buf[pos] = UInt8('I') + buf[pos + 1] = UInt8('n') + buf[pos + 2] = UInt8('f') if typed if x isa Float32 - buf[pos + neg + 3] = UInt8('3') - buf[pos + neg + 4] = UInt8('2') + buf[pos + 3] = UInt8('3') + buf[pos + 4] = UInt8('2') elseif x isa Float16 - buf[pos + neg + 3] = UInt8('1') - buf[pos + neg + 4] = UInt8('6') + buf[pos + 3] = UInt8('1') + buf[pos + 4] = UInt8('6') end end - return pos + neg + 3 + (typed && x isa Union{Float32, Float16} ? 2 : 0) + return pos + 3 + (typed && x isa Union{Float32, Float16} ? 2 : 0) end output, nexp = reduce_shortest(x, compact ? 999_999 : nothing) @@ -316,16 +323,7 @@ end buf[pos + 7] = UInt8('(') pos += 8 end - if neg - buf[pos] = UInt8('-') - pos += 1 - elseif plus - buf[pos] = UInt8('+') - pos += 1 - elseif space - buf[pos] = UInt8(' ') - pos += 1 - end + pos = append_sign(x, plus, space, buf, pos) olength = decimallength(output) exp_form = true diff --git a/base/ryu/utils.jl b/base/ryu/utils.jl index 3980130305837c..352f8f19cb9bed 100644 --- a/base/ryu/utils.jl +++ b/base/ryu/utils.jl @@ -195,6 +195,20 @@ Compute `(m * mul) >> j % 10^9` where `mul = mula + mulb<<64 + mulc<<128`, and ` return (v % UInt32) - UInt32(1000000000) * shifted end +@inline function append_sign(x, plus, space, buf, pos) + if signbit(x) && !isnan(x) # suppress minus sign for signaling NaNs + buf[pos] = UInt8('-') + pos += 1 + elseif plus + buf[pos] = UInt8('+') + pos += 1 + elseif space + buf[pos] = UInt8(' ') + pos += 1 + end + return pos +end + @inline function append_n_digits(olength, digits, buf, pos) i = 0 while digits >= 10000 @@ -353,10 +367,11 @@ end """ function pow5invsplit_lookup end for T in (Float64, Float32, Float16) - e2_max = exponent_max(T) - precision(T) - 2 + e2_max = exponent_max(T) - precision(T) - 1 i_max = log10pow2(e2_max) - table = Any[pow5invsplit(T, i) for i = 0:i_max] - @eval pow5invsplit_lookup(::Type{$T}, i) = @inbounds($table[i+1]) + table_sym = Symbol("pow5invsplit_table_", string(T)) + @eval const $table_sym = Tuple(Any[pow5invsplit($T, i) for i = 0:$i_max]) + @eval pow5invsplit_lookup(::Type{$T}, i) = @inbounds($table_sym[i+1]) end @@ -382,8 +397,9 @@ function pow5split_lookup end for T in (Float64, Float32, Float16) e2_min = 1 - exponent_bias(T) - significand_bits(T) - 2 i_max = 1 - e2_min - log10pow5(-e2_min) - table = Any[pow5split(T, i) for i = 0:i_max] - @eval pow5split_lookup(::Type{$T}, i) = @inbounds($table[i+1]) + table_sym = Symbol("pow5split_table_", string(T)) + @eval const $table_sym = Tuple(Any[pow5split($T, i) for i = 0:$i_max]) + @eval pow5split_lookup(::Type{$T}, i) = @inbounds($table_sym[i+1]) end const DIGIT_TABLE = UInt8[ diff --git a/base/set.jl b/base/set.jl index 75e2d5a744bb3a..66b5ef33fb4f31 100644 --- a/base/set.jl +++ b/base/set.jl @@ -3,13 +3,21 @@ struct Set{T} <: AbstractSet{T} dict::Dict{T,Nothing} - Set{T}() where {T} = new(Dict{T,Nothing}()) - Set{T}(s::Set{T}) where {T} = new(Dict{T,Nothing}(s.dict)) + global _Set(dict::Dict{T,Nothing}) where {T} = new{T}(dict) end +Set{T}() where {T} = _Set(Dict{T,Nothing}()) +Set{T}(s::Set{T}) where {T} = _Set(Dict{T,Nothing}(s.dict)) Set{T}(itr) where {T} = union!(Set{T}(), itr) Set() = Set{Any}() +function Set{T}(s::KeySet{T, <:Dict{T}}) where {T} + d = s.dict + slots = copy(d.slots) + keys = copy(d.keys) + vals = similar(d.vals, Nothing) + _Set(Dict{T,Nothing}(slots, keys, vals, d.ndel, d.count, d.age, d.idxfloor, d.maxprobe)) +end """ Set([itr]) @@ -17,6 +25,8 @@ Set() = Set{Any}() Construct a [`Set`](@ref) of the values generated by the given iterable object, or an empty set. Should be used instead of [`BitSet`](@ref) for sparse integer sets, or for sets of arbitrary objects. + +See also: [`push!`](@ref), [`empty!`](@ref), [`union!`](@ref), [`in`](@ref). """ Set(itr) = _Set(itr, IteratorEltype(itr)) @@ -34,7 +44,7 @@ empty(s::AbstractSet{T}, ::Type{U}=T) where {T,U} = Set{U}() # by default, a Set is returned emptymutable(s::AbstractSet{T}, ::Type{U}=T) where {T,U} = Set{U}() -_similar_for(c::AbstractSet, ::Type{T}, itr, isz) where {T} = empty(c, T) +_similar_for(c::AbstractSet, ::Type{T}, itr, isz, len) where {T} = empty(c, T) function show(io::IO, s::Set) if isempty(s) @@ -54,6 +64,16 @@ end isempty(s::Set) = isempty(s.dict) length(s::Set) = length(s.dict) in(x, s::Set) = haskey(s.dict, x) + +# This avoids hashing and probing twice and it works the same as +# in!(x, s::Set) = in(x, s) ? true : (push!(s, x); false) +function in!(x, s::Set) + idx, sh = ht_keyindex2_shorthash!(s.dict, x) + idx > 0 && return true + _setindex!(s.dict, nothing, x, -idx, sh) + return false +end + push!(s::Set, x) = (s.dict[x] = nothing; s) pop!(s::Set, x) = (pop!(s.dict, x); x) pop!(s::Set, x, default) = (x in s ? pop!(s, x) : default) @@ -105,6 +125,8 @@ as determined by [`isequal`](@ref), in the order that the first of each set of equivalent elements originally appears. The element type of the input is preserved. +See also: [`unique!`](@ref), [`allunique`](@ref), [`allequal`](@ref). + # Examples ```jldoctest julia> unique([1, 2, 6, 2]) @@ -125,10 +147,7 @@ function unique(itr) out = Vector{T}() seen = Set{T}() for x in itr - if !in(x, seen) - push!(seen, x) - push!(out, x) - end + !in!(x, seen) && push!(out, x) end return out end @@ -152,16 +171,10 @@ _unique_from(itr, out, seen, i) = unique_from(itr, out, seen, i) R = promote_typejoin(S, T) seenR = convert(Set{R}, seen) outR = convert(Vector{R}, out) - if !in(x, seenR) - push!(seenR, x) - push!(outR, x) - end + !in!(x, seenR) && push!(outR, x) return _unique_from(itr, outR, seenR, i) end - if !in(x, seen) - push!(seen, x) - push!(out, x) - end + !in!(x, seen) && push!(out, x) end return out end @@ -187,11 +200,7 @@ function unique(f, C; seen::Union{Nothing,Set}=nothing) out = Vector{eltype(C)}() if seen !== nothing for x in C - y = f(x) - if y ∉ seen - push!(out, x) - push!(seen, y) - end + !in!(f(x), seen) && push!(out, x) end return out end @@ -308,10 +317,12 @@ function _groupedunique!(A::AbstractVector) idxs = eachindex(A) y = first(A) # We always keep the first element - it = iterate(idxs, iterate(idxs)[2]) + T = NTuple{2,Any} # just to eliminate `iterate(idxs)::Nothing` candidate + it = iterate(idxs, (iterate(idxs)::T)[2]) count = 1 for x in Iterators.drop(A, 1) if !isequal(x, y) + it = it::T y = A[it[1]] = x count += 1 it = iterate(idxs, it[2]) @@ -369,6 +380,8 @@ end Return `true` if all values from `itr` are distinct when compared with [`isequal`](@ref). +See also: [`unique`](@ref), [`issorted`](@ref), [`allequal`](@ref). + # Examples ```jldoctest julia> a = [1; 2; 3] @@ -377,28 +390,27 @@ julia> a = [1; 2; 3] 2 3 +julia> allunique(a) +true + julia> allunique([a, a]) false ``` """ function allunique(C) - seen = Dict{eltype(C), Nothing}() + seen = Set{eltype(C)}() x = iterate(C) if haslength(C) && length(C) > 1000 for i in OneTo(1000) v, s = x - idx = ht_keyindex2!(seen, v) - idx > 0 && return false - _setindex!(seen, nothing, v, -idx) + in!(v, seen) && return false x = iterate(C, s) end sizehint!(seen, length(C)) end while x !== nothing v, s = x - idx = ht_keyindex2!(seen, v) - idx > 0 && return false - _setindex!(seen, nothing, v, -idx) + in!(v, seen) && return false x = iterate(C, s) end return true @@ -408,6 +420,40 @@ allunique(::Union{AbstractSet,AbstractDict}) = true allunique(r::AbstractRange) = !iszero(step(r)) || length(r) <= 1 +""" + allequal(itr) -> Bool + +Return `true` if all values from `itr` are equal when compared with [`isequal`](@ref). + +See also: [`unique`](@ref), [`allunique`](@ref). + +!!! compat "Julia 1.8" + The `allequal` function requires at least Julia 1.8. + +# Examples +```jldoctest +julia> allequal([]) +true + +julia> allequal([1]) +true + +julia> allequal([1, 1]) +true + +julia> allequal([1, 2]) +false + +julia> allequal(Dict(:a => 1, :b => 1)) +false +``` +""" +allequal(itr) = isempty(itr) ? true : all(isequal(first(itr)), itr) + +allequal(c::Union{AbstractSet,AbstractDict}) = length(c) <= 1 + +allequal(r::AbstractRange) = iszero(step(r)) || length(r) <= 1 + filter!(f, s::Set) = unsafe_filter!(f, s) const hashs_seed = UInt === UInt64 ? 0x852ada37cfe8e0ce : 0xcfe8e0ce @@ -433,7 +479,7 @@ end # TODO: use copy!, which is currently unavailable from here since it is defined in Future _copy_oftype(x, ::Type{T}) where {T} = copyto!(similar(x, T), x) # TODO: use similar() once deprecation is removed and it preserves keys -_copy_oftype(x::AbstractDict, ::Type{T}) where {T} = merge!(empty(x, T), x) +_copy_oftype(x::AbstractDict, ::Type{Pair{K,V}}) where {K,V} = merge!(empty(x, K, V), x) _copy_oftype(x::AbstractSet, ::Type{T}) where {T} = union!(empty(x, T), x) _copy_oftype(x::AbstractArray{T}, ::Type{T}) where {T} = copy(x) @@ -535,7 +581,10 @@ of the result will not include singleton types which are replaced with values of a different type: for example, `Union{T,Missing}` will become `T` if `missing` is replaced. -See also [`replace!`](@ref). +See also [`replace!`](@ref), [`splice!`](@ref), [`delete!`](@ref), [`insert!`](@ref). + +!!! compat "Julia 1.7" + Version 1.7 is required to replace elements of a `Tuple`. # Examples ```jldoctest @@ -570,7 +619,7 @@ promote_valuetype(x::Pair{K, V}, y::Pair...) where {K, V} = # Subtract singleton types which are going to be replaced function subtract_singletontype(::Type{T}, x::Pair{K}) where {T, K} if issingletontype(K) - Core.Compiler.typesubtract(T, K) + typesplit(T, K) else T end @@ -585,6 +634,9 @@ Return a copy of `A` where each value `x` in `A` is replaced by `new(x)`. If `count` is specified, then replace at most `count` values in total (replacements being defined as `new(x) !== x`). +!!! compat "Julia 1.7" + Version 1.7 is required to replace elements of a `Tuple`. + # Examples ```jldoctest julia> replace(x -> isodd(x) ? 2x : x, [1, 2, 3, 4]) @@ -610,15 +662,16 @@ replace!(a::Callable, b::Pair; count::Integer=-1) = throw(MethodError(replace!, replace!(a::Callable, b::Pair, c::Pair; count::Integer=-1) = throw(MethodError(replace!, (a, b, c))) replace(a::Callable, b::Pair; count::Integer=-1) = throw(MethodError(replace, (a, b))) replace(a::Callable, b::Pair, c::Pair; count::Integer=-1) = throw(MethodError(replace, (a, b, c))) -replace(a::AbstractString, b::Pair, c::Pair) = throw(MethodError(replace, (a, b, c))) ### replace! for AbstractDict/AbstractSet askey(k, ::AbstractDict) = k.first askey(k, ::AbstractSet) = k -function _replace!(new::Callable, res::T, A::T, - count::Int) where T<:Union{AbstractDict,AbstractSet} +function _replace!(new::Callable, res::Union{AbstractDict,AbstractSet}, + A::Union{AbstractDict,AbstractSet}, count::Int) + @assert res isa AbstractDict && A isa AbstractDict || + res isa AbstractSet && A isa AbstractSet count == 0 && return res c = 0 if res === A # cannot replace elements while iterating over A @@ -683,3 +736,73 @@ function _replace!(new::Callable, res::AbstractArray, A::AbstractArray, count::I end res end + +### specialization for Dict / Set + +function _replace!(new::Callable, t::Dict{K,V}, A::AbstractDict, count::Int) where {K,V} + # we ignore A, which is supposed to be equal to the destination t, + # as it can generally be faster to just replace inline + count == 0 && return t + c = 0 + news = Pair{K,V}[] + i = skip_deleted_floor!(t) + @inbounds while i != 0 + k1, v1 = t.keys[i], t.vals[i] + x1 = Pair{K,V}(k1, v1) + x2 = new(x1) + if x1 !== x2 + k2, v2 = first(x2), last(x2) + if isequal(k1, k2) + t.keys[i] = k2 + t.vals[i] = v2 + t.age += 1 + else + _delete!(t, i) + push!(news, x2) + end + c += 1 + c == count && break + end + i = i == typemax(Int) ? 0 : skip_deleted(t, i+1) + end + for n in news + push!(t, n) + end + t +end + +function _replace!(new::Callable, t::Set{T}, ::AbstractSet, count::Int) where {T} + _replace!(t.dict, t.dict, count) do kv + k = first(kv) + k2 = new(k) + k2 === k ? kv : k2 => nothing + end + t +end + +### replace for tuples + +function _replace(f::Callable, t::Tuple, count::Int) + if count == 0 || isempty(t) + t + else + x = f(t[1]) + (x, _replace(f, tail(t), count - !==(x, t[1]))...) + end +end + +replace(f::Callable, t::Tuple; count::Integer=typemax(Int)) = + _replace(f, t, check_count(count)) + +function _replace(t::Tuple, count::Int, old_new::Tuple{Vararg{Pair}}) + _replace(t, count) do x + @inline + for o_n in old_new + isequal(first(o_n), x) && return last(o_n) + end + return x + end +end + +replace(t::Tuple, old_new::Pair...; count::Integer=typemax(Int)) = + _replace(t, check_count(count), old_new) diff --git a/base/shell.jl b/base/shell.jl index a58f48034b6d21..f443a1f9c094ad 100644 --- a/base/shell.jl +++ b/base/shell.jl @@ -49,22 +49,24 @@ function shell_parse(str::AbstractString, interpolate::Bool=true; empty!(innerlist) end + C = eltype(str) + P = Pair{Int,C} for (j, c) in st - j, c = j::Int, c::eltype(str) + j, c = j::Int, c::C if !in_single_quotes && !in_double_quotes && isspace(c) i = consume_upto!(arg, s, i, j) append_2to1!(args, arg) while !isempty(st) # We've made sure above that we don't end in whitespace, # so updating `i` here is ok - (i, c) = peek(st)::Pair{Int,eltype(str)} + (i, c) = peek(st)::P isspace(c) || break popfirst!(st) end elseif interpolate && !in_single_quotes && c == '$' i = consume_upto!(arg, s, i, j) isempty(st) && error("\$ right before end of command") - stpos, c = popfirst!(st)::Pair{Int,eltype(str)} + stpos, c = popfirst!(st)::P isspace(c) && error("space not allowed right after \$") if startswith(SubString(s, stpos), "var\"") # Disallow var"#" syntax in cmd interpolations. @@ -87,15 +89,25 @@ function shell_parse(str::AbstractString, interpolate::Bool=true; elseif !in_single_quotes && c == '"' in_double_quotes = !in_double_quotes i = consume_upto!(arg, s, i, j) - elseif c == '\\' - if in_double_quotes + elseif !in_single_quotes && c == '\\' + if !isempty(st) && (peek(st)::P)[2] in ('\n', '\r') + i = consume_upto!(arg, s, i, j) + 1 + if popfirst!(st)[2] == '\r' && (peek(st)::P)[2] == '\n' + i += 1 + popfirst!(st) + end + while !isempty(st) && (peek(st)::P)[2] in (' ', '\t') + i = nextind(str, i) + _ = popfirst!(st) + end + elseif in_double_quotes isempty(st) && error("unterminated double quote") - k, c′ = peek(st) + k, c′ = peek(st)::P if c′ == '"' || c′ == '$' || c′ == '\\' i = consume_upto!(arg, s, i, j) _ = popfirst!(st) end - elseif !in_single_quotes + else isempty(st) && error("dangling backslash") i = consume_upto!(arg, s, i, j) _ = popfirst!(st) @@ -199,8 +211,8 @@ function print_shell_escaped_posixly(io::IO, args::AbstractString...) first || print(io, ' ') # avoid printing quotes around simple enough strings # that any (reasonable) shell will definitely never consider them to be special - have_single = false - have_double = false + have_single::Bool = false + have_double::Bool = false function isword(c::AbstractChar) if '0' <= c <= '9' || 'a' <= c <= 'z' || 'A' <= c <= 'Z' # word characters @@ -251,61 +263,200 @@ julia> Base.shell_escape_posixly("echo", "this", "&&", "that") shell_escape_posixly(args::AbstractString...) = sprint(print_shell_escaped_posixly, args...) - -function print_shell_escaped_winsomely(io::IO, args::AbstractString...) +""" + shell_escape_csh(args::Union{Cmd,AbstractString...}) + shell_escape_csh(io::IO, args::Union{Cmd,AbstractString...}) + +This function quotes any metacharacters in the string arguments such +that the string returned can be inserted into a command-line for +interpretation by the Unix C shell (csh, tcsh), where each string +argument will form one word. + +In contrast to a POSIX shell, csh does not support the use of the +backslash as a general escape character in double-quoted strings. +Therefore, this function wraps strings that might contain +metacharacters in single quotes, except for parts that contain single +quotes, which it wraps in double quotes instead. It switches between +these types of quotes as needed. Linefeed characters are escaped with +a backslash. + +This function should also work for a POSIX shell, except if the input +string contains a linefeed (`"\\n"`) character. + +See also: [`shell_escape_posixly`](@ref) +""" +function shell_escape_csh(io::IO, args::AbstractString...) first = true for arg in args first || write(io, ' ') first = false - # Quote any arg that contains a whitespace (' ' or '\t') or a double quote mark '"'. - # It's also valid to quote an arg with just a whitespace, - # but the following may be 'safer', and both implementations are valid anyways. - quotes = any(c -> c in (' ', '\t', '"'), arg) || isempty(arg) - quotes && write(io, '"') - backslashes = 0 - for c in arg - if c == '\\' - backslashes += 1 - else - # escape all backslashes and the following double quote - c == '"' && (backslashes = backslashes * 2 + 1) - for j = 1:backslashes - # backslashes aren't special here - write(io, '\\') + i = 1 + while true + for (r,e) = (r"^[A-Za-z0-9/\._-]+\z" => "", + r"^[^']*\z" => "'", r"^[^\$\`\"]*\z" => "\"", + r"^[^']+" => "'", r"^[^\$\`\"]+" => "\"") + if ((m = match(r, SubString(arg, i))) !== nothing) + write(io, e) + write(io, replace(m.match, '\n' => "\\\n")) + write(io, e) + i += ncodeunits(m.match) + break end - backslashes = 0 - write(io, c) end + i <= lastindex(arg) || break end - # escape all backslashes, letting the terminating double quote we add below to then be interpreted as a special char - quotes && (backslashes *= 2) - for j = 1:backslashes - write(io, '\\') - end - quotes && write(io, '"') end - return nothing end - +shell_escape_csh(args::AbstractString...) = + sprint(shell_escape_csh, args...; + sizehint = sum(sizeof.(args)) + length(args) * 3) """ - shell_escaped_winsomely(args::Union{Cmd,AbstractString...})::String - -Convert the collection of strings `args` into single string suitable for passing as the argument -string for a Windows command line. Windows passes the entire command line as a single string to -the application (unlike POSIX systems, where the list of arguments are passed separately). -Many Windows API applications (including julia.exe), use the conventions of the [Microsoft C -runtime](https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments) to -split that command line into a list of strings. This function implements the inverse of such a -C runtime command-line parser. It joins command-line arguments to be passed to a Windows console -application into a command line, escaping or quoting meta characters such as space, -double quotes and backslash where needed. This may be useful in concert with the `windows_verbatim` -flag to [`Cmd`](@ref) when constructing process pipelines. + shell_escape_wincmd(s::AbstractString) + shell_escape_wincmd(io::IO, s::AbstractString) + +The unexported `shell_escape_wincmd` function escapes Windows `cmd.exe` shell +meta characters. It escapes `()!^<>&|` by placing a `^` in front. An `@` is +only escaped at the start of the string. Pairs of `"` characters and the +strings they enclose are passed through unescaped. Any remaining `"` is escaped +with `^` to ensure that the number of unescaped `"` characters in the result +remains even. + +Since `cmd.exe` substitutes variable references (like `%USER%`) _before_ +processing the escape characters `^` and `"`, this function makes no attempt to +escape the percent sign (`%`), the presence of `%` in the input may cause +severe breakage, depending on where the result is used. + +Input strings with ASCII control characters that cannot be escaped (NUL, CR, +LF) will cause an `ArgumentError` exception. + +The result is safe to pass as an argument to a command call being processed by +`CMD.exe /S /C " ... "` (with surrounding double-quote pair) and will be +received verbatim by the target application if the input does not contain `%` +(else this function will fail with an ArgumentError). The presence of `%` in +the input string may result in command injection vulnerabilities and may +invalidate any claim of suitability of the output of this function for use as +an argument to cmd (due to the ordering described above), so use caution when +assembling a string from various sources. + +This function may be useful in concert with the `windows_verbatim` flag to +[`Cmd`](@ref) when constructing process pipelines. + +```julia +wincmd(c::String) = + run(Cmd(Cmd(["cmd.exe", "/s /c \\" \$c \\""]); + windows_verbatim=true)) +wincmd_echo(s::String) = + wincmd("echo " * Base.shell_escape_wincmd(s)) +wincmd_echo("hello \$(ENV["USERNAME"]) & the \\"whole\\" world! (=^I^=)") +``` + +But take note that if the input string `s` contains a `%`, the argument list +and echo'ed text may get corrupted, resulting in arbitrary command execution. +The argument can alternatively be passed as an environment variable, which +avoids the problem with `%` and the need for the `windows_verbatim` flag: + +```julia +cmdargs = Base.shell_escape_wincmd("Passing args with %cmdargs% works 100%!") +run(setenv(`cmd /C echo %cmdargs%`, "cmdargs" => cmdargs)) +``` + +!warning + The argument parsing done by CMD when calling batch files (either inside + `.bat` files or as arguments to them) is not fully compatible with the + output of this function. In particular, the processing of `%` is different. + +!important + Due to a peculiar behavior of the CMD parser/interpreter, each command + after a literal `|` character (indicating a command pipeline) must have + `shell_escape_wincmd` applied twice since it will be parsed twice by CMD. + This implies ENV variables would also be expanded twice! + For example: + ```julia + to_print = "All for 1 & 1 for all!" + to_print_esc = Base.shell_escape_wincmd(Base.shell_escape_wincmd(to_print)) + run(Cmd(Cmd(["cmd", "/S /C \\" break | echo \$(to_print_esc) \\""]), windows_verbatim=true)) + ``` + +With an I/O stream parameter `io`, the result will be written there, +rather than returned as a string. + +See also [`escape_microsoft_c_args`](@ref), [`shell_escape_posixly`](@ref). # Example ```jldoctest -julia> println(shell_escaped_winsomely("A B\\", "C")) -"A B\\" C +julia> Base.shell_escape_wincmd("a^\\"^o\\"^u\\"") +"a^^\\"^o\\"^^u^\\"" +``` +""" +function shell_escape_wincmd(io::IO, s::AbstractString) + # https://stackoverflow.com/a/4095133/1990689 + occursin(r"[\r\n\0]", s) && + throw(ArgumentError("control character unsupported by CMD.EXE")) + i = 1 + len = ncodeunits(s) + if len > 0 && s[1] == '@' + write(io, '^') + end + while i <= len + c = s[i] + if c == '"' && (j = findnext('"', s, nextind(s,i))) !== nothing + write(io, SubString(s,i,j)) + i = j + else + if c in ('"', '(', ')', '!', '^', '<', '>', '&', '|') + write(io, '^', c) + else + write(io, c) + end + end + i = nextind(s,i) + end +end +shell_escape_wincmd(s::AbstractString) = sprint(shell_escape_wincmd, s; + sizehint = 2*sizeof(s)) + """ -shell_escape_winsomely(args::AbstractString...) = - sprint(print_shell_escaped_winsomely, args..., sizehint=(sum(length, args)) + 3*length(args)) + escape_microsoft_c_args(args::Union{Cmd,AbstractString...}) + escape_microsoft_c_args(io::IO, args::Union{Cmd,AbstractString...}) + +Convert a collection of string arguments into a string that can be +passed to many Windows command-line applications. + +Microsoft Windows passes the entire command line as a single string to +the application (unlike POSIX systems, where the shell splits the +command line into a list of arguments). Many Windows API applications +(including julia.exe), use the conventions of the [Microsoft C/C++ +runtime](https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments) +to split that command line into a list of strings. + +This function implements an inverse for a parser compatible with these rules. +It joins command-line arguments to be passed to a Windows +C/C++/Julia application into a command line, escaping or quoting the +meta characters space, TAB, double quote and backslash where needed. + +See also [`shell_escape_wincmd`](@ref), [`escape_raw_string`](@ref). +""" +function escape_microsoft_c_args(io::IO, args::AbstractString...) + # http://daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULES + first = true + for arg in args + if first + first = false + else + write(io, ' ') # separator + end + if isempty(arg) || occursin(r"[ \t\"]", arg) + # Julia raw strings happen to use the same escaping convention + # as the argv[] parser in Microsoft's C runtime library. + write(io, '"') + escape_raw_string(io, arg) + write(io, '"') + else + write(io, arg) + end + end +end +escape_microsoft_c_args(args::AbstractString...) = + sprint(escape_microsoft_c_args, args...; + sizehint = (sum(sizeof.(args)) + 3*length(args))) diff --git a/base/show.jl b/base/show.jl index f4a65dfbf6d0f6..e59f2c8a9ce8ee 100644 --- a/base/show.jl +++ b/base/show.jl @@ -45,6 +45,7 @@ function show(io::IO, ::MIME"text/plain", f::Function) end show(io::IO, ::MIME"text/plain", c::ComposedFunction) = show(io, c) +show(io::IO, ::MIME"text/plain", c::Returns) = show(io, c) const ansi_regex = r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])" # An iterator similar to `pairs` but skips over "tokens" corresponding to @@ -393,6 +394,8 @@ getindex(io::IOContext, key) = getindex(io.dict, key) getindex(io::IO, key) = throw(KeyError(key)) get(io::IOContext, key, default) = get(io.dict, key, default) get(io::IO, key, default) = default +keys(io::IOContext) = keys(io.dict) +keys(io::IO) = keys(ImmutableDict{Symbol,Any}()) displaysize(io::IOContext) = haskey(io, :displaysize) ? io[:displaysize]::Tuple{Int,Int} : displaysize(io.io) @@ -439,14 +442,14 @@ Hello World! """ show(io::IO, @nospecialize(x)) = show_default(io, x) -show(x) = show(stdout::IO, x) +show(x) = show(stdout, x) # avoid inferring show_default on the type of `x` show_default(io::IO, @nospecialize(x)) = _show_default(io, inferencebarrier(x)) function _show_default(io::IO, @nospecialize(x)) t = typeof(x) - show(io, inferencebarrier(t)) + show(io, inferencebarrier(t)::DataType) print(io, '(') nf = nfields(x) nb = sizeof(x)::Int @@ -468,7 +471,7 @@ function _show_default(io::IO, @nospecialize(x)) end else print(io, "0x") - r = Ref(x) + r = Ref{Any}(x) GC.@preserve r begin p = unsafe_convert(Ptr{Cvoid}, r) for i in (nb - 1):-1:0 @@ -493,28 +496,29 @@ function is_exported_from_stdlib(name::Symbol, mod::Module) return isexported(mod, name) && isdefined(mod, name) && !isdeprecated(mod, name) && getfield(mod, name) === orig end -function show_function(io::IO, f::Function, compact::Bool) +function show_function(io::IO, f::Function, compact::Bool, fallback::Function) ft = typeof(f) mt = ft.name.mt if mt === Symbol.name.mt # uses shared method table - show_default(io, f) + fallback(io, f) elseif compact print(io, mt.name) elseif isdefined(mt, :module) && isdefined(mt.module, mt.name) && getfield(mt.module, mt.name) === f if is_exported_from_stdlib(mt.name, mt.module) || mt.module === Main - print(io, mt.name) + show_sym(io, mt.name) else - print(io, mt.module, ".", mt.name) + print(io, mt.module, ".") + show_sym(io, mt.name) end else - show_default(io, f) + fallback(io, f) end end -show(io::IO, f::Function) = show_function(io, f, get(io, :compact, false)::Bool) -print(io::IO, f::Function) = show_function(io, f, true) +show(io::IO, f::Function) = show_function(io, f, get(io, :compact, false)::Bool, show_default) +print(io::IO, f::Function) = show_function(io, f, true, show) function show(io::IO, f::Core.IntrinsicFunction) if !(get(io, :compact, false)::Bool) @@ -561,36 +565,21 @@ end # we're attempting to represent. # Union{T} where T is a degenerate case and is equal to T.ub, but we don't want # to print them that way, so filter those out from our aliases completely. -function makeproper(io::IO, x::Type) - properx = x - x = unwrap_unionall(x) +function makeproper(io::IO, @nospecialize(x::Type)) if io isa IOContext for (key, val) in io.dict if key === :unionall_env && val isa TypeVar - properx = UnionAll(val, properx) + x = UnionAll(val, x) end end end - if x isa Union - y = [] - normal = true - for typ in uniontypes(x) - if isa(typ, TypeVar) - normal = false - else - push!(y, typ) - end - end - normal || (x = Union{y...}) - properx = rewrap_unionall(x, properx) - end - has_free_typevars(properx) && return Any - return properx + has_free_typevars(x) && return Any + return x end function make_typealias(@nospecialize(x::Type)) - Any <: x && return - x <: Tuple && return + Any === x && return nothing + x <: Tuple && return nothing mods = modulesof!(Set{Module}(), x) Core in mods && push!(mods, Base) aliases = Tuple{GlobalRef,SimpleVector}[] @@ -603,7 +592,7 @@ function make_typealias(@nospecialize(x::Type)) for name in names(mod) if isdefined(mod, name) && !isdeprecated(mod, name) && isconst(mod, name) alias = getfield(mod, name) - if alias isa Type && !has_free_typevars(alias) && !isvarargtype(alias) && !print_without_params(alias) && x <: alias + if alias isa Type && !has_free_typevars(alias) && !print_without_params(alias) && x <: alias if alias isa UnionAll (ti, env) = ccall(:jl_type_intersection_with_env, Any, (Any, Any), x, alias)::SimpleVector # ti === Union{} && continue # impossible, since we already checked that x <: alias @@ -629,8 +618,8 @@ function make_typealias(@nospecialize(x::Type)) applied = rewrap_unionall(applied, p) end has_free_typevars(applied) && continue - applied == x || continue # it couldn't figure out the parameter matching - elseif alias <: x + applied === x || continue # it couldn't figure out the parameter matching + elseif alias === x env = Core.svec() else continue # not a complete match @@ -645,7 +634,70 @@ function make_typealias(@nospecialize(x::Type)) end end -function show_typealias(io::IO, name::GlobalRef, x::Type, env::SimpleVector) +isgensym(s::Symbol) = '#' in string(s) + +function show_can_elide(p::TypeVar, wheres::Vector, elide::Int, env::SimpleVector, skip::Int) + elide == 0 && return false + wheres[elide] === p || return false + for i = (elide + 1):length(wheres) + v = wheres[i]::TypeVar + has_typevar(v.lb, p) && return false + has_typevar(v.ub, p) && return false + end + for i = 1:length(env) + i == skip && continue + has_typevar(env[i], p) && return false + end + return true +end + +function show_typeparams(io::IO, env::SimpleVector, orig::SimpleVector, wheres::Vector) + n = length(env) + elide = length(wheres) + function egal_var(p::TypeVar, @nospecialize o) + return o isa TypeVar && + ccall(:jl_types_egal, Cint, (Any, Any), p.ub, o.ub) != 0 && + ccall(:jl_types_egal, Cint, (Any, Any), p.lb, o.lb) != 0 + end + for i = n:-1:1 + p = env[i] + if p isa TypeVar + if i == n && egal_var(p, orig[i]) && show_can_elide(p, wheres, elide, env, i) + n -= 1 + elide -= 1 + elseif p.lb === Union{} && isgensym(p.name) && show_can_elide(p, wheres, elide, env, i) + elide -= 1 + elseif p.ub === Any && isgensym(p.name) && show_can_elide(p, wheres, elide, env, i) + elide -= 1 + end + end + end + if n > 0 + print(io, "{") + for i = 1:n + p = env[i] + if p isa TypeVar + if p.lb === Union{} && something(findfirst(@nospecialize(w) -> w === p, wheres), 0) > elide + print(io, "<:") + show(io, p.ub) + elseif p.ub === Any && something(findfirst(@nospecialize(w) -> w === p, wheres), 0) > elide + print(io, ">:") + show(io, p.lb) + else + show(io, p) + end + else + show(io, p) + end + i < n && print(io, ", ") + end + print(io, "}") + end + resize!(wheres, elide) + nothing +end + +function show_typealias(io::IO, name::GlobalRef, x::Type, env::SimpleVector, wheres::Vector) if !(get(io, :compact, false)::Bool) # Print module prefix unless alias is visible from module passed to # IOContext. If :module is not set, default to Main. nothing can be used @@ -657,61 +709,99 @@ function show_typealias(io::IO, name::GlobalRef, x::Type, env::SimpleVector) end end print(io, name.name) - n = length(env) - n == 0 && return + isempty(env) && return + io = IOContext(io) + for p in wheres + io = IOContext(io, :unionall_env => p) + end + orig = getfield(name.mod, name.name) + vars = TypeVar[] + while orig isa UnionAll + push!(vars, orig.var) + orig = orig.body + end + show_typeparams(io, env, Core.svec(vars...), wheres) + nothing +end - print(io, "{") - let io = IOContext(io) - for i = n:-1:1 - p = env[i] - if p isa TypeVar - io = IOContext(io, :unionall_env => p) +function make_wheres(io::IO, env::SimpleVector, @nospecialize(x::Type)) + seen = IdSet() + wheres = TypeVar[] + # record things printed by the context + if io isa IOContext + for (key, val) in io.dict + if key === :unionall_env && val isa TypeVar && has_typevar(x, val) + push!(seen, val) end end - for i = 1:n - p = env[i] - show(io, p) - i < n && print(io, ", ") + end + # record things in x to print outermost + while x isa UnionAll + if !(x.var in seen) + push!(seen, x.var) + push!(wheres, x.var) end + x = x.body end - print(io, "}") - for i = n:-1:1 + # record remaining things in env to print innermost + for i = length(env):-1:1 p = env[i] - if p isa TypeVar && !io_has_tvar_name(io, p.name, x) - print(io, " where ") - show(io, p) + if p isa TypeVar && !(p in seen) + push!(seen, p) + pushfirst!(wheres, p) end end + return wheres +end + +function show_wheres(io::IO, wheres::Vector{TypeVar}) + isempty(wheres) && return + io = IOContext(io) + n = length(wheres) + for i = 1:n + p = wheres[i] + print(io, n == 1 ? " where " : i == 1 ? " where {" : ", ") + show(io, p) + io = IOContext(io, :unionall_env => p) + end + n > 1 && print(io, "}") + nothing end -function show_typealias(io::IO, x::Type) +function show_typealias(io::IO, @nospecialize(x::Type)) properx = makeproper(io, x) alias = make_typealias(properx) alias === nothing && return false - show_typealias(io, alias[1], x, alias[2]) + wheres = make_wheres(io, alias[2], x) + show_typealias(io, alias[1], x, alias[2], wheres) + show_wheres(io, wheres) return true end function make_typealiases(@nospecialize(x::Type)) - Any <: x && return Core.svec(), Union{} - x <: Tuple && return Core.svec(), Union{} + aliases = SimpleVector[] + Any === x && return aliases, Union{} + x <: Tuple && return aliases, Union{} mods = modulesof!(Set{Module}(), x) Core in mods && push!(mods, Base) - aliases = SimpleVector[] vars = Dict{Symbol,TypeVar}() xenv = UnionAll[] + each = Any[] for p in uniontypes(unwrap_unionall(x)) p isa UnionAll && push!(xenv, p) + push!(each, rewrap_unionall(p, x)) end x isa UnionAll && push!(xenv, x) for mod in mods for name in names(mod) if isdefined(mod, name) && !isdeprecated(mod, name) && isconst(mod, name) alias = getfield(mod, name) - if alias isa Type && !has_free_typevars(alias) && !isvarargtype(alias) && !print_without_params(alias) && !(alias <: Tuple) + if alias isa Type && !has_free_typevars(alias) && !print_without_params(alias) && !(alias <: Tuple) (ti, env) = ccall(:jl_type_intersection_with_env, Any, (Any, Any), x, alias)::SimpleVector ti === Union{} && continue - mod in modulesof!(Set{Module}(), alias) || continue # make sure this alias wasn't from an unrelated part of the Union + # make sure this alias wasn't from an unrelated part of the Union + mod2 = modulesof!(Set{Module}(), alias) + mod in mod2 || (mod === Base && Core in mods) || continue env = env::SimpleVector applied = alias if !isempty(env) @@ -734,29 +824,35 @@ function make_typealiases(@nospecialize(x::Type)) has_free_typevars(applied) && continue applied <: x || continue # parameter matching didn't make a subtype print_without_params(x) && (env = Core.svec()) - push!(aliases, Core.svec(GlobalRef(mod, name), env, applied, (ul, -length(env)))) + for typ in each # check that the alias also fully subsumes at least component of the input + if typ <: applied + push!(aliases, Core.svec(GlobalRef(mod, name), env, applied, (ul, -length(env)))) + break + end + end end end end end if isempty(aliases) - return Core.svec(), Union{} + return aliases, Union{} end - sort!(aliases, by = x -> x[4], rev = true) # heuristic sort by "best" environment + sort!(aliases, by = x -> x[4]::Tuple{Int,Int}, rev = true) # heuristic sort by "best" environment let applied = Union{} applied1 = Union{} keep = SimpleVector[] prev = (0, 0) for alias in aliases - if alias[4][1] < 2 + alias4 = alias[4]::Tuple{Int,Int} + if alias4[1] < 2 if !(alias[3] <: applied) applied1 = Union{applied1, alias[3]} push!(keep, alias) end - elseif alias[4] == prev || !(alias[3] <: applied) + elseif alias4 == prev || !(alias[3] <: applied) applied = applied1 = Union{applied1, alias[3]} push!(keep, alias) - prev = alias[4] + prev = alias4 end end return keep, applied1 @@ -766,51 +862,78 @@ end function show_unionaliases(io::IO, x::Union) properx = makeproper(io, x) aliases, applied = make_typealiases(properx) + isempty(aliases) && return false first = true + tvar = false for typ in uniontypes(x) - if !isa(typ, TypeVar) && rewrap_unionall(typ, properx) <: applied + if isa(typ, TypeVar) + tvar = true # sort bare TypeVars to the end + continue + elseif rewrap_unionall(typ, properx) <: applied continue end print(io, first ? "Union{" : ", ") first = false show(io, typ) end - if first && length(aliases) == 1 + if first && !tvar && length(aliases) == 1 alias = aliases[1] - show_typealias(io, alias[1], x, alias[2]) + env = alias[2]::SimpleVector + wheres = make_wheres(io, env, x) + show_typealias(io, alias[1], x, env, wheres) + show_wheres(io, wheres) else for alias in aliases print(io, first ? "Union{" : ", ") first = false - env = alias[2] - show_typealias(io, alias[1], x, alias[2]) + env = alias[2]::SimpleVector + wheres = make_wheres(io, env, x) + show_typealias(io, alias[1], x, env, wheres) + show_wheres(io, wheres) + end + if tvar + for typ in uniontypes(x) + if isa(typ, TypeVar) + print(io, ", ") + show(io, typ) + end + end end print(io, "}") end + return true end function show(io::IO, ::MIME"text/plain", @nospecialize(x::Type)) - show(io, x) - if !print_without_params(x) && get(io, :compact, true) + if !print_without_params(x) properx = makeproper(io, x) - if make_typealias(properx) !== nothing || x <: make_typealiases(properx)[2] - print(io, " (alias for ") - show(IOContext(io, :compact => false), x) - print(io, ")") + if make_typealias(properx) !== nothing || (unwrap_unionall(x) isa Union && x <: make_typealiases(properx)[2]) + show(IOContext(io, :compact => true), x) + if !(get(io, :compact, false)::Bool) + printstyled(io, " (alias for "; color = :light_black) + printstyled(IOContext(io, :compact => false), x, color = :light_black) + printstyled(io, ")"; color = :light_black) + end + return + end + end + show(io, x) + # give a helpful hint for function types + if x isa DataType && x !== UnionAll && !(get(io, :compact, false)::Bool) + tn = x.name::Core.TypeName + globname = isdefined(tn, :mt) ? tn.mt.name : nothing + if is_global_function(tn, globname) + print(io, " (singleton type of function ") + show_sym(io, globname) + print(io, ", subtype of Function)") end end - - #s1 = sprint(show, x, context = io) - #s2 = sprint(show, x, context = IOContext(io, :compact => false)) - #print(io, s1) - #if s1 != s2 - # print(io, " = ", s2) - #end end -function show(io::IO, @nospecialize(x::Type)) +show(io::IO, @nospecialize(x::Type)) = _show_type(io, inferencebarrier(x)) +function _show_type(io::IO, @nospecialize(x::Type)) if print_without_params(x) - show_type_name(io, unwrap_unionall(x).name) + show_type_name(io, (unwrap_unionall(x)::DataType).name) return elseif get(io, :compact, true) && show_typealias(io, x) return @@ -818,32 +941,43 @@ function show(io::IO, @nospecialize(x::Type)) show_datatype(io, x) return elseif x isa Union - if get(io, :compact, true) - show_unionaliases(io, x) - else - print(io, "Union") - show_delim_array(io, uniontypes(x), '{', ',', '}', false) + if get(io, :compact, true) && show_unionaliases(io, x) + return end + print(io, "Union") + show_delim_array(io, uniontypes(x), '{', ',', '}', false) return end x = x::UnionAll - if x.var.name === :_ || io_has_tvar_name(io, x.var.name, x) - counter = 1 - while true - newname = Symbol(x.var.name, counter) - if !io_has_tvar_name(io, newname, x) - newtv = TypeVar(newname, x.var.lb, x.var.ub) - x = UnionAll(newtv, x{newtv}) - break + wheres = TypeVar[] + let io = IOContext(io) + while x isa UnionAll + var = x.var + if var.name === :_ || io_has_tvar_name(io, var.name, x) + counter = 1 + while true + newname = Symbol(var.name, counter) + if !io_has_tvar_name(io, newname, x) + var = TypeVar(newname, var.lb, var.ub) + x = x{var} + break + end + counter += 1 + end + else + x = x.body end - counter += 1 + push!(wheres, var) + io = IOContext(io, :unionall_env => var) + end + if x isa DataType + show_datatype(io, x, wheres) + else + show(io, x) end end - - show(IOContext(io, :unionall_env => x.var), x.body) - print(io, " where ") - show(io, x.var) + show_wheres(io, wheres) end # Check whether 'sym' (defined in module 'parent') is visible from module 'from' @@ -857,22 +991,26 @@ function isvisible(sym::Symbol, parent::Module, from::Module) isdefined(from, sym) # if we're going to return true, force binding resolution end -function show_type_name(io::IO, tn::Core.TypeName) - if tn === UnionAll.name - # by coincidence, `typeof(Type)` is a valid representation of the UnionAll type. - # intercept this case and print `UnionAll` instead. - return print(io, "UnionAll") - end - globname = isdefined(tn, :mt) ? tn.mt.name : nothing - globfunc = false +function is_global_function(tn::Core.TypeName, globname::Union{Symbol,Nothing}) if globname !== nothing globname_str = string(globname::Symbol) if ('#' ∉ globname_str && '@' ∉ globname_str && isdefined(tn, :module) && isbindingresolved(tn.module, globname) && isdefined(tn.module, globname) && isconcretetype(tn.wrapper) && isa(getfield(tn.module, globname), tn.wrapper)) - globfunc = true + return true end end + return false +end + +function show_type_name(io::IO, tn::Core.TypeName) + if tn === UnionAll.name + # by coincidence, `typeof(Type)` is a valid representation of the UnionAll type. + # intercept this case and print `UnionAll` instead. + return print(io, "UnionAll") + end + globname = isdefined(tn, :mt) ? tn.mt.name : nothing + globfunc = is_global_function(tn, globname) sym = (globfunc ? globname : tn.name)::Symbol globfunc && print(io, "typeof(") quo = false @@ -899,29 +1037,42 @@ function show_type_name(io::IO, tn::Core.TypeName) nothing end -function show_datatype(io::IO, @nospecialize(x::DataType)) +function show_datatype(io::IO, x::DataType, wheres::Vector{TypeVar}=TypeVar[]) parameters = x.parameters::SimpleVector istuple = x.name === Tuple.name n = length(parameters) - # Print homogeneous tuples with more than 3 elements compactly as NTuple{N, T} - if istuple && n > 3 && all(i -> (parameters[1] === i), parameters) - print(io, "NTuple{", n, ", ", parameters[1], "}") - else - show_type_name(io, x.name) - if (n > 0 || istuple) && x !== Tuple - # Do not print the type parameters for the primary type if we are - # printing a method signature or type parameter. - # Always print the type parameter if we are printing the type directly - # since this information is still useful. - print(io, '{') - for i = 1:n - p = parameters[i] - show(io, p) - i < n && print(io, ", ") + # Print tuple types with homogeneous tails longer than max_n compactly using `NTuple` or `Vararg` + max_n = 3 + if istuple + taillen = 1 + for i in (n-1):-1:1 + if parameters[i] === parameters[n] + taillen += 1 + else + break + end + end + if n == taillen > max_n + print(io, "NTuple{", n, ", ") + show(io, parameters[1]) + print(io, "}") + else + print(io, "Tuple{") + for i = 1:(taillen > max_n ? n-taillen : n) + i > 1 && print(io, ", ") + show(io, parameters[i]) end - print(io, '}') + if taillen > max_n + print(io, ", Vararg{") + show(io, parameters[n]) + print(io, ", ", taillen, "}") + end + print(io, "}") end + else + show_type_name(io, x.name) + show_typeparams(io, parameters, (unwrap_unionall(x.name.wrapper)::DataType).parameters, wheres) end end @@ -936,9 +1087,22 @@ end show_supertypes(typ::DataType) = show_supertypes(stdout, typ) """ - @show + @show exs... + +Prints one or more expressions, and their results, to `stdout`, and returns the last result. + +See also: [`show`](@ref), [`@info`](@ref man-logging), [`println`](@ref). + +# Examples +```jldoctest +julia> x = @show 1+2 +1 + 2 = 3 +3 -Show an expression and result, returning the result. See also [`show`](@ref). +julia> @show x^2 x/2; +x ^ 2 = 9 +x / 2 = 1.5 +``` """ macro show(exs...) blk = Expr(:block) @@ -1003,7 +1167,20 @@ function show(io::IO, m::Module) if is_root_module(m) print(io, nameof(m)) else - print(io, join(fullname(m),".")) + print_fullname(io, m) + end +end +# The call to print_fullname above was originally `print(io, join(fullname(m),"."))`, +# which allocates. The method below provides the same behavior without allocating. +# See https://github.com/JuliaLang/julia/pull/42773 for perf information. +function print_fullname(io::IO, m::Module) + mp = parentmodule(m) + if m === Main || m === Base || m === Core || mp === m + show_sym(io, nameof(m)) + else + print_fullname(io, mp) + print(io, '.') + show_sym(io, nameof(m)) end end @@ -1012,6 +1189,10 @@ function sourceinfo_slotnames(src::CodeInfo) names = Dict{String,Int}() printnames = Vector{String}(undef, length(slotnames)) for i in eachindex(slotnames) + if slotnames[i] == :var"#unused#" + printnames[i] = "_" + continue + end name = string(slotnames[i]) idx = get!(names, name, i) if idx != i || isempty(name) @@ -1026,7 +1207,9 @@ function sourceinfo_slotnames(src::CodeInfo) return printnames end -function show(io::IO, l::Core.MethodInstance) +show(io::IO, l::Core.MethodInstance) = show_mi(io, l) + +function show_mi(io::IO, l::Core.MethodInstance, from_stackframe::Bool=false) def = l.def if isa(def, Method) if isdefined(def, :generator) && l === def.generator @@ -1034,13 +1217,49 @@ function show(io::IO, l::Core.MethodInstance) show(io, def) else print(io, "MethodInstance for ") - show_tuple_as_call(io, def.name, l.specTypes) + show_tuple_as_call(io, def.name, l.specTypes; qualified=true) end else print(io, "Toplevel MethodInstance thunk") + # `thunk` is not very much information to go on. If this + # MethodInstance is part of a stacktrace, it gets location info + # added by other means. But if it isn't, then we should try + # to print a little more identifying information. + if !from_stackframe + linetable = l.uninferred.linetable + line = isempty(linetable) ? "unknown" : (lt = linetable[1]::Union{LineNumberNode,Core.LineInfoNode}; string(lt.file, ':', lt.line)) + print(io, " from ", def, " starting at ", line) + end + end +end + +# These sometimes show up as Const-values in InferenceFrameInfo signatures +show(io::IO, r::Core.Compiler.UnitRange) = show(io, r.start : r.stop) +show(io::IO, mime::MIME{Symbol("text/plain")}, r::Core.Compiler.UnitRange) = show(io, mime, r.start : r.stop) + +function show(io::IO, mi_info::Core.Compiler.Timings.InferenceFrameInfo) + mi = mi_info.mi + def = mi.def + if isa(def, Method) + if isdefined(def, :generator) && mi === def.generator + print(io, "InferenceFrameInfo generator for ") + show(io, def) + else + print(io, "InferenceFrameInfo for ") + argnames = [isa(a, Core.Const) ? (isa(a.val, Type) ? "" : a.val) : "" for a in mi_info.slottypes[1:mi_info.nargs]] + show_tuple_as_call(io, def.name, mi.specTypes; argnames, qualified=true) + end + else + linetable = mi.uninferred.linetable + line = isempty(linetable) ? "" : (lt = linetable[1]; string(lt.file, ':', lt.line)) + print(io, "Toplevel InferenceFrameInfo thunk from ", def, " starting at ", line) end end +function show(io::IO, tinf::Core.Compiler.Timings.Timing) + print(io, "Core.Compiler.Timings.Timing(", tinf.mi_info, ") with ", length(tinf.children), " children") +end + function show_delim_array(io::IO, itr::Union{AbstractArray,SimpleVector}, op, delim, cl, delim_one, i1=first(LinearIndices(itr)), l=last(LinearIndices(itr))) print(io, op) @@ -1142,7 +1361,7 @@ const ExprNode = Union{Expr, QuoteNode, Slot, LineNumberNode, SSAValue, # IOContext(io, :unquote_fallback => false) tells show_unquoted to treat any # Expr whose head is :$ as if it is inside a quote, preventing fallback to the # "unhandled" case: this is used by print/string to be lawful to Rule 1 above. -# On the countrary, show/repr have to follow Rule 2, requiring any Expr whose +# On the contrary, show/repr have to follow Rule 2, requiring any Expr whose # head is :$ and which is not inside a quote to fallback to the "unhandled" case: # this is behavior is triggered by IOContext(io, :unquote_fallback => true) print( io::IO, ex::ExprNode) = (show_unquoted(IOContext(io, :unquote_fallback => false), ex, 0, -1); nothing) @@ -1157,32 +1376,60 @@ show_unquoted(io::IO, ex, indent::Int, prec::Int, ::Int) = show_unquoted(io, ex, const indent_width = 4 const quoted_syms = Set{Symbol}([:(:),:(::),:(:=),:(=),:(==),:(===),:(=>)]) const uni_syms = Set{Symbol}([:(::), :(<:), :(>:)]) -const uni_ops = Set{Symbol}([:(+), :(-), :(!), :(¬), :(~), :(<:), :(>:), :(√), :(∛), :(∜)]) +const uni_ops = Set{Symbol}([:(+), :(-), :(!), :(¬), :(~), :(<:), :(>:), :(√), :(∛), :(∜), :(∓), :(±)]) const expr_infix_wide = Set{Symbol}([ :(=), :(+=), :(-=), :(*=), :(/=), :(\=), :(^=), :(&=), :(|=), :(÷=), :(%=), :(>>>=), :(>>=), :(<<=), :(.=), :(.+=), :(.-=), :(.*=), :(./=), :(.\=), :(.^=), :(.&=), :(.|=), :(.÷=), :(.%=), :(.>>>=), :(.>>=), :(.<<=), - :(&&), :(||), :(<:), :($=), :(⊻=), :(>:), :(-->)]) -const expr_infix = Set{Symbol}([:(:), :(->), Symbol("::")]) + :(&&), :(||), :(<:), :($=), :(⊻=), :(>:), :(-->), + :(:=), :(≔), :(⩴), :(≕)]) +const expr_infix = Set{Symbol}([:(:), :(->), :(::)]) const expr_infix_any = union(expr_infix, expr_infix_wide) const expr_calls = Dict(:call => ('(',')'), :calldecl => ('(',')'), :ref => ('[',']'), :curly => ('{','}'), :(.) => ('(',')')) const expr_parens = Dict(:tuple=>('(',')'), :vcat=>('[',']'), :hcat =>('[',']'), :row =>('[',']'), :vect=>('[',']'), + :ncat =>('[',']'), :nrow =>('[',']'), :braces=>('{','}'), :bracescat=>('{','}')) ## AST decoding helpers ## is_id_start_char(c::AbstractChar) = ccall(:jl_id_start_char, Cint, (UInt32,), c) != 0 is_id_char(c::AbstractChar) = ccall(:jl_id_char, Cint, (UInt32,), c) != 0 + +""" + isidentifier(s) -> Bool + +Return whether the symbol or string `s` contains characters that are parsed as +a valid ordinary identifier (not a binary/unary operator) in Julia code; +see also [`Base.isoperator`](@ref). + +Internally Julia allows any sequence of characters in a `Symbol` (except `\\0`s), +and macros automatically use variable names containing `#` in order to avoid +naming collision with the surrounding code. In order for the parser to +recognize a variable, it uses a limited set of characters (greatly extended by +Unicode). `isidentifier()` makes it possible to query the parser directly +whether a symbol contains valid characters. + +# Examples +```jldoctest +julia> Meta.isidentifier(:x), Meta.isidentifier("1x") +(true, false) +``` +""" function isidentifier(s::AbstractString) - isempty(s) && return false + x = Iterators.peel(s) + isnothing(x) && return false (s == "true" || s == "false") && return false - c, rest = Iterators.peel(s) + c, rest = x is_id_start_char(c) || return false return all(is_id_char, rest) end isidentifier(s::Symbol) = isidentifier(string(s)) +is_op_suffix_char(c::AbstractChar) = ccall(:jl_op_suffix_char, Cint, (UInt32,), c) != 0 + +_isoperator(s) = ccall(:jl_is_operator, Cint, (Cstring,), s) != 0 + """ isoperator(s::Symbol) @@ -1190,11 +1437,11 @@ Return `true` if the symbol can be used as an operator, `false` otherwise. # Examples ```jldoctest -julia> Base.isoperator(:+), Base.isoperator(:f) +julia> Meta.isoperator(:+), Meta.isoperator(:f) (true, false) ``` """ -isoperator(s::Union{Symbol,AbstractString}) = ccall(:jl_is_operator, Cint, (Cstring,), s) != 0 +isoperator(s::Union{Symbol,AbstractString}) = _isoperator(s) || ispostfixoperator(s) """ isunaryoperator(s::Symbol) @@ -1203,12 +1450,13 @@ Return `true` if the symbol can be used as a unary (prefix) operator, `false` ot # Examples ```jldoctest -julia> Base.isunaryoperator(:-), Base.isunaryoperator(:√), Base.isunaryoperator(:f) +julia> Meta.isunaryoperator(:-), Meta.isunaryoperator(:√), Meta.isunaryoperator(:f) (true, true, false) ``` """ isunaryoperator(s::Symbol) = ccall(:jl_is_unary_operator, Cint, (Cstring,), s) != 0 is_unary_and_binary_operator(s::Symbol) = ccall(:jl_is_unary_and_binary_operator, Cint, (Cstring,), s) != 0 +is_syntactic_operator(s::Symbol) = ccall(:jl_is_syntactic_operator, Cint, (Cstring,), s) != 0 """ isbinaryoperator(s::Symbol) @@ -1217,11 +1465,30 @@ Return `true` if the symbol can be used as a binary (infix) operator, `false` ot # Examples ```jldoctest -julia> Base.isbinaryoperator(:-), Base.isbinaryoperator(:√), Base.isbinaryoperator(:f) +julia> Meta.isbinaryoperator(:-), Meta.isbinaryoperator(:√), Meta.isbinaryoperator(:f) (true, false, false) ``` """ -isbinaryoperator(s::Symbol) = isoperator(s) && (!isunaryoperator(s) || is_unary_and_binary_operator(s)) +function isbinaryoperator(s::Symbol) + return _isoperator(s) && (!isunaryoperator(s) || is_unary_and_binary_operator(s)) && + s !== Symbol("'") +end + +""" + ispostfixoperator(s::Union{Symbol,AbstractString}) + +Return `true` if the symbol can be used as a postfix operator, `false` otherwise. + +# Examples +```jldoctest +julia> Meta.ispostfixoperator(Symbol("'")), Meta.ispostfixoperator(Symbol("'ᵀ")), Meta.ispostfixoperator(:-) +(true, true, false) +``` +""" +function ispostfixoperator(s::Union{Symbol,AbstractString}) + s = String(s)::String + return startswith(s, '\'') && all(is_op_suffix_char, SubString(s, 2)) +end """ operator_precedence(s::Symbol) @@ -1275,9 +1542,6 @@ function operator_associativity(s::Symbol) return :left end -is_expr(@nospecialize(ex), head::Symbol) = isa(ex, Expr) && (ex.head === head) -is_expr(@nospecialize(ex), head::Symbol, n::Int) = is_expr(ex, head) && length((ex::Expr).args) == n - is_quoted(ex) = false is_quoted(ex::QuoteNode) = true is_quoted(ex::Expr) = is_expr(ex, :quote, 1) || is_expr(ex, :inert, 1) @@ -1290,16 +1554,14 @@ unquoted(ex::Expr) = ex.args[1] function printstyled end function with_output_color end -const indent_width = 4 - is_expected_union(u::Union) = u.a == Nothing || u.b == Nothing || u.a == Missing || u.b == Missing emphasize(io, str::AbstractString, col = Base.error_color()) = get(io, :color, false) ? printstyled(io, str; color=col, bold=true) : print(io, uppercase(str)) -show_linenumber(io::IO, line) = print(io, "#= line ", line, " =#") -show_linenumber(io::IO, line, file) = print(io, "#= ", file, ":", line, " =#") +show_linenumber(io::IO, line) = printstyled(io, "#= line ", line, " =#", color=:light_black) +show_linenumber(io::IO, line, file) = printstyled(io, "#= ", file, ":", line, " =#", color=:light_black) show_linenumber(io::IO, line, file::Nothing) = show_linenumber(io, line) # show a block, e g if/for/etc @@ -1344,7 +1606,7 @@ function show_list(io::IO, items, sep, indent::Int, prec::Int=0, quote_level::In (first && prec >= prec_power && ((item isa Expr && item.head === :call && (callee = item.args[1]; isa(callee, Symbol) && callee in uni_ops)) || (item isa Real && item < 0))) || - (enclose_operators && item isa Symbol && isoperator(item)) + (enclose_operators && item isa Symbol && isoperator(item) && is_valid_identifier(item)) parens && print(io, '(') if kw && is_expr(item, :kw, 2) item = item::Expr @@ -1366,11 +1628,20 @@ function show_enclosed_list(io::IO, op, items, sep, cl, indent, prec=0, quote_le print(io, cl) end +function is_valid_identifier(sym) + return isidentifier(sym) || ( + _isoperator(sym) && + !(sym in (Symbol("'"), :(::), :?)) && + !is_syntactic_operator(sym) + ) +end + # show a normal (non-operator) function call, e.g. f(x, y) or A[z] # kw: `=` expressions are parsed with head `kw` in this context function show_call(io::IO, head, func, func_args, indent, quote_level, kw::Bool) op, cl = expr_calls[head] if (isa(func, Symbol) && func !== :(:) && !(head === :. && isoperator(func))) || + (isa(func, Symbol) && !is_valid_identifier(func)) || (isa(func, Expr) && (func.head === :. || func.head === :curly || func.head === :macroname)) || isa(func, GlobalRef) show_unquoted(io, func, indent, 0, quote_level) @@ -1396,12 +1667,12 @@ end # Print `sym` as it would appear as an identifier name in code # * Print valid identifiers & operators literally; also macros names if allow_macroname=true # * Escape invalid identifiers with var"" syntax -function show_sym(io::IO, sym; allow_macroname=false) - if isidentifier(sym) || (isoperator(sym) && sym !== Symbol("'")) +function show_sym(io::IO, sym::Symbol; allow_macroname=false) + if is_valid_identifier(sym) print(io, sym) elseif allow_macroname && (sym_str = string(sym); startswith(sym_str, '@')) print(io, '@') - show_sym(io, sym_str[2:end]) + show_sym(io, Symbol(sym_str[2:end])) else print(io, "var", repr(string(sym))) end @@ -1456,14 +1727,16 @@ function show_unquoted(io::IO, ex::QuoteNode, indent::Int, prec::Int) end function show_unquoted_quote_expr(io::IO, @nospecialize(value), indent::Int, prec::Int, quote_level::Int) - if isa(value, Symbol) && !(value in quoted_syms) - value = value::Symbol - s = string(value) - if isidentifier(s) || (isoperator(value) && value !== Symbol("'")) - print(io, ":") - print(io, value) + if isa(value, Symbol) + sym = value::Symbol + if value in quoted_syms + print(io, ":(", sym, ")") else - print(io, "Symbol(", repr(s), ")") + if isidentifier(sym) || (_isoperator(sym) && sym !== Symbol("'")) + print(io, ":", sym) + else + print(io, "Symbol(", repr(String(sym)), ")") + end end else if isa(value,Expr) && value.head === :block @@ -1499,7 +1772,10 @@ function show_generator(io, ex::Expr, indent, quote_level) end end -function valid_import_path(@nospecialize ex) +function valid_import_path(@nospecialize(ex), allow_as = true) + if allow_as && is_expr(ex, :as) && length((ex::Expr).args) == 2 + ex = (ex::Expr).args[1] + end return is_expr(ex, :(.)) && length((ex::Expr).args) > 0 && all(a->isa(a,Symbol), (ex::Expr).args) end @@ -1517,10 +1793,12 @@ function show_import_path(io::IO, ex, quote_level) end elseif ex.head === :(.) for i = 1:length(ex.args) - if i > 1 && ex.args[i-1] !== :(.) + if ex.args[i] === :(.) print(io, '.') + else + show_sym(io, ex.args[i]::Symbol, allow_macroname=(i==length(ex.args))) + i < length(ex.args) && print(io, '.') end - show_sym(io, ex.args[i]::Symbol, allow_macroname=(i==length(ex.args))) end else show_unquoted(io, ex, 0, 0, quote_level) @@ -1563,7 +1841,10 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In unhandled = false # dot (i.e. "x.y"), but not compact broadcast exps if head === :(.) && (nargs != 2 || !is_expr(args[2], :tuple)) - if nargs == 2 && is_quoted(args[2]) + # standalone .op + if nargs == 1 && args[1] isa Symbol && isoperator(args[1]::Symbol) + print(io, "(.", args[1], ")") + elseif nargs == 2 && is_quoted(args[2]) item = args[1] # field field = unquoted(args[2]) @@ -1613,14 +1894,16 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In # list-like forms, e.g. "[1, 2, 3]" elseif haskey(expr_parens, head) || # :vcat etc. - head === :typed_vcat || head === :typed_hcat + head === :typed_vcat || head === :typed_hcat || head === :typed_ncat # print the type and defer to the untyped case - if head === :typed_vcat || head === :typed_hcat + if head === :typed_vcat || head === :typed_hcat || head === :typed_ncat show_unquoted(io, args[1], indent, prec, quote_level) if head === :typed_vcat head = :vcat - else + elseif head === :typed_hcat head = :hcat + else + head = :ncat end args = args[2:end] nargs = nargs - 1 @@ -1630,15 +1913,33 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In sep = "; " elseif head === :hcat || head === :row sep = " " + elseif head === :ncat || head === :nrow + sep = ";"^args[1]::Int * " " + args = args[2:end] + nargs = nargs - 1 else sep = ", " end - head !== :row && print(io, op) + head !== :row && head !== :nrow && print(io, op) show_list(io, args, sep, indent, 0, quote_level) - if nargs == 1 && head === :vcat - print(io, ';') + if nargs <= 1 && (head === :vcat || head === :ncat) + print(io, sep[1:end-1]) end - head !== :row && print(io, cl) + head !== :row && head !== :nrow && print(io, cl) + + # transpose + elseif (head === Symbol("'") && nargs == 1) || ( + # ' with unicode suffix is a call expression + head === :call && nargs == 2 && args[1] isa Symbol && + ispostfixoperator(args[1]::Symbol) && args[1]::Symbol !== Symbol("'") + ) + op, arg1 = head === Symbol("'") ? (head, args[1]) : (args[1], args[2]) + if isa(arg1, Expr) || (isa(arg1, Symbol) && isoperator(arg1::Symbol)) + show_enclosed_list(io, '(', [arg1::Union{Expr, Symbol}], ", ", ')', indent, 0) + else + show_unquoted(io, arg1, indent, 0, quote_level) + end + print(io, op) # function call elseif head === :call && nargs >= 1 @@ -1668,10 +1969,10 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In end # unary operator (i.e. "!z") - elseif isa(func,Symbol) && func in uni_ops && length(func_args) == 1 + elseif isa(func,Symbol) && length(func_args) == 1 && func in uni_ops show_unquoted(io, func, indent, 0, quote_level) arg1 = func_args[1] - if isa(arg1, Expr) || (isa(arg1, Symbol) && isoperator(arg1)) + if isa(arg1, Expr) || (isa(arg1, Symbol) && isoperator(arg1) && is_valid_identifier(arg1)) show_enclosed_list(io, '(', func_args, ", ", ')', indent, func_prec) else show_unquoted(io, arg1, indent, func_prec, quote_level) @@ -1681,8 +1982,8 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In elseif func_prec > 0 # is a binary operator na = length(func_args) if (na == 2 || (na > 2 && isa(func, Symbol) && func in (:+, :++, :*)) || (na == 3 && func === :(:))) && - all(!isa(a, Expr) || a.head !== :... for a in func_args) - sep = func === :(:) ? "$func" : " $func " + all(a -> !isa(a, Expr) || a.head !== :..., func_args) + sep = func === :(:) ? "$func" : " " * convert(String, string(func))::String * " " # if func::Any, avoid string interpolation (invalidation) if func_prec <= prec show_enclosed_list(io, '(', func_args, sep, ')', indent, func_prec, quote_level, true) @@ -1819,8 +2120,12 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In # var-arg declaration or expansion # (i.e. "function f(L...) end" or "f(B...)") elseif head === :(...) && nargs == 1 - show_unquoted(io, args[1], indent, 0, quote_level) + dotsprec = operator_precedence(:(:)) - 1 + parens = dotsprec <= prec + parens && print(io, "(") + show_unquoted(io, args[1], indent, dotsprec, quote_level) print(io, "...") + parens && print(io, ")") elseif (nargs == 0 && head in (:break, :continue)) print(io, head) @@ -1908,12 +2213,15 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In elseif head === :line && 1 <= nargs <= 2 show_linenumber(io, args...) - elseif head === :try && 3 <= nargs <= 4 + elseif head === :try && 3 <= nargs <= 5 iob = IOContext(io, beginsym=>false) show_block(iob, "try", args[1], indent, quote_level) if is_expr(args[3], :block) show_block(iob, "catch", args[2] === false ? Any[] : args[2], args[3]::Expr, indent, quote_level) end + if nargs >= 5 && is_expr(args[5], :block) + show_block(iob, "else", Any[], args[5]::Expr, indent, quote_level) + end if nargs >= 4 && is_expr(args[4], :block) show_block(iob, "finally", Any[], args[4]::Expr, indent, quote_level) end @@ -2005,17 +2313,6 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In parens && print(io, ")") end - # transpose - elseif head === Symbol('\'') && nargs == 1 - if isa(args[1], Symbol) - show_unquoted(io, args[1], 0, 0, quote_level) - else - print(io, "(") - show_unquoted(io, args[1], 0, 0, quote_level) - print(io, ")") - end - print(io, head) - # `where` syntax elseif head === :where && nargs > 1 parens = 1 <= prec @@ -2047,12 +2344,16 @@ function show_unquoted(io::IO, ex::Expr, indent::Int, prec::Int, quote_level::In first = false show_import_path(io, a, quote_level) end + elseif head === :as && nargs == 2 && valid_import_path(args[1], false) + show_import_path(io, args[1], quote_level) + print(io, " as ") + show_unquoted(io, args[2], indent, 0, quote_level) elseif head === :meta && nargs >= 2 && args[1] === :push_loc print(io, "# meta: location ", join(args[2:end], " ")) elseif head === :meta && nargs == 1 && args[1] === :pop_loc print(io, "# meta: pop location") elseif head === :meta && nargs == 2 && args[1] === :pop_loc - print(io, "# meta: pop locations ($(args[2]))") + print(io, "# meta: pop locations ($(args[2]::Int))") # print anything else as "Expr(head, args...)" else unhandled = true @@ -2075,37 +2376,44 @@ end # show the called object in a signature, given its type `ft` # `io` should contain the UnionAll env of the signature -function show_signature_function(io::IO, @nospecialize(ft), demangle=false, fargname="", html=false) +function show_signature_function(io::IO, @nospecialize(ft), demangle=false, fargname="", html=false, qualified=false) uw = unwrap_unionall(ft) if ft <: Function && isa(uw, DataType) && isempty(uw.parameters) && isdefined(uw.name.module, uw.name.mt.name) && ft == typeof(getfield(uw.name.module, uw.name.mt.name)) - print(io, (demangle ? demangle_function_name : identity)(uw.name.mt.name)) + if qualified && !is_exported_from_stdlib(uw.name.mt.name, uw.name.module) && uw.name.module !== Main + print_within_stacktrace(io, uw.name.module, '.', bold=true) + end + s = sprint(show_sym, (demangle ? demangle_function_name : identity)(uw.name.mt.name), context=io) + print_within_stacktrace(io, s, bold=true) elseif isa(ft, DataType) && ft.name === Type.body.name && (f = ft.parameters[1]; !isa(f, TypeVar)) uwf = unwrap_unionall(f) parens = isa(f, UnionAll) && !(isa(uwf, DataType) && f === uwf.name.wrapper) parens && print(io, "(") - show(io, f) + print_within_stacktrace(io, f, bold=true) parens && print(io, ")") else if html print(io, "($fargname::", ft, ")") else - print(io, "($fargname::", ft, ")") + print_within_stacktrace(io, "($fargname::", ft, ")", bold=true) end end nothing end -function print_within_stacktrace(io, s...; color, bold=false) +function print_within_stacktrace(io, s...; color=:normal, bold=false) if get(io, :backtrace, false)::Bool printstyled(io, s...; color, bold) else print(io, s...) end end -function show_tuple_as_call(io::IO, name::Symbol, sig::Type, demangle=false, kwargs=nothing, argnames=nothing) + +function show_tuple_as_call(io::IO, name::Symbol, sig::Type; + demangle=false, kwargs=nothing, argnames=nothing, + qualified=false, hasfirst=true) # print a method signature tuple for a lambda definition if sig === Tuple print(io, demangle ? demangle_function_name(name) : name, "(...)") @@ -2118,19 +2426,23 @@ function show_tuple_as_call(io::IO, name::Symbol, sig::Type, demangle=false, kwa env_io = IOContext(env_io, :unionall_env => sig.var) sig = sig.body end + n = 1 sig = (sig::DataType).parameters - show_signature_function(env_io, sig[1], demangle) + if hasfirst + show_signature_function(env_io, sig[1], demangle, "", false, qualified) + n += 1 + end first = true - print_within_stacktrace(io, "(", color=:light_black) + print_within_stacktrace(io, "(", bold=true) show_argnames = argnames !== nothing && length(argnames) == length(sig) - for i = 2:length(sig) # fixme (iter): `eachindex` with offset? + for i = n:length(sig) # fixme (iter): `eachindex` with offset? first || print(io, ", ") first = false if show_argnames - print_within_stacktrace(io, argnames[i]; bold=true, color=:light_black) + print_within_stacktrace(io, argnames[i]; color=:light_black) end print(io, "::") - print_within_stacktrace(env_io, sig[i]; color=:light_black) + print_type_stacktrace(env_io, sig[i]) end if kwargs !== nothing print(io, "; ") @@ -2138,16 +2450,29 @@ function show_tuple_as_call(io::IO, name::Symbol, sig::Type, demangle=false, kwa for (k, t) in kwargs first || print(io, ", ") first = false - print_within_stacktrace(io, k; bold=true, color=:light_black) + print_within_stacktrace(io, k; color=:light_black) print(io, "::") - print_within_stacktrace(io, t; color=:light_black) + print_type_stacktrace(io, t) end end - print_within_stacktrace(io, ")", color=:light_black) + print_within_stacktrace(io, ")", bold=true) show_method_params(io, tv) nothing end +function print_type_stacktrace(io, type; color=:normal) + str = sprint(show, type, context=io) + i = findfirst('{', str) + if !get(io, :backtrace, false)::Bool + print(io, str) + elseif i === nothing + printstyled(io, str; color=color) + else + printstyled(io, str[1:prevind(str,i)]; color=color) + printstyled(io, str[i:end]; color=:light_black) + end +end + resolvebinding(@nospecialize(ex)) = ex resolvebinding(ex::QuoteNode) = ex.value resolvebinding(ex::Symbol) = resolvebinding(GlobalRef(Main, ex)) @@ -2208,11 +2533,26 @@ function show(io::IO, tv::TypeVar) nothing end +function show(io::IO, vm::Core.TypeofVararg) + print(io, "Vararg") + if isdefined(vm, :T) + print(io, "{") + show(io, vm.T) + if isdefined(vm, :N) + print(io, ", ") + show(io, vm.N) + end + print(io, "}") + end +end + module IRShow const Compiler = Core.Compiler using Core.IR import ..Base - import .Compiler: IRCode, ReturnNode, GotoIfNot, CFG, scan_ssa_use!, Argument, isexpr, compute_basic_blocks, block_for_inst + import .Compiler: IRCode, ReturnNode, GotoIfNot, CFG, scan_ssa_use!, Argument, + isexpr, compute_basic_blocks, block_for_inst, + TriState, Effects, ALWAYS_TRUE, ALWAYS_FALSE Base.getindex(r::Compiler.StmtRange, ind::Integer) = Compiler.getindex(r, ind) Base.size(r::Compiler.StmtRange) = Compiler.size(r) Base.first(r::Compiler.StmtRange) = Compiler.first(r) @@ -2245,7 +2585,7 @@ function show(io::IO, src::CodeInfo; debuginfo::Symbol=:source) # TODO: static parameter values? # only accepts :source or :none, we can't have a fallback for default since # that would break code_typed(, debuginfo=:source) iff IRShow.default_debuginfo[] = :none - IRShow.show_ir(lambda_io, src, IRShow.__debuginfo[debuginfo](src)) + IRShow.show_ir(lambda_io, src, IRShow.IRShowConfig(IRShow.__debuginfo[debuginfo](src))) else # this is a CodeInfo that has not been used as a method yet, so its locations are still LineNumberNodes body = Expr(:block) @@ -2372,7 +2712,7 @@ function dump(io::IOContext, x::DataType, n::Int, indent) if x !== Any print(io, " <: ", supertype(x)) end - if n > 0 && !(x <: Tuple) && !x.abstract + if n > 0 && !(x <: Tuple) && !isabstracttype(x) tvar_io::IOContext = io for tparam in x.parameters # approximately recapture the list of tvar parameterization @@ -2434,7 +2774,7 @@ MyStruct function dump(arg; maxdepth=DUMP_DEFAULT_MAXDEPTH) # this is typically used interactively, so default to being in Main mod = get(stdout, :module, Main) - dump(IOContext(stdout::IO, :limit => true, :module => mod), arg; maxdepth=maxdepth) + dump(IOContext(stdout, :limit => true, :module => mod), arg; maxdepth=maxdepth) end @@ -2538,6 +2878,9 @@ function array_summary(io::IO, a, inds) print(io, " with indices ", inds2string(inds)) end +## `summary` for Function +summary(io::IO, f::Function) = show(io, MIME"text/plain"(), f) + """ showarg(io::IO, x, toplevel) @@ -2599,8 +2942,9 @@ function showarg(io::IO, v::SubArray, toplevel) showindices(io, v.indices...) print(io, ')') toplevel && print(io, " with eltype ", eltype(v)) + return nothing end -showindices(io, ::Union{Slice,IdentityUnitRange}, inds...) = +showindices(io, ::Slice, inds...) = (print(io, ", :"); showindices(io, inds...)) showindices(io, ind1, inds...) = (print(io, ", ", ind1); showindices(io, inds...)) @@ -2612,14 +2956,23 @@ function showarg(io::IO, r::ReshapedArray, toplevel) print(io, ", ", join(r.dims, ", ")) print(io, ')') toplevel && print(io, " with eltype ", eltype(r)) + return nothing end -function showarg(io::IO, r::ReinterpretArray{T}, toplevel) where {T} +function showarg(io::IO, r::NonReshapedReinterpretArray{T}, toplevel) where {T} print(io, "reinterpret(", T, ", ") showarg(io, parent(r), false) print(io, ')') end +function showarg(io::IO, r::ReshapedReinterpretArray{T}, toplevel) where {T} + print(io, "reinterpret(reshape, ", T, ", ") + showarg(io, parent(r), false) + print(io, ')') + toplevel && print(io, " with eltype ", eltype(r)) + return nothing +end + # printing iterators from Base.Iterators function show(io::IO, e::Iterators.Enumerate) @@ -2672,3 +3025,15 @@ end bitshow(B::BitArray) = bitshow(stdout, B) bitstring(B::BitArray) = sprint(bitshow, B) + +# printing OpaqueClosure +function show(io::IO, oc::Core.OpaqueClosure) + A, R = typeof(oc).parameters + show_tuple_as_call(io, Symbol(""), A; hasfirst=false) + print(io, "::", R) + print(io, "->◌") +end + +function show(io::IO, ::MIME"text/plain", oc::Core.OpaqueClosure{A, R}) where {A, R} + show(io, oc) +end diff --git a/base/simdloop.jl b/base/simdloop.jl index e0b6d89d972775..29e2382cf39aa8 100644 --- a/base/simdloop.jl +++ b/base/simdloop.jl @@ -8,7 +8,7 @@ export @simd, simd_outer_range, simd_inner_length, simd_index # Error thrown from ill-formed uses of @simd struct SimdError <: Exception - msg::AbstractString + msg::String end # Parse iteration space expression diff --git a/base/some.jl b/base/some.jl index 82638e4250d2e7..8be58739a4df41 100644 --- a/base/some.jl +++ b/base/some.jl @@ -16,7 +16,7 @@ Some(::Type{T}) where {T} = Some{Type{T}}(T) promote_rule(::Type{Some{T}}, ::Type{Some{S}}) where {T, S<:T} = Some{T} -nonnothingtype(::Type{T}) where {T} = Core.Compiler.typesubtract(T, Nothing) +nonnothingtype(::Type{T}) where {T} = typesplit(T, Nothing) promote_rule(T::Type{Nothing}, S::Type) = Union{S, Nothing} function promote_rule(T::Type{>:Nothing}, S::Type) R = nonnothingtype(T) @@ -34,6 +34,8 @@ end convert(::Type{T}, x::T) where {T>:Nothing} = x convert(::Type{T}, x) where {T>:Nothing} = convert(nonnothingtype_checked(T), x) +convert(::Type{Nothing}, x) = throw(MethodError(convert, (Nothing, x))) +convert(::Type{Nothing}, ::Nothing) = nothing convert(::Type{Some{T}}, x::Some{T}) where {T} = x convert(::Type{Some{T}}, x::Some) where {T} = Some{T}(convert(T, x.value)) @@ -62,19 +64,20 @@ Return `true` if `x === nothing`, and return `false` if not. !!! compat "Julia 1.1" This function requires at least Julia 1.1. + +See also [`something`](@ref), [`notnothing`](@ref), [`ismissing`](@ref). """ -isnothing(::Any) = false -isnothing(::Nothing) = true +isnothing(x) = x === nothing """ - something(x, y...) + something(x...) Return the first value in the arguments which is not equal to [`nothing`](@ref), if any. Otherwise throw an error. Arguments of type [`Some`](@ref) are unwrapped. -See also [`coalesce`](@ref). +See also [`coalesce`](@ref), [`skipmissing`](@ref), [`@something`](@ref). # Examples ```jldoctest @@ -97,3 +100,46 @@ something() = throw(ArgumentError("No value arguments present")) something(x::Nothing, y...) = something(y...) something(x::Some, y...) = x.value something(x::Any, y...) = x + + +""" + @something(x...) + +Short-circuiting version of [`something`](@ref). + +# Examples +```jldoctest +julia> f(x) = (println("f(\$x)"); nothing); + +julia> a = 1; + +julia> a = @something a f(2) f(3) error("Unable to find default for `a`") +1 + +julia> b = nothing; + +julia> b = @something b f(2) f(3) error("Unable to find default for `b`") +f(2) +f(3) +ERROR: Unable to find default for `b` +[...] + +julia> b = @something b f(2) f(3) Some(nothing) +f(2) +f(3) + +julia> b === nothing +true +``` + +!!! compat "Julia 1.7" + This macro is available as of Julia 1.7. +""" +macro something(args...) + expr = :(nothing) + for arg in reverse(args) + expr = :(val = $(esc(arg)); val !== nothing ? val : ($expr)) + end + something = GlobalRef(Base, :something) + return :($something($expr)) +end diff --git a/base/sort.jl b/base/sort.jl index ddd09e89cc2ec9..23579abd77547e 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -5,12 +5,13 @@ module Sort import ..@__MODULE__, ..parentmodule const Base = parentmodule(@__MODULE__) using .Base.Order -using .Base: copymutable, LinearIndices, length, (:), +using .Base: copymutable, LinearIndices, length, (:), iterate, eachindex, axes, first, last, similar, zip, OrdinalRange, AbstractVector, @inbounds, AbstractRange, @eval, @inline, Vector, @noinline, AbstractMatrix, AbstractUnitRange, isless, identity, eltype, >, <, <=, >=, |, +, -, *, !, extrema, sub_with_overflow, add_with_overflow, oneunit, div, getindex, setindex!, - length, resize!, fill, Missing, require_one_based_indexing, keytype + length, resize!, fill, Missing, require_one_based_indexing, keytype, UnitRange, + min, max, reinterpret, signed, unsigned, Signed, Unsigned, typemin, xor, Type, BitSigned using .Base: >>>, !== @@ -27,6 +28,7 @@ export # also exported by Base searchsorted, searchsortedfirst, searchsortedlast, + insorted, # order & algorithm: sort, sort!, @@ -67,7 +69,7 @@ function issorted(itr, order::Ordering) end """ - issorted(v, lt=isless, by=identity, rev:Bool=false, order::Ordering=Forward) + issorted(v, lt=isless, by=identity, rev::Bool=false, order::Ordering=Forward) Test whether a vector is in sorted order. The `lt`, `by` and `rev` keywords modify what order is considered to be sorted just as they do for [`sort`](@ref). @@ -230,92 +232,62 @@ end function searchsortedlast(a::AbstractRange{<:Real}, x::Real, o::DirectOrdering)::keytype(a) require_one_based_indexing(a) - if step(a) == 0 - lt(o, x, first(a)) ? 0 : length(a) + f, h, l = first(a), step(a), last(a) + if lt(o, x, f) + 0 + elseif h == 0 || !lt(o, x, l) + length(a) else - n = round(Integer, clamp((x - first(a)) / step(a) + 1, 1, length(a))) + n = round(Integer, (x - f) / h + 1) lt(o, x, a[n]) ? n - 1 : n end end function searchsortedfirst(a::AbstractRange{<:Real}, x::Real, o::DirectOrdering)::keytype(a) require_one_based_indexing(a) - if step(a) == 0 - lt(o, first(a), x) ? length(a) + 1 : 1 + f, h, l = first(a), step(a), last(a) + if !lt(o, f, x) + 1 + elseif h == 0 || lt(o, l, x) + length(a) + 1 else - n = round(Integer, clamp((x - first(a)) / step(a) + 1, 1, length(a))) + n = round(Integer, (x - f) / h + 1) lt(o, a[n], x) ? n + 1 : n end end function searchsortedlast(a::AbstractRange{<:Integer}, x::Real, o::DirectOrdering)::keytype(a) require_one_based_indexing(a) - h = step(a) - if h == 0 - lt(o, x, first(a)) ? 0 : length(a) - elseif h > 0 && x < first(a) - firstindex(a) - 1 - elseif h > 0 && x >= last(a) - lastindex(a) - elseif h < 0 && x > first(a) - firstindex(a) - 1 - elseif h < 0 && x <= last(a) - lastindex(a) + f, h, l = first(a), step(a), last(a) + if lt(o, x, f) + 0 + elseif h == 0 || !lt(o, x, l) + length(a) else if o isa ForwardOrdering - fld(floor(Integer, x) - first(a), h) + 1 + fld(floor(Integer, x) - f, h) + 1 else - fld(ceil(Integer, x) - first(a), h) + 1 + fld(ceil(Integer, x) - f, h) + 1 end end end function searchsortedfirst(a::AbstractRange{<:Integer}, x::Real, o::DirectOrdering)::keytype(a) require_one_based_indexing(a) - h = step(a) - if h == 0 - lt(o, first(a), x) ? length(a)+1 : 1 - elseif h > 0 && x <= first(a) - firstindex(a) - elseif h > 0 && x > last(a) - lastindex(a) + 1 - elseif h < 0 && x >= first(a) - firstindex(a) - elseif h < 0 && x < last(a) - lastindex(a) + 1 + f, h, l = first(a), step(a), last(a) + if !lt(o, f, x) + 1 + elseif h == 0 || lt(o, l, x) + length(a) + 1 else if o isa ForwardOrdering - -fld(floor(Integer, -x) + Signed(first(a)), h) + 1 + cld(ceil(Integer, x) - f, h) + 1 else - -fld(ceil(Integer, -x) + Signed(first(a)), h) + 1 + cld(floor(Integer, x) - f, h) + 1 end end end -function searchsortedfirst(a::AbstractRange{<:Integer}, x::Unsigned, o::DirectOrdering)::keytype(a) - require_one_based_indexing(a) - if lt(o, first(a), x) - if step(a) == 0 - length(a) + 1 - else - min(cld(x - first(a), step(a)), length(a)) + 1 - end - else - 1 - end -end - -function searchsortedlast(a::AbstractRange{<:Integer}, x::Unsigned, o::DirectOrdering)::keytype(a) - require_one_based_indexing(a) - if lt(o, x, first(a)) - 0 - elseif step(a) == 0 - length(a) - else - min(fld(x - first(a), step(a)) + 1, length(a)) - end -end - searchsorted(a::AbstractRange{<:Real}, x::Real, o::DirectOrdering) = searchsortedfirst(a, x, o) : searchsortedlast(a, x, o) @@ -336,6 +308,8 @@ according to the order specified by the `by`, `lt` and `rev` keywords, assuming is already sorted in that order. Return an empty range located at the insertion point if `a` does not contain values equal to `x`. +See also: [`insorted`](@ref), [`searchsortedfirst`](@ref), [`sort`](@ref), [`findall`](@ref). + # Examples ```jldoctest julia> searchsorted([1, 2, 4, 5, 5, 7], 4) # single match @@ -359,9 +333,11 @@ julia> searchsorted([1, 2, 4, 5, 5, 7], 0) # no match, insert at start searchsortedfirst(a, x; by=, lt=, rev=false) Return the index of the first value in `a` greater than or equal to `x`, according to the -specified order. Return `length(a) + 1` if `x` is greater than all values in `a`. +specified order. Return `lastindex(a) + 1` if `x` is greater than all values in `a`. `a` is assumed to be sorted. +See also: [`searchsortedlast`](@ref), [`searchsorted`](@ref), [`findfirst`](@ref). + # Examples ```jldoctest julia> searchsortedfirst([1, 2, 4, 5, 5, 7], 4) # single match @@ -385,8 +361,8 @@ julia> searchsortedfirst([1, 2, 4, 5, 5, 7], 0) # no match, insert at start searchsortedlast(a, x; by=, lt=, rev=false) Return the index of the last value in `a` less than or equal to `x`, according to the -specified order. Return `0` if `x` is less than all values in `a`. `a` is assumed to -be sorted. +specified order. Return `firstindex(a) - 1` if `x` is less than all values in `a`. `a` is +assumed to be sorted. # Examples ```jldoctest @@ -407,6 +383,40 @@ julia> searchsortedlast([1, 2, 4, 5, 5, 7], 0) # no match, insert at start ``` """ searchsortedlast +""" + insorted(a, x; by=, lt=, rev=false) -> Bool + +Determine whether an item is in the given sorted collection, in the sense that +it is [`==`](@ref) to one of the values of the collection according to the order +specified by the `by`, `lt` and `rev` keywords, assuming that `a` is already +sorted in that order, see [`sort`](@ref) for the keywords. + +See also [`in`](@ref). + +# Examples +```jldoctest +julia> insorted(4, [1, 2, 4, 5, 5, 7]) # single match +true + +julia> insorted(5, [1, 2, 4, 5, 5, 7]) # multiple matches +true + +julia> insorted(3, [1, 2, 4, 5, 5, 7]) # no match +false + +julia> insorted(9, [1, 2, 4, 5, 5, 7]) # no match +false + +julia> insorted(0, [1, 2, 4, 5, 5, 7]) # no match +false +``` + +!!! compat "Julia 1.6" + `insorted` was added in Julia 1.6. +""" +function insorted end +insorted(x, v::AbstractVector; kw...) = !isempty(searchsorted(v, x; kw...)) +insorted(x, r::AbstractRange) = in(x, r) ## sorting algorithms ## @@ -416,6 +426,22 @@ struct InsertionSortAlg <: Algorithm end struct QuickSortAlg <: Algorithm end struct MergeSortAlg <: Algorithm end +""" + AdaptiveSort(fallback) + +Indicate that a sorting function should use the fastest available algorithm. + +Adaptive sort will use the algorithm specified by `fallback` for types and orders that are +not [`UIntMappable`](@ref). Otherwise, it will typically use: + * Insertion sort for short vectors + * Radix sort for long vectors + * Counting sort for vectors of integers spanning a short range + +Adaptive sort is guaranteed to be stable if the fallback algorithm is stable. +""" +struct AdaptiveSort{Fallback <: Algorithm} <: Algorithm + fallback::Fallback +end """ PartialQuickSort{T <: Union{Integer,OrdinalRange}} @@ -441,7 +467,7 @@ end Indicate that a sorting function should use the insertion sort algorithm. Insertion sort traverses the collection one element at a time, inserting each element into its correct, sorted position in -the output list. +the output vector. Characteristics: * *stable*: preserves the ordering of elements which @@ -485,8 +511,8 @@ Characteristics: """ const MergeSort = MergeSortAlg() -const DEFAULT_UNSTABLE = QuickSort -const DEFAULT_STABLE = MergeSort +const DEFAULT_UNSTABLE = AdaptiveSort(QuickSort) +const DEFAULT_STABLE = AdaptiveSort(MergeSort) const SMALL_ALGORITHM = InsertionSort const SMALL_THRESHOLD = 20 @@ -494,13 +520,9 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, ::InsertionSortAlg, @inbounds for i = lo+1:hi j = i x = v[i] - while j > lo - if lt(o, x, v[j-1]) - v[j] = v[j-1] - j -= 1 - continue - end - break + while j > lo && lt(o, x, v[j-1]) + v[j] = v[j-1] + j -= 1 end v[j] = x end @@ -616,40 +638,20 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::MergeSortAlg, o:: return v end -function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort{<:Integer}, +function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, o::Ordering) @inbounds while lo < hi hi-lo <= SMALL_THRESHOLD && return sort!(v, lo, hi, SMALL_ALGORITHM, o) j = partition!(v, lo, hi, o) - if j >= a.k - # we don't need to sort anything bigger than j - hi = j-1 - elseif j-lo < hi-j - # recurse on the smaller chunk - # this is necessary to preserve O(log(n)) - # stack space in the worst case (rather than O(n)) - lo < (j-1) && sort!(v, lo, j-1, a, o) - lo = j+1 - else - (j+1) < hi && sort!(v, j+1, hi, a, o) - hi = j-1 - end - end - return v -end - - -function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort{T}, - o::Ordering) where T<:OrdinalRange - @inbounds while lo < hi - hi-lo <= SMALL_THRESHOLD && return sort!(v, lo, hi, SMALL_ALGORITHM, o) - j = partition!(v, lo, hi, o) if j <= first(a.k) lo = j+1 elseif j >= last(a.k) hi = j-1 else + # recurse on the smaller chunk + # this is necessary to preserve O(log(n)) + # stack space in the worst case (rather than O(n)) if j-lo < hi-j lo < (j-1) && sort!(v, lo, j-1, a, o) lo = j+1 @@ -662,11 +664,202 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort{ return v end +# This is a stable least significant bit first radix sort. +# +# That is, it first sorts the entire vector by the last chunk_size bits, then by the second +# to last chunk_size bits, and so on. Stability means that it will not reorder two elements +# that compare equal. This is essential so that the order introduced by earlier, +# less significant passes is preserved by later passes. +# +# Each pass divides the input into 2^chunk_size == mask+1 buckets. To do this, it +# * counts the number of entries that fall into each bucket +# * uses those counts to compute the indices to move elements of those buckets into +# * moves elements into the computed indices in the swap array +# * switches the swap and working array +# +# In the case of an odd number of passes, the returned vector will === the input vector t, +# not v. This is one of the many reasons radix_sort! is not exported. +function radix_sort!(v::AbstractVector{U}, lo::Integer, hi::Integer, bits::Unsigned, + t::AbstractVector{U}, chunk_size=radix_chunk_size_heuristic(lo, hi, bits)) where U <: Unsigned + # bits is unsigned for performance reasons. + mask = UInt(1) << chunk_size - 0x1 + counts = Vector{UInt}(undef, mask+2) + + @inbounds for shift in 0:chunk_size:bits-1 + + # counts[2:mask+2] will store the number of elements that fall into each bucket. + # if chunk_size = 8, counts[2] is bucket 0x00 and counts[257] is bucket 0xff. + counts .= 0 + for k in lo:hi + x = v[k] # lookup the element + i = (x >> shift)&mask + 2 # compute its bucket's index for this pass + counts[i] += 1 # increment that bucket's count + end + + counts[1] = lo # set target index for the first bucket + cumsum!(counts, counts) # set target indices for subsequent buckets + # counts[1:mask+1] now stores indices where the first member of each bucket + # belongs, not the number of elements in each bucket. We will put the first element + # of bucket 0x00 in t[counts[1]], the next element of bucket 0x00 in t[counts[1]+1], + # and the last element of bucket 0x00 in t[counts[2]-1]. + + for k in lo:hi + x = v[k] # lookup the element + i = (x >> shift)&mask + 1 # compute its bucket's index for this pass + j = counts[i] # lookup the target index + t[j] = x # put the element where it belongs + counts[i] = j + 1 # increment the target index for the next + end # ↳ element in this bucket + + v, t = t, v # swap the now sorted destination vector t back into primary vector v + + end + + v +end +function radix_chunk_size_heuristic(lo::Integer, hi::Integer, bits::Unsigned) + # chunk_size is the number of bits to radix over at once. + # We need to allocate an array of size 2^chunk size, and on the other hand the higher + # the chunk size the fewer passes we need. Theoretically, chunk size should be based on + # the Lambert W function applied to length. Empirically, we use this heuristic: + guess = min(10, log(maybe_unsigned(hi-lo))*3/4+3) + # TODO the maximum chunk size should be based on archetecture cache size. + + # We need iterations * chunk size ≥ bits, and these cld's + # make an effort to get iterations * chunk size ≈ bits + UInt8(cld(bits, cld(bits, guess))) +end + +# For AbstractVector{Bool}, counting sort is always best. +# This is an implementation of counting sort specialized for Bools. +function sort!(v::AbstractVector{<:Bool}, lo::Integer, hi::Integer, a::AdaptiveSort, o::Ordering) + first = lt(o, false, true) ? false : lt(o, true, false) ? true : return v + count = 0 + @inbounds for i in lo:hi + if v[i] == first + count += 1 + end + end + @inbounds v[lo:lo+count-1] .= first + @inbounds v[lo+count:hi] .= !first + v +end + +maybe_unsigned(x::Integer) = x # this is necessary to avoid calling unsigned on BigInt +maybe_unsigned(x::BitSigned) = unsigned(x) +function _extrema(v::AbstractArray, lo::Integer, hi::Integer, o::Ordering) + mn = mx = v[lo] + @inbounds for i in (lo+1):hi + vi = v[i] + lt(o, vi, mn) && (mn = vi) + lt(o, mx, vi) && (mx = vi) + end + mn, mx +end +function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::AdaptiveSort, o::Ordering) + # if the sorting task is not UIntMappable, then we can't radix sort or sort_int_range! + # so we skip straight to the fallback algorithm which is comparison based. + U = UIntMappable(eltype(v), o) + U === nothing && return sort!(v, lo, hi, a.fallback, o) + + # to avoid introducing excessive detection costs for the trivial sorting problem + # and to avoid overflow, we check for small inputs before any other runtime checks + hi <= lo && return v + lenm1 = maybe_unsigned(hi-lo) # adding 1 would risk overflow + # only count sort on a short range can compete with insertion sort when lenm1 < 40 + # and the optimization is not worth the detection cost, so we use insertion sort. + lenm1 < 40 && return sort!(v, lo, hi, SMALL_ALGORITHM, o) + + # For most arrays, a presorted check is cheap (overhead < 5%) and for most large + # arrays it is essentially free (<1%). Insertion sort runs in a fast O(n) on presorted + # input and this guarantees presorted input will always be efficiently handled + issorted(view(v, lo:hi), o) && return v + + # For large arrays, a reverse-sorted check is essentially free (overhead < 1%) + if lenm1 >= 500 && issorted(view(v, lo:hi), ReverseOrdering(o)) + reverse!(view(v, lo:hi)) + return v + end + + # UInt128 does not support fast bit shifting so we never + # dispatch to radix sort but we may still perform count sort + if sizeof(U) > 8 + if eltype(v) <: Integer && o isa DirectOrdering + v_min, v_max = _extrema(v, lo, hi, Forward) + v_range = maybe_unsigned(v_max-v_min) + v_range == 0 && return v # all same + + # we know lenm1 ≥ 40, so this will never underflow. + # if lenm1 > 3.7e18 (59 exabytes), then this may incorrectly dispatch to fallback + if v_range < 5lenm1-100 # count sort will outperform comparison sort if v's range is small + return sort_int_range!(v, Int(v_range+1), v_min, o === Forward ? identity : reverse, lo, hi) + end + end + return sort!(v, lo, hi, a.fallback, o) + end + + v_min, v_max = _extrema(v, lo, hi, o) + lt(o, v_min, v_max) || return v # all same + if eltype(v) <: Integer && o isa DirectOrdering + R = o === Reverse + v_range = maybe_unsigned(R ? v_min-v_max : v_max-v_min) + if v_range < div(lenm1, 2) # count sort will be superior if v's range is very small + return sort_int_range!(v, Int(v_range+1), R ? v_max : v_min, R ? reverse : identity, lo, hi) + end + end + + u_min, u_max = uint_map(v_min, o), uint_map(v_max, o) + u_range = maybe_unsigned(u_max-u_min) + if u_range < div(lenm1, 2) # count sort will be superior if u's range is very small + u = uint_map!(v, lo, hi, o) + sort_int_range!(u, Int(u_range+1), u_min, identity, lo, hi) + return uint_unmap!(v, u, lo, hi, o) + end + + # if u's range is small, then once we subtract out v_min, we'll get a vector like + # UInt16[0x001a, 0x0015, 0x0006, 0x001b, 0x0008, 0x000c, 0x0001, 0x000e, 0x001c, 0x0009] + # where we only need to radix over the last few bits (5, in the example). + bits = unsigned(8sizeof(u_range) - leading_zeros(u_range)) + + # radix sort runs in O(bits * lenm1), insertion sort runs in O(lenm1^2). Radix sort + # has a constant factor that is three times higher, so radix runtime is 3bits * lenm1 + # and insertion runtime is lenm1^2. Empirically, insertion is faster than radix iff + # lenm1 < 3bits. + # Insertion < Radix + # lenm1^2 < 3 * bits * lenm1 + # lenm1 < 3bits + if lenm1 < 3bits + # at lenm1 = 64*3-1, QuickSort is about 20% faster than InsertionSort. + alg = a.fallback === QuickSort && lenm1 > 120 ? QuickSort : SMALL_ALGORITHM + return sort!(v, lo, hi, alg, o) + end + + # At this point, we are committed to radix sort. + u = uint_map!(v, lo, hi, o) + + # we subtract u_min to avoid radixing over unnecessary bits. For example, + # Int32[3, -1, 2] uint_maps to UInt32[0x80000003, 0x7fffffff, 0x80000002] + # which uses all 32 bits, but once we subtract u_min = 0x7fffffff, we are left with + # UInt32[0x00000004, 0x00000000, 0x00000003] which uses only 3 bits, and + # Float32[2.012, 400.0, 12.345] uint_maps to UInt32[0x3fff3b63, 0x3c37ffff, 0x414570a4] + # which is reduced to UInt32[0x03c73b64, 0x00000000, 0x050d70a5] using only 26 bits. + # the overhead for this subtraction is small enough that it is worthwhile in many cases. + + # this is faster than u[lo:hi] .-= u_min as of v1.9.0-DEV.100 + @inbounds for i in lo:hi + u[i] -= u_min + end + + u2 = radix_sort!(u, lo, hi, bits, similar(u)) + uint_unmap!(v, u2, lo, hi, o, u_min) +end ## generic sorting methods ## defalg(v::AbstractArray) = DEFAULT_STABLE defalg(v::AbstractArray{<:Union{Number, Missing}}) = DEFAULT_UNSTABLE +defalg(v::AbstractArray{Missing}) = DEFAULT_UNSTABLE # for method disambiguation +defalg(v::AbstractArray{Union{}}) = DEFAULT_UNSTABLE # for method disambiguation function sort!(v::AbstractVector, alg::Algorithm, order::Ordering) inds = axes(v,1) @@ -680,7 +873,8 @@ Sort the vector `v` in place. [`QuickSort`](@ref) is used by default for numeric [`MergeSort`](@ref) is used for other arrays. You can specify an algorithm to use via the `alg` keyword (see [Sorting Algorithms](@ref) for available algorithms). The `by` keyword lets you provide a function that will be applied to each element before comparison; the `lt` keyword allows -providing a custom "less than" function; use `rev=true` to reverse the sorting order. These +providing a custom "less than" function (note that for every `x` and `y`, only one of `lt(x,y)` +and `lt(y,x)` can return `true`); use `rev=true` to reverse the sorting order. These options are independent and can be used together in all possible combinations: if both `by` and `lt` are specified, the `lt` function is applied to the result of the `by` function; `rev=true` reverses whatever ordering specified via the `by` and `lt` keywords. @@ -718,33 +912,22 @@ function sort!(v::AbstractVector; by=identity, rev::Union{Bool,Nothing}=nothing, order::Ordering=Forward) - ordr = ord(lt,by,rev,order) - if (ordr === Forward || ordr === Reverse) && eltype(v)<:Integer - n = length(v) - if n > 1 - min, max = extrema(v) - (diff, o1) = sub_with_overflow(max, min) - (rangelen, o2) = add_with_overflow(diff, oneunit(diff)) - if !o1 && !o2 && rangelen < div(n,2) - return sort_int_range!(v, rangelen, min, ordr === Reverse ? reverse : identity) - end - end - end - sort!(v, alg, ordr) + sort!(v, alg, ord(lt,by,rev,order)) end # sort! for vectors of few unique integers -function sort_int_range!(x::AbstractVector{<:Integer}, rangelen, minval, maybereverse) +function sort_int_range!(x::AbstractVector{<:Integer}, rangelen, minval, maybereverse, + lo=firstindex(x), hi=lastindex(x)) offs = 1 - minval - where = fill(0, rangelen) - @inbounds for i = eachindex(x) - where[x[i] + offs] += 1 + counts = fill(0, rangelen) + @inbounds for i = lo:hi + counts[x[i] + offs] += 1 end - idx = firstindex(x) + idx = lo @inbounds for i = maybereverse(1:rangelen) - lastidx = idx + where[i] - 1 + lastidx = idx + counts[i] - 1 val = i-offs for j = idx:lastidx x[j] = val @@ -893,7 +1076,7 @@ using the same keywords as [`sort!`](@ref). The permutation is guaranteed to be if the sorting algorithm is unstable, meaning that indices of equal elements appear in ascending order. -See also [`sortperm!`](@ref). +See also [`sortperm!`](@ref), [`partialsortperm`](@ref), [`invperm`](@ref), [`indexin`](@ref). # Examples ```jldoctest @@ -985,22 +1168,22 @@ function sortperm_int_range(x::Vector{<:Integer}, rangelen, minval) offs = 1 - minval n = length(x) - where = fill(0, rangelen+1) - where[1] = 1 + counts = fill(0, rangelen+1) + counts[1] = 1 @inbounds for i = 1:n - where[x[i] + offs + 1] += 1 + counts[x[i] + offs + 1] += 1 end - #cumsum!(where, where) - @inbounds for i = 2:length(where) - where[i] += where[i-1] + #cumsum!(counts, counts) + @inbounds for i = 2:length(counts) + counts[i] += counts[i-1] end P = Vector{Int}(undef, n) @inbounds for i = 1:n label = x[i] + offs - P[where[label]] = i - where[label] += 1 + P[counts[label]] = i + counts[label] += 1 end return P @@ -1108,7 +1291,7 @@ function sort!(A::AbstractArray; 1 <= k <= nd || throw(ArgumentError("dimension out of range")) - remdims = ntuple(i -> i == k ? 1 : size(A, i), nd) + remdims = ntuple(i -> i == k ? 1 : axes(A, i), nd) for idx in CartesianIndices(remdims) Av = view(A, ntuple(i -> i == k ? Colon() : idx[i], nd)...) sort!(Av, alg, ordr) @@ -1116,18 +1299,113 @@ function sort!(A::AbstractArray; A end + +## uint mapping to allow radix sorting primitives other than UInts ## + +""" + UIntMappable(T::Type, order::Ordering) + +Return `typeof(uint_map(x::T, order))` if [`uint_map`](@ref) and +[`uint_unmap`](@ref) are implemented. + +If either is not implemented, return `nothing`. +""" +UIntMappable(T::Type, order::Ordering) = nothing + +""" + uint_map(x, order::Ordering)::Unsigned + +Map `x` to an un unsigned integer, maintaining sort order. + +The map should be reversible with [`uint_unmap`](@ref), so `isless(order, a, b)` must be +a linear ordering for `a, b <: typeof(x)`. Satisfies +`isless(order, a, b) === (uint_map(a, order) < uint_map(b, order))` +and `x === uint_unmap(typeof(x), uint_map(x, order), order)` + +See also: [`UIntMappable`](@ref) [`uint_unmap`](@ref) +""" +function uint_map end + +""" + uint_unmap(T::Type, u::Unsigned, order::Ordering) + +Reconstruct the unique value `x::T` that uint_maps to `u`. Satisfies +`x === uint_unmap(T, uint_map(x::T, order), order)` for all `x <: T`. + +See also: [`uint_map`](@ref) [`UIntMappable`](@ref) +""" +function uint_unmap end + + +### Primitive Types + +# Integers +uint_map(x::Unsigned, ::ForwardOrdering) = x +uint_unmap(::Type{T}, u::T, ::ForwardOrdering) where T <: Unsigned = u + +uint_map(x::Signed, ::ForwardOrdering) = + unsigned(xor(x, typemin(x))) +uint_unmap(::Type{T}, u::Unsigned, ::ForwardOrdering) where T <: Signed = + xor(signed(u), typemin(T)) + +# unsigned(Int) is not available during bootstrapping. +for (U, S) in [(UInt8, Int8), (UInt16, Int16), (UInt32, Int32), (UInt64, Int64), (UInt128, Int128)] + @eval UIntMappable(::Type{<:Union{$U, $S}}, ::ForwardOrdering) = $U +end + +# Floats are not UIntMappable under regular orderings because they fail on NaN edge cases. +# uint mappings for floats are defined in Float, where the Left and Right orderings +# guarantee that there are no NaN values + +# Chars +uint_map(x::Char, ::ForwardOrdering) = reinterpret(UInt32, x) +uint_unmap(::Type{Char}, u::UInt32, ::ForwardOrdering) = reinterpret(Char, u) +UIntMappable(::Type{Char}, ::ForwardOrdering) = UInt32 + +### Reverse orderings +uint_map(x, rev::ReverseOrdering) = ~uint_map(x, rev.fwd) +uint_unmap(T::Type, u::Unsigned, rev::ReverseOrdering) = uint_unmap(T, ~u, rev.fwd) +UIntMappable(T::Type, order::ReverseOrdering) = UIntMappable(T, order.fwd) + + +### Vectors + +# Convert v to unsigned integers in place, maintaining sort order. +function uint_map!(v::AbstractVector, lo::Integer, hi::Integer, order::Ordering) + u = reinterpret(UIntMappable(eltype(v), order), v) + @inbounds for i in lo:hi + u[i] = uint_map(v[i], order) + end + u +end + +function uint_unmap!(v::AbstractVector, u::AbstractVector{U}, lo::Integer, hi::Integer, + order::Ordering, offset::U=zero(U)) where U <: Unsigned + @inbounds for i in lo:hi + v[i] = uint_unmap(eltype(v), u[i]+offset, order) + end + v +end + + ## fast clever sorting for floats ## module Float using ..Sort using ...Order -using ..Base: @inbounds, AbstractVector, Vector, last, axes +using ..Base: @inbounds, AbstractVector, Vector, last, axes, Missing, Type, reinterpret import Core.Intrinsics: slt_int -import ..Sort: sort! +import ..Sort: sort!, UIntMappable, uint_map, uint_unmap import ...Order: lt, DirectOrdering const Floats = Union{Float32,Float64} +const FPSortable = Union{ # Mixed Float32 and Float64 are not allowed. + AbstractVector{Union{Float32, Missing}}, + AbstractVector{Union{Float64, Missing}}, + AbstractVector{Float32}, + AbstractVector{Float64}, + AbstractVector{Missing}} struct Left <: Ordering end struct Right <: Ordering end @@ -1141,17 +1419,40 @@ right(o::Perm) = Perm(right(o.order), o.data) lt(::Left, x::T, y::T) where {T<:Floats} = slt_int(y, x) lt(::Right, x::T, y::T) where {T<:Floats} = slt_int(x, y) +uint_map(x::Float32, ::Left) = ~reinterpret(UInt32, x) +uint_unmap(::Type{Float32}, u::UInt32, ::Left) = reinterpret(Float32, ~u) +uint_map(x::Float32, ::Right) = reinterpret(UInt32, x) +uint_unmap(::Type{Float32}, u::UInt32, ::Right) = reinterpret(Float32, u) +UIntMappable(::Type{Float32}, ::Union{Left, Right}) = UInt32 + +uint_map(x::Float64, ::Left) = ~reinterpret(UInt64, x) +uint_unmap(::Type{Float64}, u::UInt64, ::Left) = reinterpret(Float64, ~u) +uint_map(x::Float64, ::Right) = reinterpret(UInt64, x) +uint_unmap(::Type{Float64}, u::UInt64, ::Right) = reinterpret(Float64, u) +UIntMappable(::Type{Float64}, ::Union{Left, Right}) = UInt64 + isnan(o::DirectOrdering, x::Floats) = (x!=x) +isnan(o::DirectOrdering, x::Missing) = false isnan(o::Perm, i::Integer) = isnan(o.order,o.data[i]) -function nans2left!(v::AbstractVector, o::Ordering, lo::Integer=first(axes(v,1)), hi::Integer=last(axes(v,1))) +ismissing(o::DirectOrdering, x::Floats) = false +ismissing(o::DirectOrdering, x::Missing) = true +ismissing(o::Perm, i::Integer) = ismissing(o.order,o.data[i]) + +allowsmissing(::AbstractVector{T}, ::DirectOrdering) where {T} = T >: Missing +allowsmissing(::AbstractVector{<:Integer}, + ::Perm{<:DirectOrdering,<:AbstractVector{T}}) where {T} = + T >: Missing + +function specials2left!(testf::Function, v::AbstractVector, o::Ordering, + lo::Integer=first(axes(v,1)), hi::Integer=last(axes(v,1))) i = lo - @inbounds while i <= hi && isnan(o,v[i]) + @inbounds while i <= hi && testf(o,v[i]) i += 1 end j = i + 1 @inbounds while j <= hi - if isnan(o,v[j]) + if testf(o,v[j]) v[i], v[j] = v[j], v[i] i += 1 end @@ -1159,14 +1460,15 @@ function nans2left!(v::AbstractVector, o::Ordering, lo::Integer=first(axes(v,1)) end return i, hi end -function nans2right!(v::AbstractVector, o::Ordering, lo::Integer=first(axes(v,1)), hi::Integer=last(axes(v,1))) +function specials2right!(testf::Function, v::AbstractVector, o::Ordering, + lo::Integer=first(axes(v,1)), hi::Integer=last(axes(v,1))) i = hi - @inbounds while lo <= i && isnan(o,v[i]) + @inbounds while lo <= i && testf(o,v[i]) i -= 1 end j = i - 1 @inbounds while lo <= j - if isnan(o,v[j]) + if testf(o,v[j]) v[i], v[j] = v[j], v[i] i -= 1 end @@ -1175,17 +1477,46 @@ function nans2right!(v::AbstractVector, o::Ordering, lo::Integer=first(axes(v,1) return lo, i end -nans2end!(v::AbstractVector, o::ForwardOrdering) = nans2right!(v,o) -nans2end!(v::AbstractVector, o::ReverseOrdering) = nans2left!(v,o) -nans2end!(v::AbstractVector{<:Integer}, o::Perm{<:ForwardOrdering}) = nans2right!(v,o) -nans2end!(v::AbstractVector{<:Integer}, o::Perm{<:ReverseOrdering}) = nans2left!(v,o) +function specials2left!(v::AbstractVector, a::Algorithm, o::Ordering) + lo, hi = first(axes(v,1)), last(axes(v,1)) + if allowsmissing(v, o) + i, _ = specials2left!((v, o) -> ismissing(v, o) || isnan(v, o), v, o, lo, hi) + sort!(v, lo, i-1, a, o) + return i, hi + else + return specials2left!(isnan, v, o, lo, hi) + end +end +function specials2right!(v::AbstractVector, a::Algorithm, o::Ordering) + lo, hi = first(axes(v,1)), last(axes(v,1)) + if allowsmissing(v, o) + _, i = specials2right!((v, o) -> ismissing(v, o) || isnan(v, o), v, o, lo, hi) + sort!(v, i+1, hi, a, o) + return lo, i + else + return specials2right!(isnan, v, o, lo, hi) + end +end + +specials2end!(v::AbstractVector, a::Algorithm, o::ForwardOrdering) = + specials2right!(v, a, o) +specials2end!(v::AbstractVector, a::Algorithm, o::ReverseOrdering) = + specials2left!(v, a, o) +specials2end!(v::AbstractVector{<:Integer}, a::Algorithm, o::Perm{<:ForwardOrdering}) = + specials2right!(v, a, o) +specials2end!(v::AbstractVector{<:Integer}, a::Algorithm, o::Perm{<:ReverseOrdering}) = + specials2left!(v, a, o) issignleft(o::ForwardOrdering, x::Floats) = lt(o, x, zero(x)) issignleft(o::ReverseOrdering, x::Floats) = lt(o, x, -zero(x)) issignleft(o::Perm, i::Integer) = issignleft(o.order, o.data[i]) function fpsort!(v::AbstractVector, a::Algorithm, o::Ordering) - i, j = lo, hi = nans2end!(v,o) + # fpsort!'s optimizations speed up comparisons, of which there are O(nlogn). + # The overhead is O(n). For n < 10, it's not worth it. + length(v) < 10 && return sort!(v, first(axes(v,1)), last(axes(v,1)), SMALL_ALGORITHM, o) + + i, j = lo, hi = specials2end!(v,a,o) @inbounds while true while i <= j && issignleft(o,v[i]); i += 1; end while i <= j && !issignleft(o,v[j]); j -= 1; end @@ -1202,8 +1533,10 @@ end fpsort!(v::AbstractVector, a::Sort.PartialQuickSort, o::Ordering) = sort!(v, first(axes(v,1)), last(axes(v,1)), a, o) -sort!(v::AbstractVector{<:Floats}, a::Algorithm, o::DirectOrdering) = fpsort!(v,a,o) -sort!(v::Vector{Int}, a::Algorithm, o::Perm{<:DirectOrdering,<:Vector{<:Floats}}) = fpsort!(v,a,o) +sort!(v::FPSortable, a::Algorithm, o::DirectOrdering) = + fpsort!(v, a, o) +sort!(v::AbstractVector{<:Union{Signed, Unsigned}}, a::Algorithm, o::Perm{<:DirectOrdering,<:FPSortable}) = + fpsort!(v, a, o) end # module Sort.Float diff --git a/base/special/cbrt.jl b/base/special/cbrt.jl index 23b518a87a9a7d..9fda5c41fb09e0 100644 --- a/base/special/cbrt.jl +++ b/base/special/cbrt.jl @@ -31,7 +31,6 @@ julia> cbrt(big(-27)) -3.0 ``` """ -cbrt(x::Real) = cbrt(float(x)) cbrt(x::AbstractFloat) = x < 0 ? -(-x)^(1//3) : x^(1//3) """ @@ -147,3 +146,20 @@ function cbrt(x::Union{Float32,Float64}) t = _approx_cbrt(x) return _improve_cbrt(x, t) end + +function cbrt(a::Float16) + if !isfinite(a) || iszero(a) + return a + end + x = Float32(a) + + # 5 bit approximation. Simpler than _approx_cbrt since subnormals can not appear + u = highword(x) & 0x7fff_ffff + v = div(u, UInt32(3)) + 0x2a5119f2 + t = copysign(fromhighword(Float32, v), x) + + # 2 newton iterations + t = 0.33333334f0 * (2f0*t + x/(t*t)) + t = 0.33333334f0 * (2f0*t + x/(t*t)) + return Float16(t) +end diff --git a/base/special/exp.jl b/base/special/exp.jl index 493e5167f1e664..837310bc7ed197 100644 --- a/base/special/exp.jl +++ b/base/special/exp.jl @@ -1,138 +1,499 @@ -# Based on FDLIBM http://www.netlib.org/fdlibm/e_exp.c -# which is made available under the following licence - -## Copyright (C) 2004 by Sun Microsystems, Inc. All rights reserved. Permission -## to use, copy, modify, and distribute this software is freely granted, -## provided that this notice is preserved. - -# Method -# 1. Argument reduction: Reduce x to an r so that |r| <= 0.5*ln(2). Given x, -# find r and integer k such that -# x = k*ln(2) + r, |r| <= 0.5*ln(2). -# Here r is represented as r = hi - lo for better accuracy. -# -# 2. Approximate exp(r) by a special rational function on [0, 0.5*ln(2)]: -# R(r^2) = r*(exp(r)+1)/(exp(r)-1) = 2 + r*r/6 - r^4/360 + ... -# -# A special Remez algorithm on [0, 0.5*ln(2)] is used to generate a -# polynomial to approximate R. +# This file is a part of Julia. License is MIT: https://julialang.org/license + +# magic rounding constant: 1.5*2^52 Adding, then subtracting it from a float rounds it to an Int. +# This works because eps(MAGIC_ROUND_CONST(T)) == one(T), so adding it to a smaller number aligns the lsb to the 1s place. +# Values for which this trick doesn't work are going to have outputs of 0 or Inf. +MAGIC_ROUND_CONST(::Type{Float64}) = 6.755399441055744e15 +MAGIC_ROUND_CONST(::Type{Float32}) = 1.048576f7 + +# max, min, and subnormal arguments +# max_exp = T(exponent_bias(T)*log(base, big(2)) + log(base, 2 - big(2.0)^-significand_bits(T))) +MAX_EXP(n::Val{2}, ::Type{Float32}) = 128.0f0 +MAX_EXP(n::Val{2}, ::Type{Float64}) = 1024.0 +MAX_EXP(n::Val{:ℯ}, ::Type{Float32}) = 88.72284f0 +MAX_EXP(n::Val{:ℯ}, ::Type{Float64}) = 709.7827128933841 +MAX_EXP(n::Val{10}, ::Type{Float32}) = 38.53184f0 +MAX_EXP(n::Val{10}, ::Type{Float64}) = 308.25471555991675 + +# min_exp = T(-(exponent_bias(T)+significand_bits(T)) * log(base, big(2))) +MIN_EXP(n::Val{2}, ::Type{Float32}) = -150.0f0 +MIN_EXP(n::Val{2}, ::Type{Float64}) = -1075.0 +MIN_EXP(n::Val{:ℯ}, ::Type{Float32}) = -103.97208f0 +MIN_EXP(n::Val{:ℯ}, ::Type{Float64}) = -745.1332191019412 +MIN_EXP(n::Val{10}, ::Type{Float32}) = -45.1545f0 +MIN_EXP(n::Val{10}, ::Type{Float64}) = -323.60724533877976 + +# subnorm_exp = abs(log(base, floatmin(T))) +# these vals are positive since it's easier to take abs(x) than -abs(x) +SUBNORM_EXP(n::Val{2}, ::Type{Float32}) = 126.00001f0 +SUBNORM_EXP(n::Val{2}, ::Type{Float64}) = 1022.0 +SUBNORM_EXP(n::Val{:ℯ}, ::Type{Float32}) = 87.33655f0 +SUBNORM_EXP(n::Val{:ℯ}, ::Type{Float64}) = 708.3964185322641 +SUBNORM_EXP(n::Val{10}, ::Type{Float32}) = 37.92978f0 +SUBNORM_EXP(n::Val{10}, ::Type{Float64}) = 307.6526555685887 + +# 256/log(base, 2) (For Float64 reductions) +LogBo256INV(::Val{2}, ::Type{Float64}) = 256.0 +LogBo256INV(::Val{:ℯ}, ::Type{Float64}) = 369.3299304675746 +LogBo256INV(::Val{10}, ::Type{Float64}) = 850.4135922911647 + +# -log(base, 2)/256 in upper and lower bits +# Upper is truncated to only have 34 bits of significand since N has at most +# ceil(log2(-MIN_EXP(base, Float64)*LogBo256INV(Val(2), Float64))) = 19 bits. +# This ensures no rounding when multiplying LogBo256U*N for FMAless hardware +LogBo256U(::Val{2}, ::Type{Float64}) = -0.00390625 +LogBo256U(::Val{:ℯ}, ::Type{Float64}) = -0.002707606173999011 +LogBo256U(::Val{10}, ::Type{Float64}) = -0.0011758984204561784 +LogBo256L(::Val{2}, ::Type{Float64}) = 0.0 +LogBo256L(::Val{:ℯ}, ::Type{Float64}) = -6.327543041662719e-14 +LogBo256L(::Val{10}, ::Type{Float64}) = -1.0624811566412999e-13 + +# 1/log(base, 2) (For Float32 reductions) +LogBINV(::Val{2}, ::Type{Float32}) = 1.0f0 +LogBINV(::Val{:ℯ}, ::Type{Float32}) = 1.442695f0 +LogBINV(::Val{10}, ::Type{Float32}) = 3.321928f0 + +# -log(base, 2) in upper and lower bits +# Upper is truncated to only have 16 bits of significand since N has at most +# ceil(log2(-MIN_EXP(n, Float32)*LogBINV(Val(2), Float32))) = 8 bits. +# This ensures no rounding when multiplying LogBU*N for FMAless hardware +LogBU(::Val{2}, ::Type{Float32}) = -1.0f0 +LogBU(::Val{:ℯ}, ::Type{Float32}) = -0.69314575f0 +LogBU(::Val{10}, ::Type{Float32}) = -0.3010254f0 +LogBL(::Val{2}, ::Type{Float32}) = 0.0f0 +LogBL(::Val{:ℯ}, ::Type{Float32}) = -1.4286068f-6 +LogBL(::Val{10}, ::Type{Float32}) = -4.605039f-6 + +# -log(base, 2) as a Float32 for Float16 version. +LogB(::Val{2}, ::Type{Float16}) = -1.0f0 +LogB(::Val{:ℯ}, ::Type{Float16}) = -0.6931472f0 +LogB(::Val{10}, ::Type{Float16}) = -0.30103f0 + +# Range reduced kernels +@inline function expm1b_kernel(::Val{2}, x::Float64) + return x * evalpoly(x, (0.6931471805599393, 0.24022650695910058, + 0.05550411502333161, 0.009618129548366803)) +end +@inline function expm1b_kernel(::Val{:ℯ}, x::Float64) + return x * evalpoly(x, (0.9999999999999912, 0.4999999999999997, + 0.1666666857598779, 0.04166666857598777)) +end + +@inline function expm1b_kernel(::Val{10}, x::Float64) + return x * evalpoly(x, (2.3025850929940255, 2.6509490552391974, + 2.034678825384765, 1.1712552025835192)) +end + +@inline function expb_kernel(::Val{2}, x::Float32) + return evalpoly(x, (1.0f0, 0.6931472f0, 0.2402265f0, + 0.05550411f0, 0.009618025f0, + 0.0013333423f0, 0.00015469732f0, 1.5316464f-5)) +end +@inline function expb_kernel(::Val{:ℯ}, x::Float32) + return evalpoly(x, (1.0f0, 1.0f0, 0.5f0, 0.16666667f0, + 0.041666217f0, 0.008333249f0, + 0.001394858f0, 0.00019924171f0)) +end +@inline function expb_kernel(::Val{10}, x::Float32) + return evalpoly(x, (1.0f0, 2.3025851f0, 2.650949f0, + 2.0346787f0, 1.1712426f0, 0.53937745f0, + 0.20788547f0, 0.06837386f0)) +end + +# Table stores data with 60 sig figs by using the fact that the first 12 bits of all the +# values would be the same if stored as regular Float64. +# This only gains 8 bits since the least significant 4 bits of the exponent +# of the small part are not the same for all table entries +const JU_MASK = typemax(UInt64)>>12 +const JL_MASK = typemax(UInt64)>>8 +const JU_CONST = 0x3FF0000000000000 +const JL_CONST = 0x3C00000000000000 + + +#function make_table(size) +# t_array = zeros(UInt64, size); +# for j in 1:size +# val = 2.0^(BigFloat(j-1)/size) +# valU = Float64(val, RoundDown) +# valL = Float64(val-valU) +# valU = reinterpret(UInt64, valU) & JU_MASK +# valL = ((reinterpret(UInt64, valL) & JL_MASK)>>44)<<52 +# t_array[j] = valU | valL +# end +# return Tuple(t_array) +#end +#const J_TABLE = make_table(256); +const J_TABLE = (0x0000000000000000, 0xaac00b1afa5abcbe, 0x9b60163da9fb3335, 0xab502168143b0280, 0xadc02c9a3e778060, + 0x656037d42e11bbcc, 0xa7a04315e86e7f84, 0x84c04e5f72f654b1, 0x8d7059b0d3158574, 0xa510650a0e3c1f88, + 0xa8d0706b29ddf6dd, 0x83207bd42b72a836, 0x6180874518759bc8, 0xa4b092bdf66607df, 0x91409e3ecac6f383, + 0x85d0a9c79b1f3919, 0x98a0b5586cf9890f, 0x94f0c0f145e46c85, 0x9010cc922b7247f7, 0xa210d83b23395deb, + 0x4030e3ec32d3d1a2, 0xa5b0efa55fdfa9c4, 0xae40fb66affed31a, 0x8d41073028d7233e, 0xa4911301d0125b50, + 0xa1a11edbab5e2ab5, 0xaf712abdc06c31cb, 0xae8136a814f204aa, 0xa661429aaea92ddf, 0xa9114e95934f312d, + 0x82415a98c8a58e51, 0x58f166a45471c3c2, 0xab9172b83c7d517a, 0x70917ed48695bbc0, 0xa7718af9388c8de9, + 0x94a1972658375d2f, 0x8e51a35beb6fcb75, 0x97b1af99f8138a1c, 0xa351bbe084045cd3, 0x9001c82f95281c6b, + 0x9e01d4873168b9aa, 0xa481e0e75eb44026, 0xa711ed5022fcd91c, 0xa201f9c18438ce4c, 0x8dc2063b88628cd6, + 0x935212be3578a819, 0x82a21f49917ddc96, 0x8d322bdda27912d1, 0x99b2387a6e756238, 0x8ac2451ffb82140a, + 0x8ac251ce4fb2a63f, 0x93e25e85711ece75, 0x82b26b4565e27cdd, 0x9e02780e341ddf29, 0xa2d284dfe1f56380, + 0xab4291ba7591bb6f, 0x86129e9df51fdee1, 0xa352ab8a66d10f12, 0xafb2b87fd0dad98f, 0xa572c57e39771b2e, + 0x9002d285a6e4030b, 0x9d12df961f641589, 0x71c2ecafa93e2f56, 0xaea2f9d24abd886a, 0x86f306fe0a31b715, + 0x89531432edeeb2fd, 0x8a932170fc4cd831, 0xa1d32eb83ba8ea31, 0x93233c08b26416ff, 0xab23496266e3fa2c, + 0xa92356c55f929ff0, 0xa8f36431a2de883a, 0xa4e371a7373aa9ca, 0xa3037f26231e7549, 0xa0b38cae6d05d865, + 0xa3239a401b7140ee, 0xad43a7db34e59ff6, 0x9543b57fbfec6cf4, 0xa083c32dc313a8e4, 0x7fe3d0e544ede173, + 0x8ad3dea64c123422, 0xa943ec70df1c5174, 0xa413fa4504ac801b, 0x8bd40822c367a024, 0xaf04160a21f72e29, + 0xa3d423fb27094689, 0xab8431f5d950a896, 0x88843ffa3f84b9d4, 0x48944e086061892d, 0xae745c2042a7d231, + 0x9c946a41ed1d0057, 0xa1e4786d668b3236, 0x73c486a2b5c13cd0, 0xab1494e1e192aed1, 0x99c4a32af0d7d3de, + 0xabb4b17dea6db7d6, 0x7d44bfdad5362a27, 0x9054ce41b817c114, 0x98e4dcb299fddd0d, 0xa564eb2d81d8abfe, + 0xa5a4f9b2769d2ca6, 0x7a2508417f4531ee, 0xa82516daa2cf6641, 0xac65257de83f4eee, 0xabe5342b569d4f81, + 0x879542e2f4f6ad27, 0xa8a551a4ca5d920e, 0xa7856070dde910d1, 0x99b56f4736b527da, 0xa7a57e27dbe2c4ce, + 0x82958d12d497c7fd, 0xa4059c0827ff07cb, 0x9635ab07dd485429, 0xa245ba11fba87a02, 0x3c45c9268a5946b7, + 0xa195d84590998b92, 0x9ba5e76f15ad2148, 0xa985f6a320dceb70, 0xa60605e1b976dc08, 0x9e46152ae6cdf6f4, + 0xa636247eb03a5584, 0x984633dd1d1929fd, 0xa8e6434634ccc31f, 0xa28652b9febc8fb6, 0xa226623882552224, + 0xa85671c1c70833f5, 0x60368155d44ca973, 0x880690f4b19e9538, 0xa216a09e667f3bcc, 0x7a36b052fa75173e, + 0xada6c012750bdabe, 0x9c76cfdcddd47645, 0xae46dfb23c651a2e, 0xa7a6ef9298593ae4, 0xa9f6ff7df9519483, + 0x59d70f7466f42e87, 0xaba71f75e8ec5f73, 0xa6f72f8286ead089, 0xa7a73f9a48a58173, 0x90474fbd35d7cbfd, + 0xa7e75feb564267c8, 0x9b777024b1ab6e09, 0x986780694fde5d3f, 0x934790b938ac1cf6, 0xaaf7a11473eb0186, + 0xa207b17b0976cfda, 0x9f17c1ed0130c132, 0x91b7d26a62ff86f0, 0x7057e2f336cf4e62, 0xabe7f3878491c490, + 0xa6c80427543e1a11, 0x946814d2add106d9, 0xa1582589994cce12, 0x9998364c1eb941f7, 0xa9c8471a4623c7ac, + 0xaf2857f4179f5b20, 0xa01868d99b4492ec, 0x85d879cad931a436, 0x99988ac7d98a6699, 0x9d589bd0a478580f, + 0x96e8ace5422aa0db, 0x9ec8be05bad61778, 0xade8cf3216b5448b, 0xa478e06a5e0866d8, 0x85c8f1ae99157736, + 0x959902fed0282c8a, 0xa119145b0b91ffc5, 0xab2925c353aa2fe1, 0xae893737b0cdc5e4, 0xa88948b82b5f98e4, + 0xad395a44cbc8520e, 0xaf296bdd9a7670b2, 0xa1797d829fde4e4f, 0x7ca98f33e47a22a2, 0xa749a0f170ca07b9, + 0xa119b2bb4d53fe0c, 0x7c79c49182a3f090, 0xa579d674194bb8d4, 0x7829e86319e32323, 0xaad9fa5e8d07f29d, + 0xa65a0c667b5de564, 0x9c6a1e7aed8eb8bb, 0x963a309bec4a2d33, 0xa2aa42c980460ad7, 0xa16a5503b23e255c, + 0x650a674a8af46052, 0x9bca799e1330b358, 0xa58a8bfe53c12e58, 0x90fa9e6b5579fdbf, 0x889ab0e521356eba, + 0xa81ac36bbfd3f379, 0x97ead5ff3a3c2774, 0x97aae89f995ad3ad, 0xa5aafb4ce622f2fe, 0xa21b0e07298db665, + 0x94db20ce6c9a8952, 0xaedb33a2b84f15fa, 0xac1b468415b749b0, 0xa1cb59728de55939, 0x92ab6c6e29f1c52a, + 0xad5b7f76f2fb5e46, 0xa24b928cf22749e3, 0xa08ba5b030a10649, 0xafcbb8e0b79a6f1e, 0x823bcc1e904bc1d2, + 0xafcbdf69c3f3a206, 0xa08bf2c25bd71e08, 0xa89c06286141b33c, 0x811c199bdd85529c, 0xa48c2d1cd9fa652b, + 0x9b4c40ab5fffd07a, 0x912c544778fafb22, 0x928c67f12e57d14b, 0xa86c7ba88988c932, 0x71ac8f6d9406e7b5, + 0xaa0ca3405751c4da, 0x750cb720dcef9069, 0xac5ccb0f2e6d1674, 0xa88cdf0b555dc3f9, 0xa2fcf3155b5bab73, + 0xa1ad072d4a07897b, 0x955d1b532b08c968, 0xa15d2f87080d89f1, 0x93dd43c8eacaa1d6, 0x82ed5818dcfba487, + 0x5fed6c76e862e6d3, 0xa77d80e316c98397, 0x9a0d955d71ff6075, 0x9c2da9e603db3285, 0xa24dbe7cd63a8314, + 0x92ddd321f301b460, 0xa1ade7d5641c0657, 0xa72dfc97337b9b5e, 0xadae11676b197d16, 0xa42e264614f5a128, + 0xa30e3b333b16ee11, 0x839e502ee78b3ff6, 0xaa7e653924676d75, 0x92de7a51fbc74c83, 0xa77e8f7977cdb73f, + 0xa0bea4afa2a490d9, 0x948eb9f4867cca6e, 0xa1becf482d8e67f0, 0x91cee4aaa2188510, 0x9dcefa1bee615a27, + 0xa66f0f9c1cb64129, 0x93af252b376bba97, 0xacdf3ac948dd7273, 0x99df50765b6e4540, 0x9faf6632798844f8, + 0xa12f7bfdad9cbe13, 0xaeef91d802243c88, 0x874fa7c1819e90d8, 0xacdfbdba3692d513, 0x62efd3c22b8f71f1, 0x74afe9d96b2a23d9) + +@inline function table_unpack(ind) + j = @inbounds J_TABLE[ind] + jU = reinterpret(Float64, JU_CONST | (j&JU_MASK)) + jL = reinterpret(Float64, JL_CONST | (j>>8)) + return jU, jL +end + +# Method for Float64 +# 1. Argument reduction: Reduce x to an r so that |r| <= log(b, 2)/512. Given x, base b, +# find r and integers k, j such that +# x = (k + j/256)*log(b, 2) + r, 0 <= j < 256, |r| <= log(b,2)/512. # -# The computation of exp(r) thus becomes -# 2*r -# exp(r) = 1 + ---------- -# R(r) - r -# r*c(r) -# = 1 + r + ----------- (for better accuracy) -# 2 - c(r) -# where -# c(r) = r - (P1*r^2 + P2*r^4 + ... + P5*r^10 + ...). +# 2. Approximate b^r-1 by 3rd-degree minimax polynomial p_b(r) on the interval [-log(b,2)/512, log(b,2)/512]. +# Since the bounds on r are very tight, this is sufficient to be accurate to floating point epsilon. # -# 3. Scale back: exp(x) = 2^k * exp(r) +# 3. Scale back: b^x = 2^k * 2^(j/256) * (1 + p_b(r)) +# Since the range of possible j is small, 2^(j/256) is stored for all possible values in slightly extended precision. -# log(2) -const LN2 = 6.931471805599453094172321214581765680755001343602552541206800094933936219696955e-01 -# log2(e) -const LOG2_E = 1.442695040888963407359924681001892137426646 - -# log(2) into upper and lower bits -LN2U(::Type{Float64}) = 6.93147180369123816490e-1 -LN2U(::Type{Float32}) = 6.9313812256f-1 +# Method for Float32 +# 1. Argument reduction: Reduce x to an r so that |r| <= log(b, 2)/2. Given x, base b, +# find r and integer N such that +# x = N*log(b, 2) + r, |r| <= log(b,2)/2. +# +# 2. Approximate b^r by 7th-degree minimax polynomial p_b(r) on the interval [-log(b,2)/2, log(b,2)/2]. +# 3. Scale back: b^x = 2^N * p_b(r) +# For both, a little extra care needs to be taken if b^r is subnormal. +# The solution is to do the scaling back in 2 steps as just messing with the exponent wouldn't work. -LN2L(::Type{Float64}) = 1.90821492927058770002e-10 -LN2L(::Type{Float32}) = 9.0580006145f-6 +@inline function exp_impl(x::Float64, base) + T = Float64 + N_float = muladd(x, LogBo256INV(base, T), MAGIC_ROUND_CONST(T)) + N = reinterpret(UInt64, N_float) % Int32 + N_float -= MAGIC_ROUND_CONST(T) #N_float now equals round(x*LogBo256INV(base, T)) + r = muladd(N_float, LogBo256U(base, T), x) + r = muladd(N_float, LogBo256L(base, T), r) + k = N >> 8 + jU, jL = table_unpack(N&255 + 1) + small_part = muladd(jU, expm1b_kernel(base, r), jL) + jU -# max and min arguments -MAX_EXP(::Type{Float64}) = 7.09782712893383996732e2 # log 2^1023*(2-2^-52) -MAX_EXP(::Type{Float32}) = 88.72283905206835f0 # log 2^127 *(2-2^-23) + if !(abs(x) <= SUBNORM_EXP(base, T)) + x >= MAX_EXP(base, T) && return Inf + x <= MIN_EXP(base, T) && return 0.0 + if k <= -53 + # The UInt64 forces promotion. (Only matters for 32 bit systems.) + twopk = (k + UInt64(53)) << 52 + return reinterpret(T, twopk + reinterpret(UInt64, small_part))*(2.0^-53) + end + #k == 1024 && return (small_part * 2.0) * 2.0^1023 + end + twopk = Int64(k) << 52 + return reinterpret(T, twopk + reinterpret(Int64, small_part)) +end +# Computes base^(x+xlo). Used for pow. +@inline function exp_impl(x::Float64, xlo::Float64, base) + T = Float64 + N_float = muladd(x, LogBo256INV(base, T), MAGIC_ROUND_CONST(T)) + N = reinterpret(UInt64, N_float) % Int32 + N_float -= MAGIC_ROUND_CONST(T) #N_float now equals round(x*LogBo256INV(base, T)) + r = muladd(N_float, LogBo256U(base, T), x) + r = muladd(N_float, LogBo256L(base, T), r) + k = N >> 8 + jU, jL = table_unpack(N&255 + 1) + very_small = muladd(jU, expm1b_kernel(base, r), jL) + small_part = muladd(jU,xlo,very_small) + jU + if !(abs(x) <= SUBNORM_EXP(base, T)) + x >= MAX_EXP(base, T) && return Inf + x <= MIN_EXP(base, T) && return 0.0 + if k <= -53 + # The UInt64 forces promotion. (Only matters for 32 bit systems.) + twopk = (k + UInt64(53)) << 52 + return reinterpret(T, twopk + reinterpret(UInt64, small_part))*(2.0^-53) + end + #k == 1024 && return (small_part * 2.0) * 2.0^1023 + end + twopk = Int64(k) << 52 + return reinterpret(T, twopk + reinterpret(Int64, small_part)) +end +@inline function exp_impl_fast(x::Float64, base) + T = Float64 + x >= MAX_EXP(base, T) && return Inf + x <= -SUBNORM_EXP(base, T) && return 0.0 + N_float = muladd(x, LogBo256INV(base, T), MAGIC_ROUND_CONST(T)) + N = reinterpret(UInt64, N_float) % Int32 + N_float -= MAGIC_ROUND_CONST(T) #N_float now equals round(x*LogBo256INV(base, T)) + r = muladd(N_float, LogBo256U(base, T), x) + r = muladd(N_float, LogBo256L(base, T), r) + k = N >> 8 + jU = reinterpret(Float64, JU_CONST | (@inbounds J_TABLE[N&255 + 1] & JU_MASK)) + small_part = muladd(jU, expm1b_kernel(base, r), jU) + twopk = Int64(k) << 52 + return reinterpret(T, twopk + reinterpret(Int64, small_part)) +end -# one less than the min exponent since we can sqeeze a bit more from the exp function -MIN_EXP(::Type{Float64}) = -7.451332191019412076235e2 # log 2^-1075 -MIN_EXP(::Type{Float32}) = -103.97207708f0 # log 2^-150 +@inline function exp_impl(x::Float32, base) + T = Float32 + N_float = round(x*LogBINV(base, T)) + N = unsafe_trunc(Int32, N_float) + r = muladd(N_float, LogBU(base, T), x) + r = muladd(N_float, LogBL(base, T), r) + small_part = expb_kernel(base, r) + power = (N+Int32(127)) + x > MAX_EXP(base, T) && return Inf32 + x < MIN_EXP(base, T) && return 0.0f0 + if x <= -SUBNORM_EXP(base, T) + power += Int32(24) + small_part *= Float32(0x1p-24) + end + if N == 128 + power -= Int32(1) + small_part *= 2f0 + end + return small_part * reinterpret(T, power << Int32(23)) +end -@inline exp_kernel(x::Float64) = @horner(x, 1.66666666666666019037e-1, - -2.77777777770155933842e-3, 6.61375632143793436117e-5, - -1.65339022054652515390e-6, 4.13813679705723846039e-8) +@inline function exp_impl_fast(x::Float32, base) + T = Float32 + x >= MAX_EXP(base, T) && return Inf32 + x <= -SUBNORM_EXP(base, T) && return 0f0 + N_float = round(x*LogBINV(base, T)) + N = unsafe_trunc(Int32, N_float) + r = muladd(N_float, LogBU(base, T), x) + r = muladd(N_float, LogBL(base, T), r) + small_part = expb_kernel(base, r) + twopk = reinterpret(T, (N+Int32(127)) << Int32(23)) + return twopk*small_part +end -@inline exp_kernel(x::Float32) = @horner(x, 1.6666625440f-1, -2.7667332906f-3) +@inline function exp_impl(a::Float16, base) + T = Float32 + x = T(a) + N_float = round(x*LogBINV(base, T)) + N = unsafe_trunc(Int32, N_float) + r = muladd(N_float, LogB(base, Float16), x) + small_part = expb_kernel(base, r) + if !(abs(x) <= 25) + x > 16 && return Inf16 + x < 25 && return zero(Float16) + end + twopk = reinterpret(T, (N+Int32(127)) << Int32(23)) + return Float16(twopk*small_part) +end -# for values smaller than this threshold just use a Taylor expansion -@eval exp_small_thres(::Type{Float64}) = $(2.0^-28) -@eval exp_small_thres(::Type{Float32}) = $(2.0f0^-13) +for (func, fast_func, base) in ((:exp2, :exp2_fast, Val(2)), + (:exp, :exp_fast, Val(:ℯ)), + (:exp10, :exp10_fast, Val(10))) + @eval begin + $func(x::Union{Float16,Float32,Float64}) = exp_impl(x, $base) + $fast_func(x::Union{Float32,Float64}) = exp_impl_fast(x, $base) + end +end -""" +@doc """ exp(x) -Compute the natural base exponential of `x`, in other words ``e^x``. +Compute the natural base exponential of `x`, in other words ``ℯ^x``. + +See also [`exp2`](@ref), [`exp10`](@ref) and [`cis`](@ref). # Examples ```jldoctest julia> exp(1.0) 2.718281828459045 + +julia> exp(im * pi) ≈ cis(pi) +true ``` +""" exp(x::Real) + """ -exp(x::Real) = exp(float(x)) -function exp(x::T) where T<:Union{Float32,Float64} - xa = reinterpret(Unsigned, x) & ~sign_mask(T) - xsb = signbit(x) - - # filter out non-finite arguments - if xa > reinterpret(Unsigned, MAX_EXP(T)) - if xa >= exponent_mask(T) - xa & significand_mask(T) != 0 && return T(NaN) - return xsb ? T(0.0) : T(Inf) # exp(+-Inf) - end - x > MAX_EXP(T) && return T(Inf) - x < MIN_EXP(T) && return T(0.0) + exp2(x) + +Compute the base 2 exponential of `x`, in other words ``2^x``. + +See also [`ldexp`](@ref), [`<<`](@ref). + +# Examples +```jldoctest +julia> exp2(5) +32.0 + +julia> 2^5 +32 + +julia> exp2(63) > typemax(Int) +true +``` +""" +exp2(x) + +""" + exp10(x) + +Compute the base 10 exponential of `x`, in other words ``10^x``. + +# Examples +```jldoctest +julia> exp10(2) +100.0 + +julia> 10^2 +100 +``` +""" +exp10(x) + +# functions with special cases for integer arguments +@inline function exp2(x::Base.BitInteger) + if x > 1023 + Inf64 + elseif x <= -1023 + # if -1073 < x <= -1023 then Result will be a subnormal number + # Hex literal with padding must be used to work on 32bit machine + reinterpret(Float64, 0x0000_0000_0000_0001 << ((x + 1074) % UInt)) + else + # We will cast everything to Int64 to avoid errors in case of Int128 + # If x is a Int128, and is outside the range of Int64, then it is not -1023 MAX_EXP(Float64) && return Inf + x < MIN_EXP(Float64) && return -1.0 end - # This implementation gives 2.7182818284590455 for exp(1.0) when T == - # Float64, which is well within the allowable error; however, - # 2.718281828459045 is closer to the true value so we prefer that answer, - # given that 1.0 is such an important argument value. - if x == T(1.0) && T == Float64 - return 2.718281828459045235360 + + N_float = muladd(x, LogBo256INV(Val(:ℯ), T), MAGIC_ROUND_CONST(T)) + N = reinterpret(UInt64, N_float) % Int32 + N_float -= MAGIC_ROUND_CONST(T) #N_float now equals round(x*LogBo256INV(Val(:ℯ), T)) + r = muladd(N_float, LogBo256U(Val(:ℯ), T), x) + r = muladd(N_float, LogBo256L(Val(:ℯ), T), r) + k = Int64(N >> 8) + jU, jL = table_unpack(N&255 +1) + p = expm1b_kernel(Val(:ℯ), r) + twopk = reinterpret(Float64, (1023+k) << 52) + twopnk = reinterpret(Float64, (1023-k) << 52) + k>=106 && return reinterpret(Float64, (1022+k) << 52)*(jU + muladd(jU, p, jL))*2 + k>=53 && return twopk*(jU + muladd(jU, p, (jL-twopnk))) + k<=-2 && return twopk*(jU + muladd(jU, p, jL))-1 + return twopk*((jU-twopnk) + fma(jU, p, jL)) +end + +function expm1(x::Float32) + x > MAX_EXP(Float32) && return Inf32 + x < MIN_EXP(Float32) && return -1f0 + if -0.2876821f0 <=x <= 0.22314355f0 + return expm1_small(x) end - # compute approximation - if xa > reinterpret(Unsigned, T(0.5)*T(LN2)) # |x| > 0.5 log(2) - # argument reduction - if xa < reinterpret(Unsigned, T(1.5)*T(LN2)) # |x| < 1.5 log(2) - if xsb - k = -1 - hi = x + LN2U(T) - lo = -LN2L(T) - else - k = 1 - hi = x - LN2U(T) - lo = LN2L(T) - end - else - n = round(T(LOG2_E)*x) - k = unsafe_trunc(Int,n) - hi = muladd(n, -LN2U(T), x) - lo = n*LN2L(T) - end - # compute approximation on reduced argument - r = hi - lo - z = r*r - p = r - z*exp_kernel(z) - y = T(1.0) - ((lo - (r*p)/(T(2.0) - p)) - hi) - # scale back - if k > -significand_bits(T) - # multiply by 2.0 first to prevent overflow, which helps extends the range - k == exponent_max(T) && return y * T(2.0) * T(2.0)^(exponent_max(T) - 1) - twopk = reinterpret(T, rem(exponent_bias(T) + k, uinttype(T)) << significand_bits(T)) - return y*twopk - else - # add significand_bits(T) + 1 to lift the range outside the subnormals - twopk = reinterpret(T, rem(exponent_bias(T) + significand_bits(T) + 1 + k, uinttype(T)) << significand_bits(T)) - return y * twopk * T(2.0)^(-significand_bits(T) - 1) - end - elseif xa < reinterpret(Unsigned, exp_small_thres(T)) # |x| < exp_small_thres - # Taylor approximation for small values: exp(x) ≈ 1.0 + x - return T(1.0) + x - else - # primary range with k = 0, so compute approximation directly - z = x*x - p = x - z*exp_kernel(z) - return T(1.0) - ((x*p)/(p - T(2.0)) - x) + x = Float64(x) + N_float = round(x*Ln2INV(Float64)) + N = unsafe_trunc(UInt64, N_float) + r = muladd(N_float, Ln2(Float64), x) + hi = evalpoly(r, (1.0, .5, 0.16666667546642386, 0.041666183019487026, + 0.008332997481506921, 0.0013966479175977883, 0.0002004037059220124)) + small_part = r*hi + twopk = reinterpret(Float64, (N+1023) << 52) + return Float32(muladd(twopk, small_part, twopk-1.0)) +end + +function expm1(x::Float16) + x > MAX_EXP(Float16) && return Inf16 + x < MIN_EXP(Float16) && return Float16(-1.0) + x = Float32(x) + if -0.2876821f0 <=x <= 0.22314355f0 + return Float16(x*evalpoly(x, (1f0, .5f0, 0.16666628f0, 0.04166785f0, 0.008351848f0, 0.0013675707f0))) end + N_float = round(x*Ln2INV(Float32)) + N = unsafe_trunc(UInt32, N_float) + r = muladd(N_float, Ln2(Float32), x) + hi = evalpoly(r, (1f0, .5f0, 0.16666667f0, 0.041665863f0, 0.008333111f0, 0.0013981499f0, 0.00019983904f0)) + small_part = r*hi + twopk = reinterpret(Float32, (N+Int32(127)) << Int32(23)) + return Float16(muladd(twopk, small_part, twopk-1f0)) end + +""" + expm1(x) + +Accurately compute ``e^x-1``. It avoids the loss of precision involved in the direct +evaluation of exp(x)-1 for small values of x. +# Examples +```jldoctest +julia> expm1(1e-16) +1.0e-16 + +julia> exp(1e-16) - 1 +0.0 +``` +""" +expm1(x) diff --git a/base/special/exp10.jl b/base/special/exp10.jl deleted file mode 100644 index c32d0a98702ee0..00000000000000 --- a/base/special/exp10.jl +++ /dev/null @@ -1,139 +0,0 @@ -# This file is a part of Julia. License is MIT: https://julialang.org/license - -# Method -# 1. Argument reduction: Reduce x to an r so that |r| <= 0.5*log10(2). Given x, -# find r and integer k such that -# -# x = k*log10(2) + r, |r| <= 0.5*log10(2). -# -# 2. Approximate exp10(r) by a polynomial on the interval [-0.5*log10(2), 0.5*log10(2)]: -# -# exp10(x) = 1.0 + polynomial(x), -# -# sup norm relative error within the interval of the polynomial approximations: -# Float64 : [2.7245504724394698952e-18; 2.7245529895753476720e-18] -# Float32 : [9.6026471477842205871e-10; 9.6026560194009888672e-10] -# -# 3. Scale back: exp10(x) = 2^k * exp10(r) - -# log2(10) -const LOG2_10 = 3.321928094887362347870319429489390175864831393024580612054756395815934776608624 -# log10(2) -const LOG10_2 = 3.010299956639811952137388947244930267681898814621085413104274611271081892744238e-01 -# log(10) -const LN10 = 2.302585092994045684017991454684364207601101488628772976033327900967572609677367 - -# log10(2) into upper and lower bits -LOG10_2U(::Type{Float64}) = 3.01025390625000000000e-1 -LOG10_2U(::Type{Float32}) = 3.00781250000000000000f-1 - -LOG10_2L(::Type{Float64}) = 4.60503898119521373889e-6 -LOG10_2L(::Type{Float32}) = 2.48745663981195213739f-4 - -# max and min arguments -MAX_EXP10(::Type{Float64}) = 3.08254715559916743851e2 # log 2^1023*(2-2^-52) -MAX_EXP10(::Type{Float32}) = 38.531839419103626f0 # log 2^127 *(2-2^-23) - -# one less than the min exponent since we can sqeeze a bit more from the exp10 function -MIN_EXP10(::Type{Float64}) = -3.23607245338779784854769e2 # log10 2^-1075 -MIN_EXP10(::Type{Float32}) = -45.15449934959718f0 # log10 2^-150 - -@inline exp10_kernel(x::Float64) = - @horner(x, 1.0, - 2.30258509299404590109361379290930926799774169921875, - 2.6509490552391992146397114993305876851081848144531, - 2.03467859229323178027470930828712880611419677734375, - 1.17125514891212478829629617393948137760162353515625, - 0.53938292928868392106522833273629657924175262451172, - 0.20699584873167015119932443667494226247072219848633, - 6.8089348259156870502017966373387025669217109680176e-2, - 1.9597690535095281527677713029333972372114658355713e-2, - 5.015553121397981796436571499953060992993414402008e-3, - 1.15474960721768829356725927226534622604958713054657e-3, - 1.55440426715227567738830671828509366605430841445923e-4, - 3.8731032432074128681303432086835414338565897196531e-5, - 2.3804466459036747669197886523306806338950991630554e-3, - 9.3881392238209649520573607528461934634833596646786e-5, - -2.64330486232183387018679354696359951049089431762695e-2) - -@inline exp10_kernel(x::Float32) = - @horner(x, 1.0f0, - 2.302585124969482421875f0, - 2.650949001312255859375f0, - 2.0346698760986328125f0, - 1.17125606536865234375f0, - 0.5400512218475341796875f0, - 0.20749187469482421875f0, - 5.2789829671382904052734375f-2) - -@eval exp10_small_thres(::Type{Float64}) = $(2.0^-29) -@eval exp10_small_thres(::Type{Float32}) = $(2.0f0^-14) - -""" - exp10(x) - -Compute ``10^x``. - -# Examples -```jldoctest -julia> exp10(2) -100.0 - -julia> exp10(0.2) -1.5848931924611136 -``` -""" -exp10(x::Real) = exp10(float(x)) -function exp10(x::T) where T<:Union{Float32,Float64} - xa = reinterpret(Unsigned, x) & ~sign_mask(T) - xsb = signbit(x) - - # filter out non-finite arguments - if xa > reinterpret(Unsigned, MAX_EXP10(T)) - if xa >= exponent_mask(T) - xa & significand_mask(T) != 0 && return T(NaN) - return xsb ? T(0.0) : T(Inf) # exp10(+-Inf) - end - x > MAX_EXP10(T) && return T(Inf) - x < MIN_EXP10(T) && return T(0.0) - end - # compute approximation - if xa > reinterpret(Unsigned, T(0.5)*T(LOG10_2)) # |x| > 0.5 log10(2). - # argument reduction - if xa < reinterpret(Unsigned, T(1.5)*T(LOG10_2)) # |x| <= 1.5 log10(2) - if xsb - k = -1 - r = LOG10_2U(T) + x - r = LOG10_2L(T) + r - else - k = 1 - r = x - LOG10_2U(T) - r = r - LOG10_2L(T) - end - else - n = round(T(LOG2_10)*x) - k = unsafe_trunc(Int,n) - r = muladd(n, -LOG10_2U(T), x) - r = muladd(n, -LOG10_2L(T), r) - end - # compute approximation on reduced argument - y = exp10_kernel(r) - # scale back - if k > -significand_bits(T) - # multiply by 2.0 first to prevent overflow, extending the range - k == exponent_max(T) && return y * T(2.0) * T(2.0)^(exponent_max(T) - 1) - twopk = reinterpret(T, rem(exponent_bias(T) + k, uinttype(T)) << significand_bits(T)) - return y*twopk - else - # add significand_bits(T) + 1 to lift the range outside the subnormals - twopk = reinterpret(T, rem(exponent_bias(T) + significand_bits(T) + 1 + k, uinttype(T)) << significand_bits(T)) - return y * twopk * T(2.0)^(-significand_bits(T) - 1) - end - elseif xa < reinterpret(Unsigned, exp10_small_thres(T)) # |x| < exp10_small_thres - # Taylor approximation for small values: exp10(x) ≈ 1.0 + log(10)*x - return muladd(x, T(LN10), T(1.0)) - else - # primary range with k = 0, so compute approximation directly - return exp10_kernel(x) - end -end diff --git a/base/special/hyperbolic.jl b/base/special/hyperbolic.jl index 4b0e994b7e610a..74f750064c7c25 100644 --- a/base/special/hyperbolic.jl +++ b/base/special/hyperbolic.jl @@ -1,6 +1,6 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -# sinh, cosh, tanh, asinh, acosh, and atanh are heavily based on FDLIBM code: +# asinh, acosh, and atanh are heavily based on FDLIBM code: # e_sinh.c, e_sinhf, e_cosh.c, e_coshf, s_tanh.c, s_tanhf.c, s_asinh.c, # s_asinhf.c, e_acosh.c, e_coshf.c, e_atanh.c, and e_atanhf.c # that are made available under the following licence: @@ -14,6 +14,7 @@ # is preserved. # ==================================================== + # Hyperbolic functions # sinh methods H_SMALL_X(::Type{Float64}) = 2.0^-28 @@ -23,159 +24,139 @@ H_SMALL_X(::Type{Float32}) = 2f-12 H_MEDIUM_X(::Type{Float32}) = 9f0 H_LARGE_X(::Type{Float64}) = 709.7822265633563 # nextfloat(709.7822265633562) -H_OVERFLOW_X(::Type{Float64}) = 710.475860073944 # nextfloat(710.4758600739439) H_LARGE_X(::Type{Float32}) = 88.72283f0 -H_OVERFLOW_X(::Type{Float32}) = 89.415985f0 -function sinh(x::T) where T <: Union{Float32, Float64} + +SINH_SMALL_X(::Type{Float64}) = 2.1 +SINH_SMALL_X(::Type{Float32}) = 3.0f0 + +# For Float64, use DoubleFloat scheme for extra accuracy +function sinh_kernel(x::Float64) + x2, x2lo = two_mul(x,x) + hi_order = evalpoly(x2, (8.333333333336817e-3, 1.9841269840165435e-4, + 2.7557319381151335e-6, 2.5052096530035283e-8, + 1.6059550718903307e-10, 7.634842144412119e-13, + 2.9696954760355812e-15)) + hi,lo = exthorner(x2, (1.0, 0.16666666666666635, hi_order)) + return muladd(x, hi, muladd(x, lo, x*x2lo*0.16666666666666635)) +end +# For Float32, using Float64 is simpler, faster, and doesn't require FMA +function sinh_kernel(x::Float32) + x=Float64(x) + res = evalpoly(x*x, (1.0, 0.1666666779967941, 0.008333336726447933, + 0.00019841001151414065, 2.7555538207080807e-6, + 2.5143389765825282e-8, 1.6260094552031644e-10)) + return Float32(res*x) +end + +@inline function sinh16_kernel(x::Float32) + res = evalpoly(x*x, (1.0f0, 0.16666667f0, 0.008333337f0, 0.00019841001f0, + 2.7555539f-6, 2.514339f-8, 1.6260095f-10)) + return Float16(res*x) +end + +function sinh(x::T) where T<:Union{Float32,Float64} # Method # mathematically sinh(x) is defined to be (exp(x)-exp(-x))/2 - # 1. Replace x by |x| (sinh(-x) = -sinh(x)). + # 1. Sometimes replace x by |x| (sinh(-x) = -sinh(x)). # 2. Find the branch and the expression to calculate and return it - # a) 0 <= x < H_SMALL_X - # return x - # b) H_SMALL_X <= x < H_MEDIUM_X - # return sinh(x) = (E + E/(E+1))/2, where E=expm1(x) - # c) H_MEDIUM_X <= x < H_LARGE_X - # return sinh(x) = exp(x)/2 - # d) H_LARGE_X <= x < H_OVERFLOW_X + # a) 0 <= x < SINH_SMALL_X + # approximate sinh(x) with a minimax polynomial + # b) SINH_SMALL_X <= x < H_LARGE_X + # return sinh(x) = (exp(x) - exp(-x))/2 + # d) H_LARGE_X <= x # return sinh(x) = exp(x/2)/2 * exp(x/2) - # e) H_OVERFLOW_X <= x - # return sinh(x) = T(Inf) - # - # Notes: - # only sinh(0) = 0 is exact for finite x. + # Note that this branch automatically deals with Infs and NaNs - isnan(x) && return x + absx = abs(x) + if absx <= SINH_SMALL_X(T) + return sinh_kernel(x) + elseif absx >= H_LARGE_X(T) + E = exp(T(.5)*absx) + return copysign(T(.5)*E*E, x) + end + E = exp(absx) + return copysign(T(.5)*(E - 1/E),x) +end +function Base.sinh(a::Float16) + x = Float32(a) absx = abs(x) + absx <= SINH_SMALL_X(Float32) && return sinh16_kernel(x) + E = exp(absx) + return Float16(copysign(.5f0*(E - 1/E),x)) +end - h = T(0.5) - if x < 0 - h = -h - end - # in a) or b) - if absx < H_MEDIUM_X(T) - # in a) - if absx < H_SMALL_X(T) - return x - end - t = expm1(absx) - if absx < T(1) - return h*(T(2)*t - t*t/(t + T(1))) - end - return h*(t + t/(t + T(1))) - end - # in c) - if absx < H_LARGE_X(T) - return h*exp(absx) - end - # in d) - if absx < H_OVERFLOW_X(T) - return h*T(2)*_ldexp_exp(absx, Int32(-1)) - end - # in e) - return copysign(T(Inf), x) +COSH_SMALL_X(::Type{T}) where T= one(T) + +function cosh_kernel(x2::Float32) + return evalpoly(x2, (1.0f0, 0.49999997f0, 0.041666888f0, 0.0013882756f0, 2.549933f-5)) +end + +function cosh_kernel(x2::Float64) + return evalpoly(x2, (1.0, 0.5000000000000002, 0.04166666666666269, + 1.3888888889206764e-3, 2.4801587176784207e-5, + 2.7557345825742837e-7, 2.0873617441235094e-9, + 1.1663435515945578e-11)) end -sinh(x::Real) = sinh(float(x)) -# cosh methods -COSH_SMALL_X(::Type{Float32}) = 0.00024414062f0 -COSH_SMALL_X(::Type{Float64}) = 2.7755602085408512e-17 -function cosh(x::T) where T <: Union{Float32, Float64} +function cosh(x::T) where T<:Union{Float32,Float64} # Method # mathematically cosh(x) is defined to be (exp(x)+exp(-x))/2 # 1. Replace x by |x| (cosh(x) = cosh(-x)). # 2. Find the branch and the expression to calculate and return it # a) x <= COSH_SMALL_X - # return T(1) - # b) COSH_SMALL_X <= x <= ln2/2 - # return 1+expm1(|x|)^2/(2*exp(|x|)) - # c) ln2/2 <= x <= H_MEDIUM_X - # return (exp(|x|)+1/exp(|x|)/2 - # d) H_MEDIUM_X <= x < H_LARGE_X - # return cosh(x) = exp(x)/2 - # e) H_LARGE_X <= x < H_OVERFLOW_X + # approximate sinh(x) with a minimax polynomial + # b) COSH_SMALL_X <= x < H_LARGE_X + # return cosh(x) = = (exp(x) + exp(-x))/2 + # e) H_LARGE_X <= x # return cosh(x) = exp(x/2)/2 * exp(x/2) - # f) H_OVERFLOW_X <= x - # return cosh(x) = T(Inf) - - isnan(x) && return x + # Note that this branch automatically deals with Infs and NaNs absx = abs(x) - h = T(0.5) - # in a) or b) - if absx < log(T(2))/2 - # in a) - if absx < COSH_SMALL_X(T) - return T(1) - end - t = expm1(absx) - w = T(1) + t - return T(1) + (t*t)/(w + w) - end - # in c) - if absx < H_MEDIUM_X(T) - t = exp(absx) - return h*t + h/t - end - # in d) - if absx < H_LARGE_X(T) - return h*exp(absx) + if absx <= COSH_SMALL_X(T) + return cosh_kernel(x*x) + elseif absx >= H_LARGE_X(T) + E = exp(T(.5)*absx) + return T(.5)*E*E end - # in e) - if absx < H_OVERFLOW_X(T) - return _ldexp_exp(absx, Int32(-1)) - end - # in f) - return T(Inf) + E = exp(absx) + return T(.5)*(E + 1/E) end -cosh(x::Real) = cosh(float(x)) # tanh methods -TANH_LARGE_X(::Type{Float64}) = 22.0 -TANH_LARGE_X(::Type{Float32}) = 9.0f0 +TANH_LARGE_X(::Type{Float64}) = 44.0 +TANH_LARGE_X(::Type{Float32}) = 18.0f0 +TANH_SMALL_X(::Type{Float64}) = 1.0 +TANH_SMALL_X(::Type{Float32}) = 1.3862944f0 #2*log(2) +@inline function tanh_kernel(x::Float64) + return evalpoly(x, (1.0, -0.33333333333332904, 0.13333333333267555, + -0.05396825393066753, 0.02186948742242217, + -0.008863215974794633, 0.003591910693118715, + -0.0014542587440487815, 0.0005825521659411748, + -0.00021647574085351332, 5.5752458452673005e-5)) +end +@inline function tanh_kernel(x::Float32) + return evalpoly(x, (1.0f0, -0.3333312f0, 0.13328037f0, + -0.05350336f0, 0.019975215f0, -0.0050525228f0)) +end function tanh(x::T) where T<:Union{Float32, Float64} # Method # mathematically tanh(x) is defined to be (exp(x)-exp(-x))/(exp(x)+exp(-x)) # 1. reduce x to non-negative by tanh(-x) = -tanh(x). # 2. Find the branch and the expression to calculate and return it # a) 0 <= x < H_SMALL_X - # return x - # b) H_SMALL_X <= x < 1 - # -expm1(-2x)/(expm1(-2x) + 2) - # c) 1 <= x < TANH_LARGE_X - # 1 - 2/(expm1(2x) + 2) - # d) TANH_LARGE_X <= x + # Use a minimax polynomial over the range + # b) H_SMALL_X <= x < TANH_LARGE_X + # 1 - 2/(exp(2x) + 1) + # c) TANH_LARGE_X <= x # return 1 - if isnan(x) - return x - elseif isinf(x) - return copysign(T(1), x) - end - - absx = abs(x) - if absx < TANH_LARGE_X(T) - # in a) - if absx < H_SMALL_X(T) - return x - end - if absx >= T(1) - # in c) - t = expm1(T(2)*absx) - z = T(1) - T(2)/(t + T(2)) - else - # in b) - t = expm1(-T(2)*absx) - z = -t/(t + T(2)) - end - else - # in d) - z = T(1) - end - return copysign(z, x) + abs2x = abs(2x) + abs2x >= TANH_LARGE_X(T) && return copysign(one(T), x) + abs2x <= TANH_SMALL_X(T) && return x*tanh_kernel(x*x) + k = exp(abs2x) + return copysign(1 - 2/(k+1), x) end -tanh(x::Real) = tanh(float(x)) # Inverse hyperbolic functions AH_LN2(::Type{Float64}) = 6.93147180559945286227e-01 @@ -216,7 +197,6 @@ function asinh(x::T) where T <: Union{Float32, Float64} end return copysign(w, x) end -asinh(x::Real) = asinh(float(x)) # acosh methods @noinline acosh_domain_error(x) = throw(DomainError(x, "acosh(x) is only defined for x ≥ 1.")) @@ -255,7 +235,6 @@ function acosh(x::T) where T <: Union{Float32, Float64} return log(x) + AH_LN2(T) end end -acosh(x::Real) = acosh(float(x)) # atanh methods @noinline atanh_domain_error(x) = throw(DomainError(x, "atanh(x) is only defined for |x| ≤ 1.")) @@ -263,14 +242,10 @@ function atanh(x::T) where T <: Union{Float32, Float64} # Method # 1.Reduced x to positive by atanh(-x) = -atanh(x) # 2. Find the branch and the expression to calculate and return it - # a) 0 <= x < 2^-28 - # return x - # b) 2^-28 <= x < 0.5 - # return 0.5*log1p(2x+2x*x/(1-x)) - # c) 0.5 <= x < 1 - # return 0.5*log1p(2x/1-x) - # d) x = 1 - # return Inf + # a) 0 <= x < 0.5 + # return 0.5*log1p(2x/(1-x)) + # b) 0.5 <= x <= 1 + # return 0.5*log((x+1)/(1-x)) # Special cases: # if |x| > 1 throw DomainError isnan(x) && return x @@ -280,21 +255,12 @@ function atanh(x::T) where T <: Union{Float32, Float64} if absx > 1 atanh_domain_error(x) end - if absx < T(2)^-28 - # in a) - return x - end if absx < T(0.5) + # in a) + t = log1p(T(2)*absx/(T(1)-absx)) + else # in b) - t = absx+absx - t = T(0.5)*log1p(t+t*absx/(T(1)-absx)) - elseif absx < T(1) - # in c) - t = T(0.5)*log1p((absx + absx)/(T(1)-absx)) - elseif absx == T(1) - # in d) - return copysign(T(Inf), x) + t = log((T(1)+absx)/(T(1)-absx)) end - return copysign(t, x) + return T(0.5)*copysign(t, x) end -atanh(x::Real) = atanh(float(x)) diff --git a/base/special/ldexp_exp.jl b/base/special/ldexp_exp.jl deleted file mode 100644 index 3ea0f39373ecec..00000000000000 --- a/base/special/ldexp_exp.jl +++ /dev/null @@ -1,105 +0,0 @@ -# This file is a part of Julia. License is MIT: https://julialang.org/license - -# This code is a Julia translation of the C code from Openlibm (http://www.openlibm.org/) -# with the following license: - -# Copyright (c) 2011 David Schultz -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. - -modify_highword(x::Float32, hw) = reinterpret(Float32, hw) -modify_highword(x::Float64, hw) = reinterpret(Float64, (UInt64(hw)<<32)|(reinterpret(UInt64, x)<<32)>>32) - -exponent_rshift(T::Type{Float32}, hw) = hw >> 23 # this comes from 32 (bits in UInt32) minus 9 bits for the sign and exponent -exponent_rshift(T::Type{Float64}, hw) = hw >> 20 # this comes from 32 (bits in UInt32) minus 12 bits for the sign and exponent -exponent_lshift(T::Type{Float32}, hw) = hw << 23 # this comes from 32 (bits in UInt32) minus 9 bits for the sign and exponent -exponent_lshift(T::Type{Float64}, hw) = hw << 20 # this comes from 32 (bits in UInt32) minus 12 bits for the sign and exponent - -function modify_exponent(x::T, expnt_x) where T <: Union{Float32, Float64} - # mask away exponent; "100...0111..111" with 9 or 12 leading 0's - high_mask = T == Float32 ? 0x807fffff : 0x800fffff # don't mask away the sign - # use mask to replace with first 9 or 12 bits with expnt_x << appropriately - modify_highword(x, (highword(x) & high_mask) | exponent_lshift(T, expnt_x)) -end - -""" - _ldexp_exp(x, l2) -Returns exp(x) * 2^l2. The function is intended for large arguments, x, where -x >= ln(prevfloat(typemax(x)) and care is needed to avoid overflow. - -The present implementation is narrowly tailored for our hyperbolic and -exponential functions. We assume l2 is small (0 or -1), and the caller -has filtered out very large x, for which overflow would be inevitable. -""" -function _ldexp_exp(x::T, l2) where T <: Union{Float32, Float64} - # This function is intended for use in our hyperbolic and exponential functions. - - # Calculate exp(x) = (exp(x-kr*log(2))*2^ks*)2^k2 = exp_x*2^k2 - exp_x, k2 = _frexp_exp(x) - - # Add the two exponents together to form (2^l2)*(2^k2) = 2^(l2+k2) = 2^L - l2 += k2 - L_as_hw = exponent_lshift(T, UInt32(exponent_bias(T) + l2)) - # Form 2^L - scale = fromhighword(T, L_as_hw) - # Return exp(x)*2^l2 - return exp_x * scale -end - -""" - exp_x, k2 = _frexp_exp(x) - -Calculate exp(x) as exp_x*2^k2 and return exp_x = exp(x-kr*log(w))*2^ks where kr -is a type dependant range reduction constant, ks scales exp_x towards the largest -finite number, and k2 is used to absorb the remaning scale to allow for exp(x) -to be outside the normal floating point range. - -This function is intended for use in our hyperbolic and exponential functions. -""" -function _frexp_exp(x::T) where T<:Union{Float32, Float64} - # and should only be used for values in the range (let T = typeof(x)): - # - # log(prevfloat(typemax(x))) <= x < log(2 * prevfloat(typemax(x) / nextfloat(T(0))) - # - # where the upper bound is around 192.7f0 and ~= 1454.91. The function outputs - # exp_x in the ranges - # [2f0^127, 2f0^128) and - # [2.0^1023, 2.0^1024) - # respectively. - - # We use exp(x) = exp(x - kln2) * 2**k, carefully chosen to - # minimize |exp(kln2) - 2**k|. - kr = T == Float32 ? UInt32(235) : UInt32(1799) - - # We also scale the exponent of exp_x to exponent_bias + the largest finite - # exponent (exponent of T(Inf)-1, so that the result can be multiplied by - # a tiny number without losing accuracy due to denormalization. - exp_x = exp(x - kr*log(T(2))) # exp_x*2^k = exp(x) - - # Calculate the ks in exp_x*2^ks - ks = exponent_rshift(T, highword(exp_x)) - (exponent_bias(T) + (exponent_max(T) - 1)) + kr - - # Rescale exp_x to have exponent k2 = exponent_max(T) - 1 - exp_x = modify_exponent(exp_x, UInt32(exponent_bias(T) + (exponent_max(T) - 1))) - return exp_x, ks -end diff --git a/base/special/log.jl b/base/special/log.jl index ef578edb56795e..440a32f8da0f02 100644 --- a/base/special/log.jl +++ b/base/special/log.jl @@ -139,19 +139,24 @@ const t_log_Float32 = (0.0,0.007782140442054949,0.015504186535965254,0.023167059 0.6773988235918061,0.6813592248079031,0.6853040030989194,0.689233281238809, 0.6931471805599453) -# determine if hardware FMA is available -# should probably check with LLVM, see #9855. -const FMA_NATIVE = muladd(nextfloat(1.0),nextfloat(1.0),-nextfloat(1.0,2)) != 0 - # truncate lower order bits (up to 26) # ideally, this should be able to use ANDPD instructions, see #9868. @inline function truncbits(x::Float64) reinterpret(Float64, reinterpret(UInt64,x) & 0xffff_ffff_f800_0000) end +logb(::Type{Float32},::Val{2}) = 1.4426950408889634 +logb(::Type{Float32},::Val{:ℯ}) = 1.0 +logb(::Type{Float32},::Val{10}) = 0.4342944819032518 +logbU(::Type{Float64},::Val{2}) = 1.4426950408889634 +logbL(::Type{Float64},::Val{2}) = 2.0355273740931033e-17 +logbU(::Type{Float64},::Val{:ℯ}) = 1.0 +logbL(::Type{Float64},::Val{:ℯ}) = 0.0 +logbU(::Type{Float64},::Val{10}) = 0.4342944819032518 +logbL(::Type{Float64},::Val{10}) = 1.098319650216765e-17 # Procedure 1 -@inline function log_proc1(y::Float64,mf::Float64,F::Float64,f::Float64,jp::Int) +@inline function log_proc1(y::Float64,mf::Float64,F::Float64,f::Float64,jp::Int,base=Val(:ℯ)) ## Steps 1 and 2 @inbounds hi,lo = t_log_Float64[jp] l_hi = mf* 0.6931471805601177 + hi @@ -175,11 +180,13 @@ end 0.012500053168098584) ## Step 4 - l_hi + (u + (q + l_lo)) + m_hi = logbU(Float64, base) + m_lo = logbL(Float64, base) + return fma(m_hi, l_hi, fma(m_hi, (u + (q + l_lo)), m_lo*l_hi)) end # Procedure 2 -@inline function log_proc2(f::Float64) +@inline function log_proc2(f::Float64,base=Val(:ℯ)) ## Step 1 g = 1.0/(2.0+f) u = 2.0*f*g @@ -198,20 +205,14 @@ end # 2(f-u1-u2) - f*(u1+u2) = 0 # 2(f-u1) - f*u1 = (2+f)u2 # u2 = (2(f-u1) - f*u1)/(2+f) - if FMA_NATIVE - return u + fma(fma(-u,f,2(f-u)), g, q) - else - u1 = truncbits(u) # round to 24 bits - f1 = truncbits(f) - f2 = f-f1 - u2 = ((2.0*(f-u1)-u1*f1)-u1*f2)*g - ## Step 4 - return u1 + (u2 + q) - end + + m_hi = logbU(Float64, base) + m_lo = logbL(Float64, base) + return fma(m_hi, u, fma(m_lo, u, m_hi*fma(fma(-u,f,2(f-u)), g, q))) end -@inline function log_proc1(y::Float32,mf::Float32,F::Float32,f::Float32,jp::Int) +@inline function log_proc1(y::Float32,mf::Float32,F::Float32,f::Float32,jp::Int,base=Val(:ℯ)) ## Steps 1 and 2 @inbounds hi = t_log_Float32[jp] l = mf*0.6931471805599453 + hi @@ -228,10 +229,10 @@ end q = u*v*0.08333351f0 ## Step 4 - Float32(l + (u + q)) + Float32(logb(Float32, base)*(l + (u + q))) end -@inline function log_proc2(f::Float32) +@inline function log_proc2(f::Float32,base=Val(:ℯ)) ## Step 1 # compute in higher precision u64 = Float64(2f0*f)/(2.0+f) @@ -246,18 +247,24 @@ end ## Step 3: not required ## Step 4 - Float32(u64 + q) + Float32(logb(Float32, base)*(u64 + q)) end +log2(x::Float32) = _log(x, Val(2), :log2) +log(x::Float32) = _log(x, Val(:ℯ), :log) +log10(x::Float32) = _log(x, Val(10), :log10) +log2(x::Float64) = _log(x, Val(2), :log2) +log(x::Float64) = _log(x, Val(:ℯ), :log) +log10(x::Float64) = _log(x, Val(10), :log10) -function log(x::Float64) +function _log(x::Float64, base, func) if x > 0.0 x == Inf && return x # Step 2 if 0.9394130628134757 < x < 1.0644944589178595 f = x-1.0 - return log_proc2(f) + return log_proc2(f, base) end # Step 3 @@ -276,24 +283,24 @@ function log(x::Float64) f = y-F jp = unsafe_trunc(Int,128.0*F)-127 - return log_proc1(y,mf,F,f,jp) + return log_proc1(y,mf,F,f,jp,base) elseif x == 0.0 -Inf elseif isnan(x) NaN else - throw_complex_domainerror(:log, x) + throw_complex_domainerror(func, x) end end -function log(x::Float32) +function _log(x::Float32, base, func) if x > 0f0 x == Inf32 && return x # Step 2 if 0.939413f0 < x < 1.0644945f0 f = x-1f0 - return log_proc2(f) + return log_proc2(f, base) end # Step 3 @@ -312,13 +319,13 @@ function log(x::Float32) f = y-F jp = unsafe_trunc(Int,128.0f0*F)-127 - log_proc1(y,mf,F,f,jp) + log_proc1(y,mf,F,f,jp,base) elseif x == 0f0 -Inf32 elseif isnan(x) NaN32 else - throw_complex_domainerror(:log, x) + throw_complex_domainerror(func, x) end end @@ -390,8 +397,190 @@ function log1p(x::Float32) end end -for f in (:log,:log1p) - @eval begin - ($f)(x::Real) = ($f)(float(x)) - end +#function make_compact_table(N) +# table = Tuple{UInt64,Float64}[] +# lo, hi = 0x1.69555p-1, 0x1.69555p0 +# for i in 0:N-1 +# # I am not fully sure why this is the right formula to use, but it apparently is +# center = i/(2*N) + lo < 1 ? (i+.5)/(2*N) + lo : (i+.5)/N + hi -1 +# invc = Float64(center < 1 ? round(N/center)/N : round(2*N/center)/(N*2)) +# c = inv(big(invc)) +# logc = Float64(round(0x1p43*log(c))/0x1p43) +# logctail = reinterpret(Float64, Float64(log(c) - logc)) +# p1 = (reinterpret(UInt64,invc) >> 45) % UInt8 +# push!(table, (p1|reinterpret(UInt64,logc),logctail)) +# end +# return Tuple(table) +#end +#const t_log_table_compat = make_compact_table(128) +const t_log_table_compat = ( + (0xbfd62c82f2b9c8b5, 5.929407345889625e-15), + (0xbfd5d1bdbf5808b4, -2.544157440035963e-14), + (0xbfd57677174558b3, -3.443525940775045e-14), + (0xbfd51aad872df8b2, -2.500123826022799e-15), + (0xbfd4be5f957778b1, -8.929337133850617e-15), + (0xbfd4618bc21c60b0, 1.7625431312172662e-14), + (0xbfd404308686a8af, 1.5688303180062087e-15), + (0xbfd3a64c556948ae, 2.9655274673691784e-14), + (0xbfd347dd9a9880ad, 3.7923164802093147e-14), + (0xbfd2e8e2bae120ac, 3.993416384387844e-14), + (0xbfd2895a13de88ab, 1.9352855826489123e-14), + (0xbfd2895a13de88ab, 1.9352855826489123e-14), + (0xbfd22941fbcf78aa, -1.9852665484979036e-14), + (0xbfd1c898c16998a9, -2.814323765595281e-14), + (0xbfd1675cababa8a8, 2.7643769993528702e-14), + (0xbfd1058bf9ae48a7, -4.025092402293806e-14), + (0xbfd0a324e27390a6, -1.2621729398885316e-14), + (0xbfd0402594b4d0a5, -3.600176732637335e-15), + (0xbfd0402594b4d0a5, -3.600176732637335e-15), + (0xbfcfb9186d5e40a4, 1.3029797173308663e-14), + (0xbfcef0adcbdc60a3, 4.8230289429940886e-14), + (0xbfce27076e2af0a2, -2.0592242769647135e-14), + (0xbfcd5c216b4fc0a1, 3.149265065191484e-14), + (0xbfcc8ff7c79aa0a0, 4.169796584527195e-14), + (0xbfcc8ff7c79aa0a0, 4.169796584527195e-14), + (0xbfcbc286742d909f, 2.2477465222466186e-14), + (0xbfcaf3c94e80c09e, 3.6507188831790577e-16), + (0xbfca23bc1fe2b09d, -3.827767260205414e-14), + (0xbfca23bc1fe2b09d, -3.827767260205414e-14), + (0xbfc9525a9cf4509c, -4.7641388950792196e-14), + (0xbfc87fa06520d09b, 4.9278276214647115e-14), + (0xbfc7ab890210e09a, 4.9485167661250996e-14), + (0xbfc7ab890210e09a, 4.9485167661250996e-14), + (0xbfc6d60fe719d099, -1.5003333854266542e-14), + (0xbfc5ff3070a79098, -2.7194441649495324e-14), + (0xbfc5ff3070a79098, -2.7194441649495324e-14), + (0xbfc526e5e3a1b097, -2.99659267292569e-14), + (0xbfc44d2b6ccb8096, 2.0472357800461955e-14), + (0xbfc44d2b6ccb8096, 2.0472357800461955e-14), + (0xbfc371fc201e9095, 3.879296723063646e-15), + (0xbfc29552f81ff094, -3.6506824353335045e-14), + (0xbfc1b72ad52f6093, -5.4183331379008994e-14), + (0xbfc1b72ad52f6093, -5.4183331379008994e-14), + (0xbfc0d77e7cd09092, 1.1729485484531301e-14), + (0xbfc0d77e7cd09092, 1.1729485484531301e-14), + (0xbfbfec9131dbe091, -3.811763084710266e-14), + (0xbfbe27076e2b0090, 4.654729747598445e-14), + (0xbfbe27076e2b0090, 4.654729747598445e-14), + (0xbfbc5e548f5bc08f, -2.5799991283069902e-14), + (0xbfba926d3a4ae08e, 3.7700471749674615e-14), + (0xbfba926d3a4ae08e, 3.7700471749674615e-14), + (0xbfb8c345d631a08d, 1.7306161136093256e-14), + (0xbfb8c345d631a08d, 1.7306161136093256e-14), + (0xbfb6f0d28ae5608c, -4.012913552726574e-14), + (0xbfb51b073f06208b, 2.7541708360737882e-14), + (0xbfb51b073f06208b, 2.7541708360737882e-14), + (0xbfb341d7961be08a, 5.0396178134370583e-14), + (0xbfb341d7961be08a, 5.0396178134370583e-14), + (0xbfb16536eea38089, 1.8195060030168815e-14), + (0xbfaf0a30c0118088, 5.213620639136504e-14), + (0xbfaf0a30c0118088, 5.213620639136504e-14), + (0xbfab42dd71198087, 2.532168943117445e-14), + (0xbfab42dd71198087, 2.532168943117445e-14), + (0xbfa77458f632c086, -5.148849572685811e-14), + (0xbfa77458f632c086, -5.148849572685811e-14), + (0xbfa39e87b9fec085, 4.6652946995830086e-15), + (0xbfa39e87b9fec085, 4.6652946995830086e-15), + (0xbf9f829b0e780084, -4.529814257790929e-14), + (0xbf9f829b0e780084, -4.529814257790929e-14), + (0xbf97b91b07d58083, -4.361324067851568e-14), + (0xbf8fc0a8b0fc0082, -1.7274567499706107e-15), + (0xbf8fc0a8b0fc0082, -1.7274567499706107e-15), + (0xbf7fe02a6b100081, -2.298941004620351e-14), + (0xbf7fe02a6b100081, -2.298941004620351e-14), + (0x0000000000000080, 0.0), + (0x0000000000000080, 0.0), + (0x3f8010157589007e, -1.4902732911301337e-14), + (0x3f9020565893807c, -3.527980389655325e-14), + (0x3f98492528c9007a, -4.730054772033249e-14), + (0x3fa0415d89e74078, 7.580310369375161e-15), + (0x3fa466aed42e0076, -4.9893776716773285e-14), + (0x3fa894aa149fc074, -2.262629393030674e-14), + (0x3faccb73cdddc072, -2.345674491018699e-14), + (0x3faeea31c006c071, -1.3352588834854848e-14), + (0x3fb1973bd146606f, -3.765296820388875e-14), + (0x3fb3bdf5a7d1e06d, 5.1128335719851986e-14), + (0x3fb5e95a4d97a06b, -5.046674438470119e-14), + (0x3fb700d30aeac06a, 3.1218748807418837e-15), + (0x3fb9335e5d594068, 3.3871241029241416e-14), + (0x3fbb6ac88dad6066, -1.7376727386423858e-14), + (0x3fbc885801bc4065, 3.957125899799804e-14), + (0x3fbec739830a2063, -5.2849453521890294e-14), + (0x3fbfe89139dbe062, -3.767012502308738e-14), + (0x3fc1178e8227e060, 3.1859736349078334e-14), + (0x3fc1aa2b7e23f05f, 5.0900642926060466e-14), + (0x3fc2d1610c86805d, 8.710783796122478e-15), + (0x3fc365fcb015905c, 6.157896229122976e-16), + (0x3fc4913d8333b05a, 3.821577743916796e-14), + (0x3fc527e5e4a1b059, 3.9440046718453496e-14), + (0x3fc6574ebe8c1057, 2.2924522154618074e-14), + (0x3fc6f0128b757056, -3.742530094732263e-14), + (0x3fc7898d85445055, -2.5223102140407338e-14), + (0x3fc8beafeb390053, -1.0320443688698849e-14), + (0x3fc95a5adcf70052, 1.0634128304268335e-14), + (0x3fca93ed3c8ae050, -4.3425422595242564e-14), + (0x3fcb31d8575bd04f, -1.2527395755711364e-14), + (0x3fcbd087383be04e, -5.204008743405884e-14), + (0x3fcc6ffbc6f0104d, -3.979844515951702e-15), + (0x3fcdb13db0d4904b, -4.7955860343296286e-14), + (0x3fce530effe7104a, 5.015686013791602e-16), + (0x3fcef5ade4dd0049, -7.252318953240293e-16), + (0x3fcf991c6cb3b048, 2.4688324156011588e-14), + (0x3fd07138604d5846, 5.465121253624792e-15), + (0x3fd0c42d67616045, 4.102651071698446e-14), + (0x3fd1178e8227e844, -4.996736502345936e-14), + (0x3fd16b5ccbacf843, 4.903580708156347e-14), + (0x3fd1bf99635a6842, 5.089628039500759e-14), + (0x3fd214456d0eb841, 1.1782016386565151e-14), + (0x3fd2bef07cdc903f, 4.727452940514406e-14), + (0x3fd314f1e1d3603e, -4.4204083338755686e-14), + (0x3fd36b6776be103d, 1.548345993498083e-14), + (0x3fd3c2527733303c, 2.1522127491642888e-14), + (0x3fd419b423d5e83b, 1.1054030169005386e-14), + (0x3fd4718dc271c83a, -5.534326352070679e-14), + (0x3fd4c9e09e173039, -5.351646604259541e-14), + (0x3fd522ae0738a038, 5.4612144489920215e-14), + (0x3fd57bf753c8d037, 2.8136969901227338e-14), + (0x3fd5d5bddf596036, -1.156568624616423e-14)) + + @inline function log_tab_unpack(t::UInt64) + invc = UInt64(t&UInt64(0xff)|0x1ff00)<<45 + logc = t&(~UInt64(0xff)) + return (reinterpret(Float64, invc), reinterpret(Float64, logc)) +end + +# Log implementation that returns 2 numbers which sum to give true value with about 68 bits of precision +# Since `log` only makes sense for positive exponents, we speed up the implimentation by stealing the sign bit +# of the input for an extra bit of the exponent which is used to normalize subnormal inputs. +# Does not normalize results. +# Adapted and modified from https://github.com/ARM-software/optimized-routines/blob/master/math/pow.c +# Copyright (c) 2018-2020, Arm Limited. (which is also MIT licensed) +# note that this isn't an exact translation as this version compacts the table to reduce cache pressure. +function _log_ext(xu) + # x = 2^k z; where z is in range [0x1.69555p-1,0x1.69555p-0) and exact. + # The range is split into N subintervals. + # The ith subinterval contains z and c is near the center of the interval. + tmp = reinterpret(Int64, xu - 0x3fe6955500000000) #0x1.69555p-1 + i = (tmp >> 45) & 127 + z = reinterpret(Float64, xu - (tmp & 0xfff0000000000000)) + k = Float64(tmp >> 52) + # log(x) = k*Ln2 + log(c) + log1p(z/c-1). + t, logctail = t_log_table_compat[i+1] + invc, logc = log_tab_unpack(t) + # Note: invc is j/N or j/N/2 where j is an integer in [N,2N) and + # |z/c - 1| < 1/N, so r = z/c - 1 is exactly representible. + r = fma(z, invc, -1.0) + # k*Ln2 + log(c) + r. + t1 = muladd(k, 0.6931471805598903, logc) #ln(2) hi part + t2 = t1 + r + lo1 = muladd(k, 5.497923018708371e-14, logctail) #ln(2) lo part + lo2 = t1 - t2 + r + ar = -0.5 * r + ar2, lo3 = two_mul(r, ar) + # k*Ln2 + log(c) + r + .5*r*r. + hi = t2 + ar2 + lo4 = t2 - hi + ar2 + p = evalpoly(r, (-0x1.555555555556p-1, 0x1.0000000000006p-1, -0x1.999999959554ep-2, 0x1.555555529a47ap-2, -0x1.2495b9b4845e9p-2, 0x1.0002b8b263fc3p-2)) + lo = lo1 + lo2 + lo3 + muladd(r*ar2, p, lo4) + return hi, lo end diff --git a/base/special/rem_pio2.jl b/base/special/rem_pio2.jl index 7242eb8f17b69a..4ec9945885e7e1 100644 --- a/base/special/rem_pio2.jl +++ b/base/special/rem_pio2.jl @@ -23,7 +23,7 @@ # @printf "0x%016x,\n" k # I -= k # end -const INV_2PI = UInt64[ +const INV_2PI = ( 0x28be_60db_9391_054a, 0x7f09_d5f4_7d4d_3770, 0x36d8_a566_4f10_e410, @@ -42,7 +42,7 @@ const INV_2PI = UInt64[ 0x5d49_eeb1_faf9_7c5e, 0xcf41_ce7d_e294_a4ba, 0x9afe_d7ec_47e3_5742, - 0x1580_cc11_bf1e_daea] + 0x1580_cc11_bf1e_daea) @inline function cody_waite_2c_pio2(x::Float64, fn, n) pio2_1 = 1.57079632673412561417e+00 diff --git a/base/special/trig.jl b/base/special/trig.jl index 40feaeb79522dc..e3033aab6c2720 100644 --- a/base/special/trig.jl +++ b/base/special/trig.jl @@ -50,7 +50,6 @@ function sin(x::T) where T<:Union{Float32, Float64} return -cos_kernel(y) end end -sin(x::Real) = sin(float(x)) # Coefficients in 13th order polynomial approximation on [0; π/4] # sin(x) ≈ x + S1*x³ + S2*x⁵ + S3*x⁷ + S4*x⁹ + S5*x¹¹ + S6*x¹³ @@ -121,7 +120,6 @@ function cos(x::T) where T<:Union{Float32, Float64} end end end -cos(x::Real) = cos(float(x)) const DC1 = 4.16666666666666019037e-02 const DC2 = -1.38888888888741095749e-03 @@ -168,7 +166,10 @@ end """ sincos(x) -Simultaneously compute the sine and cosine of `x`, where the `x` is in radians. +Simultaneously compute the sine and cosine of `x`, where `x` is in radians, returning +a tuple `(sine, cosine)`. + +See also [`cis`](@ref), [`sincospi`](@ref), [`sincosd`](@ref). """ function sincos(x::T) where T<:Union{Float32, Float64} if abs(x) < T(pi)/4 @@ -230,7 +231,6 @@ function tan(x::T) where T<:Union{Float32, Float64} return tan_kernel(y,-1) end end -tan(x::Real) = tan(float(x)) @inline tan_kernel(y::Float64) = tan_kernel(DoubleFloat64(y, 0.0), 1) @inline function tan_kernel(y::DoubleFloat64, k) @@ -423,7 +423,7 @@ end flipsign(Float32(pi/2 - 2*(s + s*tRt)), x) end -@noinline asin_domain_error(x) = throw(DomainError(x, "asin(x) is not defined for |x|>1.")) +@noinline asin_domain_error(x) = throw(DomainError(x, "asin(x) is not defined for |x| > 1.")) function asin(x::T) where T<:Union{Float32, Float64} # Since asin(x) = x + x^3/6 + x^5*3/40 + x^7*15/336 + ... # we approximate asin(x) on [0,0.5] by @@ -449,7 +449,6 @@ function asin(x::T) where T<:Union{Float32, Float64} t = (T(1.0) - absx)/2 return asin_kernel(t, x) end -asin(x::Real) = asin(float(x)) # atan methods ATAN_1_O_2_HI(::Type{Float64}) = 4.63647609000806093515e-01 # atan(0.5).hi @@ -499,7 +498,6 @@ atan_q(w::Float32) = w*@horner(w, -1.9999158382f-01, -1.0648017377f-01) atan_p(x², x⁴), atan_q(x⁴) end -atan(x::Real) = atan(float(x)) function atan(x::T) where T<:Union{Float32, Float64} # Method # 1. Reduce x to positive by atan(x) = -atan(-x). @@ -723,7 +721,6 @@ function acos(x::T) where T <: Union{Float32, Float64} return T(2.0)*(df + (zRz*s + c)) end end -acos(x::Real) = acos(float(x)) # multiply in extended precision function mulpi_ext(x::Float64) @@ -747,6 +744,8 @@ mulpi_ext(x::Real) = pi*x # Fallback sinpi(x) Compute ``\\sin(\\pi x)`` more accurately than `sin(pi*x)`, especially for large `x`. + +See also [`sind`](@ref), [`cospi`](@ref), [`sincospi`](@ref). """ function sinpi(x::T) where T<:AbstractFloat if !isfinite(x) @@ -863,10 +862,13 @@ end """ sincospi(x) -Simultaneously compute `sinpi(x)` and `cospi(x)`, where the `x` is in radians. +Simultaneously compute [`sinpi(x)`](@ref) and [`cospi(x)`](@ref) (the sine and cosine of `π*x`, +where `x` is in radians), returning a tuple `(sine, cosine)`. !!! compat "Julia 1.6" This function requires Julia 1.6 or later. + +See also: [`cispi`](@ref), [`sincosd`](@ref), [`sinpi`](@ref). """ function sincospi(x::T) where T<:AbstractFloat if !isfinite(x) @@ -1073,6 +1075,8 @@ isinf_real(x::Number) = false sinc(x) Compute ``\\sin(\\pi x) / (\\pi x)`` if ``x \\neq 0``, and ``1`` if ``x = 0``. + +See also [`cosc`](@ref), its derivative. """ sinc(x::Number) = _sinc(float(x)) sinc(x::Integer) = iszero(x) ? one(x) : zero(x) @@ -1250,37 +1254,45 @@ Simultaneously compute the sine and cosine of `x`, where `x` is in degrees. !!! compat "Julia 1.3" This function requires at least Julia 1.3. """ -function sincosd(x::Real) - if isinf(x) - return throw(DomainError(x, "sincosd(x) is only defined for finite `x`.")) - elseif isnan(x) - return (oftype(x,NaN), oftype(x,NaN)) - end - - # It turns out that calling those functions separately yielded better - # performance than considering each case and calling `sincos_kernel`. - return (sind(x), cosd(x)) -end +sincosd(x) = (sind(x), cosd(x)) +# It turns out that calling these functions separately yields better +# performance than considering each case and calling `sincos_kernel`. sincosd(::Missing) = (missing, missing) for (fd, f, fn) in ((:sind, :sin, "sine"), (:cosd, :cos, "cosine"), (:tand, :tan, "tangent")) - name = string(fd) - @eval begin - @doc """ - $($name)(x) - Compute $($fn) of `x`, where `x` is in degrees. """ ($fd)(z) = ($f)(deg2rad(z)) + for (fu, un) in ((:deg2rad, "degrees"),) + name = string(fd) + @eval begin + @doc """ + $($name)(x) + + Compute $($fn) of `x`, where `x` is in $($un). + If `x` is a matrix, `x` needs to be a square matrix. + + !!! compat "Julia 1.7" + Matrix arguments require Julia 1.7 or later. + """ ($fd)(x) = ($f)(($fu).(x)) + end end end for (fd, f, fn) in ((:asind, :asin, "sine"), (:acosd, :acos, "cosine"), (:asecd, :asec, "secant"), (:acscd, :acsc, "cosecant"), (:acotd, :acot, "cotangent")) - name = string(fd) - @eval begin - @doc """ - $($name)(x) - Compute the inverse $($fn) of `x`, where the output is in degrees. """ ($fd)(y) = rad2deg(($f)(y)) + for (fu, un) in ((:rad2deg, "degrees"),) + name = string(fd) + @eval begin + @doc """ + $($name)(x) + + Compute the inverse $($fn) of `x`, where the output is in $($un). + If `x` is a matrix, `x` needs to be a square matrix. + + !!! compat "Julia 1.7" + Matrix arguments require Julia 1.7 or later. + """ ($fd)(x) = ($fu).(($f)(x)) + end end end @@ -1289,6 +1301,9 @@ end atand(y,x) Compute the inverse tangent of `y` or `y/x`, respectively, where the output is in degrees. + +!!! compat "Julia 1.7" + The one-argument method supports square matrix arguments as of Julia 1.7. """ -atand(y) = rad2deg(atan(y)) -atand(y, x) = rad2deg(atan(y,x)) +atand(y) = rad2deg.(atan(y)) +atand(y, x) = rad2deg.(atan(y,x)) diff --git a/base/stacktraces.jl b/base/stacktraces.jl index ec366b0def1985..3cb81d82bd3f79 100644 --- a/base/stacktraces.jl +++ b/base/stacktraces.jl @@ -103,7 +103,7 @@ Given a pointer to an execution context (usually generated by a call to `backtra up stack frame context information. Returns an array of frame information for all functions inlined at that point, innermost function first. """ -function lookup(pointer::Ptr{Cvoid}) +Base.@constprop :none function lookup(pointer::Ptr{Cvoid}) infos = ccall(:jl_lookup_code_address, Any, (Ptr{Cvoid}, Cint), pointer, false)::Core.SimpleVector pointer = convert(UInt64, pointer) isempty(infos) && return [StackFrame(empty_sym, empty_sym, -1, nothing, true, false, pointer)] # this is equal to UNKNOWN @@ -111,41 +111,41 @@ function lookup(pointer::Ptr{Cvoid}) for i in 1:length(infos) info = infos[i]::Core.SimpleVector @assert(length(info) == 6) - res[i] = StackFrame(info[1], info[2], info[3], info[4], info[5], info[6], pointer) + res[i] = StackFrame(info[1]::Symbol, info[2]::Symbol, info[3]::Int, info[4], info[5]::Bool, info[6]::Bool, pointer) end return res end const top_level_scope_sym = Symbol("top-level scope") -function lookup(ip::Base.InterpreterIP) - if ip.code isa MethodInstance && ip.code.def isa Method - codeinfo = ip.code.uninferred - func = ip.code.def.name - file = ip.code.def.file - line = ip.code.def.line - elseif ip.code === nothing +function lookup(ip::Union{Base.InterpreterIP,Core.Compiler.InterpreterIP}) + code = ip.code + if code === nothing # interpreted top-level expression with no CodeInfo return [StackFrame(top_level_scope_sym, empty_sym, 0, nothing, false, false, 0)] + end + codeinfo = (code isa MethodInstance ? code.uninferred : code)::CodeInfo + # prepare approximate code info + if code isa MethodInstance && (meth = code.def; meth isa Method) + func = meth.name + file = meth.file + line = meth.line else - @assert ip.code isa CodeInfo - codeinfo = ip.code func = top_level_scope_sym file = empty_sym - line = 0 + line = Int32(0) end i = max(ip.stmt+1, 1) # ip.stmt is 0-indexed if i > length(codeinfo.codelocs) || codeinfo.codelocs[i] == 0 - return [StackFrame(func, file, line, ip.code, false, false, 0)] + return [StackFrame(func, file, line, code, false, false, 0)] end - lineinfo = codeinfo.linetable[codeinfo.codelocs[i]] + lineinfo = codeinfo.linetable[codeinfo.codelocs[i]]::Core.LineInfoNode scopes = StackFrame[] while true - push!(scopes, StackFrame(lineinfo.method, lineinfo.file, lineinfo.line, ip.code, false, false, 0)) - if lineinfo.inlined_at == 0 - break - end - lineinfo = codeinfo.linetable[lineinfo.inlined_at] + inlined = lineinfo.inlined_at != 0 + push!(scopes, StackFrame(Base.IRShow.method_name(lineinfo)::Symbol, lineinfo.file, lineinfo.line, inlined ? nothing : code, false, inlined, 0)) + inlined || break + lineinfo = codeinfo.linetable[lineinfo.inlined_at]::Core.LineInfoNode end return scopes end @@ -157,7 +157,7 @@ Returns a stack trace in the form of a vector of `StackFrame`s. (By default stac doesn't return C functions, but this can be enabled.) When called without specifying a trace, `stacktrace` first calls `backtrace`. """ -function stacktrace(trace::Vector{<:Union{Base.InterpreterIP,Ptr{Cvoid}}}, c_funcs::Bool=false) +Base.@constprop :none function stacktrace(trace::Vector{<:Union{Base.InterpreterIP,Core.Compiler.InterpreterIP,Ptr{Cvoid}}}, c_funcs::Bool=false) stack = StackTrace() for ip in trace for frame in lookup(ip) @@ -170,7 +170,7 @@ function stacktrace(trace::Vector{<:Union{Base.InterpreterIP,Ptr{Cvoid}}}, c_fun return stack end -function stacktrace(c_funcs::Bool=false) +Base.@constprop :none function stacktrace(c_funcs::Bool=false) stack = stacktrace(backtrace(), c_funcs) # Remove frame for this function (and any functions called by this function). remove_frames!(stack, :stacktrace) @@ -217,7 +217,7 @@ function show_spec_linfo(io::IO, frame::StackFrame) elseif frame.func === top_level_scope_sym print(io, "top-level scope") else - print(io, Base.demangle_function_name(string(frame.func))) + Base.print_within_stacktrace(io, Base.demangle_function_name(string(frame.func)), bold=true) end elseif linfo isa MethodInstance def = linfo.def @@ -236,12 +236,15 @@ function show_spec_linfo(io::IO, frame::StackFrame) kwnames[i] = Symbol(str[1:end-3]) end end - Base.show_tuple_as_call(io, def.name, pos_sig, true, zip(kwnames, kwarg_types), argnames[def.nkw+2:end]) + Base.show_tuple_as_call(io, def.name, pos_sig; + demangle=true, + kwargs=zip(kwnames, kwarg_types), + argnames=argnames[def.nkw+2:end]) else - Base.show_tuple_as_call(io, def.name, sig, true, nothing, argnames) + Base.show_tuple_as_call(io, def.name, sig; demangle=true, argnames) end else - Base.show(io, linfo) + Base.show_mi(io, linfo, true) end elseif linfo isa CodeInfo print(io, "top-level scope") @@ -266,8 +269,9 @@ function show(io::IO, frame::StackFrame) end function Base.parentmodule(frame::StackFrame) - if frame.linfo isa MethodInstance - def = frame.linfo.def + linfo = frame.linfo + if linfo isa MethodInstance + def = linfo.def if def isa Module return def else diff --git a/base/stat.jl b/base/stat.jl index 15bbe0b34d2dd2..f38a82634dc2fe 100644 --- a/base/stat.jl +++ b/base/stat.jl @@ -26,6 +26,7 @@ export uperm struct StatStruct + desc :: Union{String, OS_HANDLE} # for show method, not included in equality or hash device :: UInt inode :: UInt mode :: UInt @@ -40,9 +41,25 @@ struct StatStruct ctime :: Float64 end -StatStruct() = StatStruct(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) +@eval function Base.:(==)(x::StatStruct, y::StatStruct) # do not include `desc` in equality or hash + $(let ex = true + for fld in fieldnames(StatStruct)[2:end] + ex = :(getfield(x, $(QuoteNode(fld))) === getfield(y, $(QuoteNode(fld))) && $ex) + end + Expr(:return, ex) + end) +end +@eval function Base.hash(obj::StatStruct, h::UInt) + $(quote + $(Any[:(h = hash(getfield(obj, $(QuoteNode(fld))), h)) for fld in fieldnames(StatStruct)[2:end]]...) + return h + end) +end -StatStruct(buf::Union{Vector{UInt8},Ptr{UInt8}}) = StatStruct( +StatStruct() = StatStruct("", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) +StatStruct(buf::Union{Vector{UInt8},Ptr{UInt8}}) = StatStruct("", buf) +StatStruct(desc::Union{AbstractString, OS_HANDLE}, buf::Union{Vector{UInt8},Ptr{UInt8}}) = StatStruct( + desc isa OS_HANDLE ? desc : String(desc), ccall(:jl_stat_dev, UInt32, (Ptr{UInt8},), buf), ccall(:jl_stat_ino, UInt32, (Ptr{UInt8},), buf), ccall(:jl_stat_mode, UInt32, (Ptr{UInt8},), buf), @@ -57,18 +74,84 @@ StatStruct(buf::Union{Vector{UInt8},Ptr{UInt8}}) = StatStruct( ccall(:jl_stat_ctime, Float64, (Ptr{UInt8},), buf), ) -show(io::IO, st::StatStruct) = print(io, "StatStruct(mode=0o$(string(filemode(st), base = 8, pad = 6)), size=$(filesize(st)))") +function iso_datetime_with_relative(t, tnow) + str = Libc.strftime("%FT%T%z", t) + secdiff = t - tnow + for (d, name) in ((24*60*60, "day"), (60*60, "hour"), (60, "minute"), (1, "second")) + tdiff = round(Int, div(abs(secdiff), d)) + if tdiff != 0 # find first unit difference + plural = tdiff == 1 ? "" : "s" + when = secdiff < 0 ? "ago" : "in the future" + return "$str ($tdiff $name$plural $when)" + end + end + return "$str (just now)" +end + + +function getusername(uid::Unsigned) + pwd = Libc.getpwuid(uid, false) + pwd === nothing && return + isempty(pwd.username) && return + return pwd.username +end + +function getgroupname(gid::Unsigned) + gp = Libc.getgrgid(gid, false) + gp === nothing && return + isempty(gp.groupname) && return + return gp.groupname +end + +function show_statstruct(io::IO, st::StatStruct, oneline::Bool) + print(io, oneline ? "StatStruct(" : "StatStruct for ") + show(io, st.desc) + oneline || print(io, "\n ") + print(io, " size: ", st.size, " bytes") + oneline || print(io, "\n") + print(io, " device: ", st.device) + oneline || print(io, "\n ") + print(io, " inode: ", st.inode) + oneline || print(io, "\n ") + print(io, " mode: 0o", string(filemode(st), base = 8, pad = 6), " (", filemode_string(st), ")") + oneline || print(io, "\n ") + print(io, " nlink: ", st.nlink) + oneline || print(io, "\n ") + print(io, " uid: $(st.uid)") + username = getusername(st.uid) + username === nothing || print(io, " (", username, ")") + oneline || print(io, "\n ") + print(io, " gid: ", st.gid) + groupname = getgroupname(st.gid) + groupname === nothing || print(io, " (", groupname, ")") + oneline || print(io, "\n ") + print(io, " rdev: ", st.rdev) + oneline || print(io, "\n ") + print(io, " blksz: ", st.blksize) + oneline || print(io, "\n") + print(io, " blocks: ", st.blocks) + tnow = round(UInt, time()) + oneline || print(io, "\n ") + print(io, " mtime: ", iso_datetime_with_relative(st.mtime, tnow)) + oneline || print(io, "\n ") + print(io, " ctime: ", iso_datetime_with_relative(st.ctime, tnow)) + oneline && print(io, ")") + return nothing +end + +show(io::IO, st::StatStruct) = show_statstruct(io, st, true) +show(io::IO, ::MIME"text/plain", st::StatStruct) = show_statstruct(io, st, false) # stat & lstat functions macro stat_call(sym, arg1type, arg) return quote - stat_buf = zeros(UInt8, ccall(:jl_sizeof_stat, Int32, ())) + stat_buf = zeros(UInt8, Int(ccall(:jl_sizeof_stat, Int32, ()))) r = ccall($(Expr(:quote, sym)), Int32, ($(esc(arg1type)), Ptr{UInt8}), $(esc(arg)), stat_buf) if !(r in (0, Base.UV_ENOENT, Base.UV_ENOTDIR, Base.UV_EINVAL)) - throw(_UVError("stat", r, "for file ", repr($(esc(arg))))) + uv_error(string("stat(", repr($(esc(arg))), ")"), r) end - st = StatStruct(stat_buf) + st = StatStruct($(esc(arg)), stat_buf) if ispath(st) != (r == 0) error("stat returned zero type for a valid path") end @@ -92,6 +175,7 @@ The fields of the structure are: | Name | Description | |:--------|:-------------------------------------------------------------------| +| desc | The path or OS file descriptor | | size | The size (in bytes) of the file | | device | ID of the device that contains the file | | inode | The inode number of the file | @@ -103,7 +187,7 @@ The fields of the structure are: | blksize | The file-system preferred block size for the file | | blocks | The number of such blocks allocated | | mtime | Unix timestamp of when the file was last modified | -| ctime | Unix timestamp of when the file was created | +| ctime | Unix timestamp of when the file's metadata was changed | """ stat(path...) = stat(joinpath(path...)) @@ -120,12 +204,73 @@ lstat(path...) = lstat(joinpath(path...)) # some convenience functions +const filemode_table = ( + [ + (S_IFLNK, "l"), + (S_IFSOCK, "s"), # Must appear before IFREG and IFDIR as IFSOCK == IFREG | IFDIR + (S_IFREG, "-"), + (S_IFBLK, "b"), + (S_IFDIR, "d"), + (S_IFCHR, "c"), + (S_IFIFO, "p") + ], + [ + (S_IRUSR, "r"), + ], + [ + (S_IWUSR, "w"), + ], + [ + (S_IXUSR|S_ISUID, "s"), + (S_ISUID, "S"), + (S_IXUSR, "x") + ], + [ + (S_IRGRP, "r"), + ], + [ + (S_IWGRP, "w"), + ], + [ + (S_IXGRP|S_ISGID, "s"), + (S_ISGID, "S"), + (S_IXGRP, "x") + ], + [ + (S_IROTH, "r"), + ], + [ + (S_IWOTH, "w"), + ], + [ + (S_IXOTH|S_ISVTX, "t"), + (S_ISVTX, "T"), + (S_IXOTH, "x") + ] +) + """ filemode(file) Equivalent to `stat(file).mode`. """ filemode(st::StatStruct) = st.mode +filemode_string(st::StatStruct) = filemode_string(st.mode) +function filemode_string(mode) + str = IOBuffer() + for table in filemode_table + complete = true + for (bit, char) in table + if mode & bit == bit + write(str, char) + complete = false + break + end + end + complete && write(str, "-") + end + return String(take!(str)) +end """ filesize(path...) @@ -187,7 +332,7 @@ julia> isdir("not/a/directory") false ``` -See also: [`isfile`](@ref) and [`ispath`](@ref). +See also [`isfile`](@ref) and [`ispath`](@ref). """ isdir(st::StatStruct) = filemode(st) & 0xf000 == 0x4000 @@ -216,7 +361,7 @@ true julia> close(f); rm("test_file.txt") ``` -See also: [`isdir`](@ref) and [`ispath`](@ref). +See also [`isdir`](@ref) and [`ispath`](@ref). """ isfile(st::StatStruct) = filemode(st) & 0xf000 == 0x8000 diff --git a/base/stream.jl b/base/stream.jl index 9ce58744b53f17..948c12ad604b44 100644 --- a/base/stream.jl +++ b/base/stream.jl @@ -43,7 +43,7 @@ end An abstract type for IO streams handled by libuv. -If`stream isa LibuvStream`, it must obey the following interface: +If `stream isa LibuvStream`, it must obey the following interface: - `stream.handle`, if present, must be a `Ptr{Cvoid}` - `stream.status`, if present, must be an `Int` @@ -109,7 +109,7 @@ function eof(s::LibuvStream) # and that we won't return true if there's a readerror pending (it'll instead get thrown). # This requires some careful ordering here (TODO: atomic loads) bytesavailable(s) > 0 && return false - open = isopen(s) # must precede readerror check + open = isreadable(s) # must precede readerror check s.readerror === nothing || throw(s.readerror) return !open end @@ -270,6 +270,7 @@ show(io::IO, stream::LibuvStream) = print(io, typeof(stream), "(", function isreadable(io::LibuvStream) bytesavailable(io) > 0 && return true isopen(io) || return false + io.status == StatusEOF && return false return ccall(:uv_is_readable, Cint, (Ptr{Cvoid},), io.handle) != 0 end @@ -282,6 +283,7 @@ end lock(s::LibuvStream) = lock(s.lock) unlock(s::LibuvStream) = unlock(s.lock) +setup_stdio(stream::LibuvStream, ::Bool) = (stream, false) rawhandle(stream::LibuvStream) = stream.handle unsafe_convert(::Type{Ptr{Cvoid}}, s::Union{LibuvStream, LibuvServer}) = s.handle @@ -375,10 +377,10 @@ if OS_HANDLE != RawFD end function isopen(x::Union{LibuvStream, LibuvServer}) - if x.status == StatusUninit || x.status == StatusInit + if x.status == StatusUninit || x.status == StatusInit || x.handle === C_NULL throw(ArgumentError("$x is not initialized")) end - return x.status != StatusClosed && x.status != StatusEOF + return x.status != StatusClosed end function check_open(x::Union{LibuvStream, LibuvServer}) @@ -390,13 +392,13 @@ end function wait_readnb(x::LibuvStream, nb::Int) # fast path before iolock acquire bytesavailable(x.buffer) >= nb && return - open = isopen(x) # must precede readerror check + open = isopen(x) && x.status != StatusEOF # must precede readerror check x.readerror === nothing || throw(x.readerror) open || return iolock_begin() # repeat fast path after iolock acquire, before other expensive work bytesavailable(x.buffer) >= nb && (iolock_end(); return) - open = isopen(x) + open = isopen(x) && x.status != StatusEOF x.readerror === nothing || throw(x.readerror) open || (iolock_end(); return) # now do the "real" work @@ -407,6 +409,7 @@ function wait_readnb(x::LibuvStream, nb::Int) while bytesavailable(x.buffer) < nb x.readerror === nothing || throw(x.readerror) isopen(x) || break + x.status != StatusEOF || break x.throttle = max(nb, x.throttle) start_reading(x) # ensure we are reading iolock_end() @@ -431,6 +434,52 @@ function wait_readnb(x::LibuvStream, nb::Int) nothing end +function closewrite(s::LibuvStream) + iolock_begin() + check_open(s) + req = Libc.malloc(_sizeof_uv_shutdown) + uv_req_set_data(req, C_NULL) # in case we get interrupted before arriving at the wait call + err = ccall(:uv_shutdown, Int32, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}), + req, s, @cfunction(uv_shutdowncb_task, Cvoid, (Ptr{Cvoid}, Cint))) + if err < 0 + Libc.free(req) + uv_error("shutdown", err) + end + ct = current_task() + preserve_handle(ct) + sigatomic_begin() + uv_req_set_data(req, ct) + iolock_end() + status = try + sigatomic_end() + wait()::Cint + finally + # try-finally unwinds the sigatomic level, so need to repeat sigatomic_end + sigatomic_end() + iolock_begin() + ct.queue === nothing || list_deletefirst!(ct.queue, ct) + if uv_req_data(req) != C_NULL + # req is still alive, + # so make sure we won't get spurious notifications later + uv_req_set_data(req, C_NULL) + else + # done with req + Libc.free(req) + end + iolock_end() + unpreserve_handle(ct) + end + if isopen(s) + if status < 0 || ccall(:uv_is_readable, Cint, (Ptr{Cvoid},), s.handle) == 0 + close(s) + end + end + if status < 0 + throw(_UVError("shutdown", status)) + end + nothing +end + function wait_close(x::Union{LibuvStream, LibuvServer}) preserve_handle(x) lock(x.cond) @@ -447,34 +496,37 @@ end function close(stream::Union{LibuvStream, LibuvServer}) iolock_begin() - should_wait = false if stream.status == StatusInit ccall(:jl_forceclose_uv, Cvoid, (Ptr{Cvoid},), stream.handle) stream.status = StatusClosing - elseif isopen(stream) || stream.status == StatusEOF - should_wait = uv_handle_data(stream) != C_NULL + elseif isopen(stream) if stream.status != StatusClosing ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), stream.handle) stream.status = StatusClosing end end iolock_end() - should_wait && wait_close(stream) + wait_close(stream) nothing end function uvfinalize(uv::Union{LibuvStream, LibuvServer}) - uv.handle == C_NULL && return iolock_begin() if uv.handle != C_NULL - disassociate_julia_struct(uv.handle) # not going to call the usual close hooks - if uv.status != StatusUninit - close(uv) - else + disassociate_julia_struct(uv.handle) # not going to call the usual close hooks (so preserve_handle is not needed) + if uv.status == StatusUninit + Libc.free(uv.handle) + elseif uv.status == StatusInit + ccall(:jl_forceclose_uv, Cvoid, (Ptr{Cvoid},), uv.handle) + elseif isopen(uv) + if uv.status != StatusClosing + ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), uv.handle) + end + elseif uv.status == StatusClosed Libc.free(uv.handle) end - uv.status = StatusClosed uv.handle = C_NULL + uv.status = StatusClosed end iolock_end() nothing @@ -503,7 +555,7 @@ julia> withenv("LINES" => 30, "COLUMNS" => 100) do To get your TTY size, -```julia +```julia-repl julia> displaysize(stdout) (34, 147) ``` @@ -554,7 +606,7 @@ function alloc_request(buffer::IOBuffer, recommended_size::UInt) ensureroom(buffer, Int(recommended_size)) ptr = buffer.append ? buffer.size + 1 : buffer.ptr nb = min(length(buffer.data), buffer.maxsize) - ptr + 1 - return (pointer(buffer.data, ptr), nb) + return (Ptr{Cvoid}(pointer(buffer.data, ptr)), nb) end notify_filled(buffer::IOBuffer, nread::Int, base::Ptr{Cvoid}, len::UInt) = notify_filled(buffer, nread) @@ -606,35 +658,33 @@ function uv_readcb(handle::Ptr{Cvoid}, nread::Cssize_t, buf::Ptr{Cvoid}) nrequested = ccall(:jl_uv_buf_len, Csize_t, (Ptr{Cvoid},), buf) function readcb_specialized(stream::LibuvStream, nread::Int, nrequested::UInt) lock(stream.cond) - try - if nread < 0 - if nread == UV_ENOBUFS && nrequested == 0 - # remind the client that stream.buffer is full - notify(stream.cond) - elseif nread == UV_EOF - if isa(stream, TTY) - stream.status = StatusEOF # libuv called uv_stop_reading already + if nread < 0 + if nread == UV_ENOBUFS && nrequested == 0 + # remind the client that stream.buffer is full + notify(stream.cond) + elseif nread == UV_EOF # libuv called uv_stop_reading already + if stream.status != StatusClosing + stream.status = StatusEOF + if stream isa TTY # TODO: || ccall(:uv_is_writable, Cint, (Ptr{Cvoid},), stream.handle) != 0 + # stream can still be used either by reseteof # TODO: or write notify(stream.cond) - elseif stream.status != StatusClosing - # begin shutdown of the stream + else + # underlying stream is no longer useful: begin finalization ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), stream.handle) stream.status = StatusClosing end - else - stream.readerror = _UVError("read", nread) - # This is a fatal connection error. Shutdown requests as per the usual - # close function won't work and libuv will fail with an assertion failure - ccall(:jl_forceclose_uv, Cvoid, (Ptr{Cvoid},), stream) - stream.status = StatusClosing - notify(stream.cond) end else - notify_filled(stream.buffer, nread) - notify(stream.cond) + stream.readerror = _UVError("read", nread) + # This is a fatal connection error + ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), stream.handle) + stream.status = StatusClosing end - finally - unlock(stream.cond) + else + notify_filled(stream.buffer, nread) + notify(stream.cond) end + unlock(stream.cond) # Stop background reading when # 1) there's nobody paying attention to the data we are reading @@ -651,6 +701,7 @@ function uv_readcb(handle::Ptr{Cvoid}, nread::Cssize_t, buf::Ptr{Cvoid}) nothing end readcb_specialized(stream_unknown_type, Int(nread), UInt(nrequested)) + nothing end function reseteof(x::TTY) @@ -665,7 +716,6 @@ end function _uv_hook_close(uv::Union{LibuvStream, LibuvServer}) lock(uv.cond) try - uv.handle = C_NULL uv.status = StatusClosed # notify any listeners that exist on this libuv stream type notify(uv.cond) @@ -844,6 +894,7 @@ function readbytes!(s::LibuvStream, a::Vector{UInt8}, nb::Int) while bytesavailable(buf) < nb s.readerror === nothing || throw(s.readerror) isopen(s) || break + s.status != StatusEOF || break iolock_end() wait_readnb(s, nb) iolock_begin() @@ -890,6 +941,7 @@ function unsafe_read(s::LibuvStream, p::Ptr{UInt8}, nb::UInt) while bytesavailable(buf) < nb s.readerror === nothing || throw(s.readerror) isopen(s) || throw(EOFError()) + s.status != StatusEOF || throw(EOFError()) iolock_end() wait_readnb(s, nb) iolock_begin() @@ -946,13 +998,14 @@ function readuntil(x::LibuvStream, c::UInt8; keep::Bool=false) @assert buf.seekable == false if !occursin(c, buf) # fast path checks first x.readerror === nothing || throw(x.readerror) - if isopen(x) + if isopen(x) && x.status != StatusEOF preserve_handle(x) lock(x.cond) try while !occursin(c, x.buffer) x.readerror === nothing || throw(x.readerror) isopen(x) || break + x.status != StatusEOF || break start_reading(x) # ensure we are reading iolock_end() wait(x.cond) @@ -1115,128 +1168,273 @@ function uv_writecb_task(req::Ptr{Cvoid}, status::Cint) nothing end +function uv_shutdowncb_task(req::Ptr{Cvoid}, status::Cint) + d = uv_req_data(req) + if d != C_NULL + uv_req_set_data(req, C_NULL) # let the Task know we got the shutdowncb + t = unsafe_pointer_to_objref(d)::Task + schedule(t, status) + else + # no owner for this req, safe to just free it + Libc.free(req) + end + nothing +end + + _fd(x::IOStream) = RawFD(fd(x)) _fd(x::Union{OS_HANDLE, RawFD}) = x function _fd(x::Union{LibuvStream, LibuvServer}) fd = Ref{OS_HANDLE}(INVALID_OS_HANDLE) - if x.status != StatusUninit && x.status != StatusClosed + if x.status != StatusUninit && x.status != StatusClosed && x.handle != C_NULL err = ccall(:uv_fileno, Int32, (Ptr{Cvoid}, Ptr{OS_HANDLE}), x.handle, fd) # handle errors by returning INVALID_OS_HANDLE end return fd[] end -for (x, writable, unix_fd, c_symbol) in - ((:stdin, false, 0, :jl_uv_stdin), - (:stdout, true, 1, :jl_uv_stdout), - (:stderr, true, 2, :jl_uv_stderr)) - f = Symbol("redirect_", lowercase(string(x))) - _f = Symbol("_", f) - @eval begin - function ($_f)(stream) - global $x - posix_fd = _fd(stream) - @static if Sys.iswindows() - ccall(:SetStdHandle, stdcall, Int32, (Int32, OS_HANDLE), - $(-10 - unix_fd), Libc._get_osfhandle(posix_fd)) - end - dup(posix_fd, RawFD($unix_fd)) - $x = stream - nothing - end - function ($f)(handle::Union{LibuvStream, IOStream}) - $(_f)(handle) - unsafe_store!(cglobal($(Expr(:quote, c_symbol)), Ptr{Cvoid}), - handle.handle) - return handle - end - function ($f)() - p = link_pipe!(Pipe()) - read, write = p.out, p.in - ($f)($(writable ? :write : :read)) - return (read, write) - end - function ($f)(::DevNull) - global $x - nulldev = @static Sys.iswindows() ? "NUL" : "/dev/null" - handle = open(nulldev, write=$writable) - $(_f)(handle) - close(handle) # handle has been dup'ed in $(_f) - $x = devnull - return devnull - end - function ($f)(io::IOContext) - io2, _dict = unwrapcontext(io) - ($f)(io2) - global $x = io - return io +struct RedirectStdStream <: Function + unix_fd::Int + writable::Bool +end +for (f, writable, unix_fd) in + ((:redirect_stdin, false, 0), + (:redirect_stdout, true, 1), + (:redirect_stderr, true, 2)) + @eval const ($f) = RedirectStdStream($unix_fd, $writable) +end +function _redirect_io_libc(stream, unix_fd::Int) + posix_fd = _fd(stream) + @static if Sys.iswindows() + if 0 <= unix_fd <= 2 + ccall(:SetStdHandle, stdcall, Int32, (Int32, OS_HANDLE), + -10 - unix_fd, Libc._get_osfhandle(posix_fd)) end end + dup(posix_fd, RawFD(unix_fd)) + nothing +end +function _redirect_io_global(io, unix_fd::Int) + unix_fd == 0 && (global stdin = io) + unix_fd == 1 && (global stdout = io) + unix_fd == 2 && (global stderr = io) + nothing +end +function (f::RedirectStdStream)(handle::Union{LibuvStream, IOStream}) + _redirect_io_libc(handle, f.unix_fd) + c_sym = f.unix_fd == 0 ? cglobal(:jl_uv_stdin, Ptr{Cvoid}) : + f.unix_fd == 1 ? cglobal(:jl_uv_stdout, Ptr{Cvoid}) : + f.unix_fd == 2 ? cglobal(:jl_uv_stderr, Ptr{Cvoid}) : + C_NULL + c_sym == C_NULL || unsafe_store!(c_sym, handle.handle) + _redirect_io_global(handle, f.unix_fd) + return handle +end +function (f::RedirectStdStream)(::DevNull) + nulldev = @static Sys.iswindows() ? "NUL" : "/dev/null" + handle = open(nulldev, write=f.writable) + _redirect_io_libc(handle, f.unix_fd) + close(handle) # handle has been dup'ed in _redirect_io_libc + _redirect_io_global(devnull, f.unix_fd) + return devnull +end +function (f::RedirectStdStream)(io::AbstractPipe) + io2 = (f.writable ? pipe_writer : pipe_reader)(io) + f(io2) + _redirect_io_global(io, f.unix_fd) + return io end +function (f::RedirectStdStream)(p::Pipe) + if p.in.status == StatusInit && p.out.status == StatusInit + link_pipe!(p) + end + io2 = getfield(p, f.writable ? :in : :out) + f(io2) + return p +end +(f::RedirectStdStream)() = f(Pipe()) + +# Deprecate these in v2 (RedirectStdStream support) +iterate(p::Pipe) = (p.out, 1) +iterate(p::Pipe, i::Int) = i == 1 ? (p.in, 2) : nothing +getindex(p::Pipe, key::Int) = key == 1 ? p.out : key == 2 ? p.in : throw(KeyError(key)) """ - redirect_stdout([stream]) -> (rd, wr) + redirect_stdout([stream]) -> stream Create a pipe to which all C and Julia level [`stdout`](@ref) output -will be redirected. -Returns a tuple `(rd, wr)` representing the pipe ends. +will be redirected. Return a stream representing the pipe ends. Data written to [`stdout`](@ref) may now be read from the `rd` end of -the pipe. The `wr` end is given for convenience in case the old -[`stdout`](@ref) object was cached by the user and needs to be replaced -elsewhere. - -If called with the optional `stream` argument, then returns `stream` itself. +the pipe. !!! note - `stream` must be an `IOStream`, a `TTY`, a `Pipe`, a socket, or `devnull`. + `stream` must be a compatible objects, such as an `IOStream`, `TTY`, + `Pipe`, socket, or `devnull`. + +See also [`redirect_stdio`](@ref). """ redirect_stdout """ - redirect_stderr([stream]) -> (rd, wr) + redirect_stderr([stream]) -> stream Like [`redirect_stdout`](@ref), but for [`stderr`](@ref). !!! note - `stream` must be an `IOStream`, a `TTY`, a `Pipe`, a socket, or `devnull`. + `stream` must be a compatible objects, such as an `IOStream`, `TTY`, + `Pipe`, socket, or `devnull`. + +See also [`redirect_stdio`](@ref). """ redirect_stderr """ - redirect_stdin([stream]) -> (rd, wr) + redirect_stdin([stream]) -> stream Like [`redirect_stdout`](@ref), but for [`stdin`](@ref). -Note that the order of the return tuple is still `(rd, wr)`, -i.e. data to be read from [`stdin`](@ref) may be written to `wr`. +Note that the direction of the stream is reversed. !!! note - `stream` must be an `IOStream`, a `TTY`, a `Pipe`, a socket, or `devnull`. + `stream` must be a compatible objects, such as an `IOStream`, `TTY`, + `Pipe`, socket, or `devnull`. + +See also [`redirect_stdio`](@ref). """ redirect_stdin -for (F,S) in ((:redirect_stdin, :stdin), (:redirect_stdout, :stdout), (:redirect_stderr, :stderr)) - @eval function $F(f::Function, stream) - STDOLD = $S - local ret - $F(stream) - try - ret = f() - finally - $F(STDOLD) - end - ret +""" + redirect_stdio(;stdin=stdin, stderr=stderr, stdout=stdout) + +Redirect a subset of the streams `stdin`, `stderr`, `stdout`. +Each argument must be an `IOStream`, `TTY`, `Pipe`, socket, or `devnull`. + +!!! compat "Julia 1.7" + `redirect_stdio` requires Julia 1.7 or later. +""" +function redirect_stdio(;stdin=nothing, stderr=nothing, stdout=nothing) + stdin === nothing || redirect_stdin(stdin) + stderr === nothing || redirect_stderr(stderr) + stdout === nothing || redirect_stdout(stdout) +end + +""" + redirect_stdio(f; stdin=nothing, stderr=nothing, stdout=nothing) + +Redirect a subset of the streams `stdin`, `stderr`, `stdout`, +call `f()` and restore each stream. + +Possible values for each stream are: +* `nothing` indicating the stream should not be redirected. +* `path::AbstractString` redirecting the stream to the file at `path`. +* `io` an `IOStream`, `TTY`, `Pipe`, socket, or `devnull`. + +# Examples +```julia-repl +julia> redirect_stdio(stdout="stdout.txt", stderr="stderr.txt") do + print("hello stdout") + print(stderr, "hello stderr") + end + +julia> read("stdout.txt", String) +"hello stdout" + +julia> read("stderr.txt", String) +"hello stderr" +``` + +# Edge cases + +It is possible to pass the same argument to `stdout` and `stderr`: +```julia-repl +julia> redirect_stdio(stdout="log.txt", stderr="log.txt", stdin=devnull) do + ... +end +``` + +However it is not supported to pass two distinct descriptors of the same file. +```julia-repl +julia> io1 = open("same/path", "w") + +julia> io2 = open("same/path", "w") + +julia> redirect_stdio(f, stdout=io1, stderr=io2) # not suppored +``` +Also the `stdin` argument may not be the same descriptor as `stdout` or `stderr`. +```julia-repl +julia> io = open(...) + +julia> redirect_stdio(f, stdout=io, stdin=io) # not supported +``` + +!!! compat "Julia 1.7" + `redirect_stdio` requires Julia 1.7 or later. +""" +function redirect_stdio(f; stdin=nothing, stderr=nothing, stdout=nothing) + + function resolve(new::Nothing, oldstream, mode) + (new=nothing, close=false, old=nothing) + end + function resolve(path::AbstractString, oldstream,mode) + (new=open(path, mode), close=true, old=oldstream) + end + function resolve(new, oldstream, mode) + (new=new, close=false, old=oldstream) + end + + same_path(x, y) = false + function same_path(x::AbstractString, y::AbstractString) + # if x = y = "does_not_yet_exist.txt" then samefile will return false + (abspath(x) == abspath(y)) || samefile(x,y) + end + if same_path(stderr, stdin) + throw(ArgumentError("stdin and stderr cannot be the same path")) + end + if same_path(stdout, stdin) + throw(ArgumentError("stdin and stdout cannot be the same path")) + end + + new_in , close_in , old_in = resolve(stdin , Base.stdin , "r") + new_out, close_out, old_out = resolve(stdout, Base.stdout, "w") + if same_path(stderr, stdout) + # make sure that in case stderr = stdout = "same/path" + # only a single io is used instead of opening the same file twice + new_err, close_err, old_err = new_out, false, Base.stderr + else + new_err, close_err, old_err = resolve(stderr, Base.stderr, "w") + end + + redirect_stdio(; stderr=new_err, stdin=new_in, stdout=new_out) + + try + return f() + finally + redirect_stdio(;stderr=old_err, stdin=old_in, stdout=old_out) + close_err && close(new_err) + close_in && close(new_in ) + close_out && close(new_out) + end +end + +function (f::RedirectStdStream)(thunk::Function, stream) + stdold = f.unix_fd == 0 ? stdin : + f.unix_fd == 1 ? stdout : + f.unix_fd == 2 ? stderr : + throw(ArgumentError("Not implemented to get old handle of fd except for stdio")) + f(stream) + try + return thunk() + finally + f(stdold) end end + """ redirect_stdout(f::Function, stream) Run the function `f` while redirecting [`stdout`](@ref) to `stream`. Upon completion, [`stdout`](@ref) is restored to its prior setting. - -!!! note - `stream` must be a `TTY`, a `Pipe`, or a socket. """ redirect_stdout(f::Function, stream) @@ -1245,9 +1443,6 @@ redirect_stdout(f::Function, stream) Run the function `f` while redirecting [`stderr`](@ref) to `stream`. Upon completion, [`stderr`](@ref) is restored to its prior setting. - -!!! note - `stream` must be a `TTY`, a `Pipe`, or a socket. """ redirect_stderr(f::Function, stream) @@ -1256,9 +1451,6 @@ redirect_stderr(f::Function, stream) Run the function `f` while redirecting [`stdin`](@ref) to `stream`. Upon completion, [`stdin`](@ref) is restored to its prior setting. - -!!! note - `stream` must be a `TTY`, a `Pipe`, or a socket. """ redirect_stdin(f::Function, stream) @@ -1280,23 +1472,26 @@ mutable struct BufferStream <: LibuvStream buffer::IOBuffer cond::Threads.Condition readerror::Any - is_open::Bool buffer_writes::Bool lock::ReentrantLock # advisory lock + status::Int - BufferStream() = new(PipeBuffer(), Threads.Condition(), nothing, true, false, ReentrantLock()) + BufferStream() = new(PipeBuffer(), Threads.Condition(), nothing, false, ReentrantLock(), StatusActive) end -isopen(s::BufferStream) = s.is_open +isopen(s::BufferStream) = s.status != StatusClosed + +closewrite(s::BufferStream) = close(s) function close(s::BufferStream) lock(s.cond) do - s.is_open = false + s.status = StatusClosed notify(s.cond) nothing end end uvfinalize(s::BufferStream) = nothing +setup_stdio(stream::BufferStream, child_readable::Bool) = invoke(setup_stdio, Tuple{IO, Bool}, stream, child_readable) function read(s::BufferStream, ::Type{UInt8}) nread = lock(s.cond) do @@ -1314,8 +1509,8 @@ function unsafe_read(s::BufferStream, a::Ptr{UInt8}, nb::UInt) end bytesavailable(s::BufferStream) = bytesavailable(s.buffer) -isreadable(s::BufferStream) = s.buffer.readable -iswritable(s::BufferStream) = s.buffer.writable +isreadable(s::BufferStream) = (isopen(s) || bytesavailable(s) > 0) && s.buffer.readable +iswritable(s::BufferStream) = isopen(s) && s.buffer.writable function wait_readnb(s::BufferStream, nb::Int) lock(s.cond) do @@ -1325,7 +1520,7 @@ function wait_readnb(s::BufferStream, nb::Int) end end -show(io::IO, s::BufferStream) = print(io, "BufferStream() bytes waiting:", bytesavailable(s.buffer), ", isopen:", s.is_open) +show(io::IO, s::BufferStream) = print(io, "BufferStream(bytes waiting=", bytesavailable(s.buffer), ", isopen=", isopen(s), ")") function readuntil(s::BufferStream, c::UInt8; keep::Bool=false) bytes = lock(s.cond) do @@ -1375,3 +1570,5 @@ function flush(s::BufferStream) nothing end end + +skip(s::BufferStream, n) = skip(s.buffer, n) diff --git a/base/strings/basic.jl b/base/strings/basic.jl index 82038d78635ac5..306ecc5cc214a7 100644 --- a/base/strings/basic.jl +++ b/base/strings/basic.jl @@ -16,7 +16,7 @@ about strings: * Each `AbstractChar` in a string is encoded by one or more code units * Only the index of the first code unit of an `AbstractChar` is a valid index * The encoding of an `AbstractChar` is independent of what precedes or follows it - * String encodings are [self-synchronizing] – i.e. `isvalid(s, i)` is O(1) + * String encodings are [self-synchronizing] – i.e. `isvalid(s, i)` is O(1) [self-synchronizing]: https://en.wikipedia.org/wiki/Self-synchronizing_code @@ -35,8 +35,8 @@ model allows index arithmetic to work with out-of- bounds indices as intermediate values so long as one never uses them to retrieve a character, which often helps avoid needing to code around edge cases. -See also: [`codeunit`](@ref), [`ncodeunits`](@ref), [`thisind`](@ref), -[`nextind`](@ref), [`prevind`](@ref) +See also [`codeunit`](@ref), [`ncodeunits`](@ref), [`thisind`](@ref), +[`nextind`](@ref), [`prevind`](@ref). """ AbstractString @@ -46,8 +46,8 @@ AbstractString ncodeunits(s::AbstractString) -> Int Return the number of code units in a string. Indices that are in bounds to -access this string must satisfy `1 ≤ i ≤ ncodeunits(s)`. Not all such indices -are valid – they may not be the start of a character, but they will return a +access this string must satisfy `1 ≤ i ≤ ncodeunits(s)`. Not all such indices +are valid – they may not be the start of a character, but they will return a code unit value when calling `codeunit(s,i)`. # Examples @@ -62,8 +62,8 @@ julia> ncodeunits('∫'), ncodeunits('e'), ncodeunits('ˣ') (3, 1, 2) ``` -See also: [`codeunit`](@ref), [`checkbounds`](@ref), [`sizeof`](@ref), -[`length`](@ref), [`lastindex`](@ref) +See also [`codeunit`](@ref), [`checkbounds`](@ref), [`sizeof`](@ref), +[`length`](@ref), [`lastindex`](@ref). """ ncodeunits(s::AbstractString) @@ -77,7 +77,7 @@ limited to these three types, but it's hard to think of widely used string encodings that don't use one of these units. `codeunit(s)` is the same as `typeof(codeunit(s,1))` when `s` is a non-empty string. -See also: [`ncodeunits`](@ref) +See also [`ncodeunits`](@ref). """ codeunit(s::AbstractString) @@ -102,9 +102,9 @@ julia> typeof(a) UInt8 ``` -See also: [`ncodeunits`](@ref), [`checkbounds`](@ref) +See also [`ncodeunits`](@ref), [`checkbounds`](@ref). """ -@propagate_inbounds codeunit(s::AbstractString, i::Integer) = typeof(i) === Int ? +@propagate_inbounds codeunit(s::AbstractString, i::Integer) = i isa Int ? throw(MethodError(codeunit, (s, i))) : codeunit(s, Int(i)) """ @@ -118,8 +118,8 @@ In order for `isvalid(s, i)` to be an O(1) function, the encoding of `s` must be [self-synchronizing](https://en.wikipedia.org/wiki/Self-synchronizing_code). This is a basic assumption of Julia's generic string support. -See also: [`getindex`](@ref), [`iterate`](@ref), [`thisind`](@ref), -[`nextind`](@ref), [`prevind`](@ref), [`length`](@ref) +See also [`getindex`](@ref), [`iterate`](@ref), [`thisind`](@ref), +[`nextind`](@ref), [`prevind`](@ref), [`length`](@ref). # Examples ```jldoctest @@ -140,7 +140,7 @@ Stacktrace: [...] ``` """ -@propagate_inbounds isvalid(s::AbstractString, i::Integer) = typeof(i) === Int ? +@propagate_inbounds isvalid(s::AbstractString, i::Integer) = i isa Int ? throw(MethodError(isvalid, (s, i))) : isvalid(s, Int(i)) """ @@ -152,9 +152,9 @@ be iterated, yielding a sequences of characters. If `i` is out of bounds in `s` then a bounds error is raised. The `iterate` function, as part of the iteration protocol may assume that `i` is the start of a character in `s`. -See also: [`getindex`](@ref), [`checkbounds`](@ref) +See also [`getindex`](@ref), [`checkbounds`](@ref). """ -@propagate_inbounds iterate(s::AbstractString, i::Integer) = typeof(i) === Int ? +@propagate_inbounds iterate(s::AbstractString, i::Integer) = i isa Int ? throw(MethodError(iterate, (s, i))) : iterate(s, Int(i)) ## basic generic definitions ## @@ -183,7 +183,7 @@ isempty(s::AbstractString) = iszero(ncodeunits(s)::Int) function getindex(s::AbstractString, i::Integer) @boundscheck checkbounds(s, i) - @inbounds return isvalid(s, i) ? iterate(s, i)[1] : string_index_err(s, i) + @inbounds return isvalid(s, i) ? (iterate(s, i)::NTuple{2,Any})[1] : string_index_err(s, i) end getindex(s::AbstractString, i::Colon) = s @@ -375,8 +375,8 @@ value `0`. the string because it counts the value on the fly. This is in contrast to the method for arrays, which is a constant-time operation. -See also: [`isvalid`](@ref), [`ncodeunits`](@ref), [`lastindex`](@ref), -[`thisind`](@ref), [`nextind`](@ref), [`prevind`](@ref) +See also [`isvalid`](@ref), [`ncodeunits`](@ref), [`lastindex`](@ref), +[`thisind`](@ref), [`nextind`](@ref), [`prevind`](@ref). # Examples ```jldoctest @@ -389,7 +389,7 @@ length(s::AbstractString) = @inbounds return length(s, 1, ncodeunits(s)::Int) function length(s::AbstractString, i::Int, j::Int) @boundscheck begin 0 < i ≤ ncodeunits(s)::Int+1 || throw(BoundsError(s, i)) - 0 ≤ j < ncodeunits(s)::Int+1 || throw(BoundsError(s, j)) + 0 ≤ j < ncodeunits(s)::Int+1 || throw(BoundsError(s, j)) end n = 0 for k = i:j @@ -438,8 +438,8 @@ thisind(s::AbstractString, i::Integer) = thisind(s, Int(i)) function thisind(s::AbstractString, i::Int) z = ncodeunits(s)::Int + 1 i == z && return i - @boundscheck 0 ≤ i ≤ z || throw(BoundsError(s, i)) - @inbounds while 1 < i && !(isvalid(s, i)::Bool) + @boundscheck 0 ≤ i ≤ z || throw(BoundsError(s, i)) + @inbounds while 1 < i && !(isvalid(s, i)::Bool) i -= 1 end return i @@ -498,7 +498,7 @@ function prevind(s::AbstractString, i::Int, n::Int) z = ncodeunits(s) + 1 @boundscheck 0 < i ≤ z || throw(BoundsError(s, i)) n == 0 && return thisind(s, i) == i ? i : string_index_err(s, i) - while n > 0 && 1 < i + while n > 0 && 1 < i @inbounds n -= isvalid(s, i -= 1) end return i - n @@ -557,7 +557,7 @@ function nextind(s::AbstractString, i::Int, n::Int) z = ncodeunits(s) @boundscheck 0 ≤ i ≤ z || throw(BoundsError(s, i)) n == 0 && return thisind(s, i) == i ? i : string_index_err(s, i) - while n > 0 && i < z + while n > 0 && i < z @inbounds n -= isvalid(s, i += 1) end return i + n @@ -596,6 +596,15 @@ true julia> isascii("αβγ") false ``` +For example, `isascii` can be used as a predicate function for [`filter`](@ref) or [`replace`](@ref) +to remove or replace non-ASCII characters, respectively: +```jldoctest +julia> filter(isascii, "abcdeγfgh") # discard non-ASCII chars +"abcdefgh" + +julia> replace("abcdeγfgh", !isascii=>' ') # replace non-ASCII chars with spaces +"abcde fgh" +``` """ isascii(c::Char) = bswap(reinterpret(UInt32, c)) < 0x80 isascii(s::AbstractString) = all(isascii, s) @@ -676,13 +685,16 @@ cases where `v` contains non-ASCII characters.) # Examples ```jldoctest -julia> r = reverse("Julia") -"ailuJ" +julia> s = "Julia🚀" +"Julia🚀" -julia> for i in 1:length(r) - print(r[reverseind("Julia", i)]) +julia> r = reverse(s) +"🚀ailuJ" + +julia> for i in eachindex(s) + print(r[reverseind(r, i)]) end -Julia +Julia🚀 ``` """ reverseind(s::AbstractString, i::Integer) = thisind(s, ncodeunits(s)-i+1) @@ -692,7 +704,7 @@ reverseind(s::AbstractString, i::Integer) = thisind(s, ncodeunits(s)-i+1) Repeat a string `r` times. This can be written as `s^r`. -See also: [`^`](@ref :^(::Union{AbstractString, AbstractChar}, ::Integer)) +See also [`^`](@ref :^(::Union{AbstractString, AbstractChar}, ::Integer)). # Examples ```jldoctest @@ -707,7 +719,7 @@ repeat(s::AbstractString, r::Integer) = repeat(String(s), r) Repeat a string or character `n` times. This can also be written as `repeat(s, n)`. -See also: [`repeat`](@ref) +See also [`repeat`](@ref). # Examples ```jldoctest @@ -737,7 +749,7 @@ end length(s::CodeUnits) = ncodeunits(s.s) sizeof(s::CodeUnits{T}) where {T} = ncodeunits(s.s) * sizeof(T) size(s::CodeUnits) = (length(s),) -elsize(s::CodeUnits{T}) where {T} = sizeof(T) +elsize(s::Type{<:CodeUnits{T}}) where {T} = sizeof(T) @propagate_inbounds getindex(s::CodeUnits, i::Int) = codeunit(s.s, i) IndexStyle(::Type{<:CodeUnits}) = IndexLinear() @inline iterate(s::CodeUnits, i=1) = (i % UInt) - 1 < length(s) ? (@inbounds s[i], i + 1) : nothing @@ -768,3 +780,16 @@ julia> codeunits("Juλia") ``` """ codeunits(s::AbstractString) = CodeUnits(s) + +function _split_rest(s::AbstractString, n::Int) + lastind = lastindex(s) + i = try + prevind(s, lastind, n) + catch e + e isa BoundsError || rethrow() + _check_length_split_rest(length(s), n) + end + last_n = SubString(s, nextind(s, i), lastind) + front = s[begin:i] + return front, last_n +end diff --git a/base/strings/io.jl b/base/strings/io.jl index f69834e63e24dc..d1bf7a763e93ce 100644 --- a/base/strings/io.jl +++ b/base/strings/io.jl @@ -15,7 +15,7 @@ avoid Julia-specific details. For example, `show` displays strings with quotes, and `print` displays strings without quotes. -[`string`](@ref) returns the output of `print` as a string. +See also [`println`](@ref), [`string`](@ref), [`printstyled`](@ref). # Examples ```jldoctest @@ -54,8 +54,10 @@ end """ println([io::IO], xs...) -Print (using [`print`](@ref)) `xs` followed by a newline. -If `io` is not supplied, prints to [`stdout`](@ref). +Print (using [`print`](@ref)) `xs` to `io` followed by a newline. +If `io` is not supplied, prints to the default output stream [`stdout`](@ref). + +See also [`printstyled`](@ref) to add colors etc. # Examples ```jldoctest @@ -64,13 +66,13 @@ Hello, world julia> io = IOBuffer(); -julia> println(io, "Hello, world") +julia> println(io, "Hello", ',', " world.") julia> String(take!(io)) -"Hello, world\\n" +"Hello, world.\\n" ``` """ -println(io::IO, xs...) = print(io, xs..., '\n') +println(io::IO, xs...) = print(io, xs..., "\n") ## conversion of general objects to strings ## @@ -79,14 +81,19 @@ println(io::IO, xs...) = print(io, xs..., '\n') Call the given function with an I/O stream and the supplied extra arguments. Everything written to this I/O stream is returned as a string. -`context` can be either an [`IOContext`](@ref) whose properties will be used, -or a `Pair` specifying a property and its value. `sizehint` suggests the capacity -of the buffer (in bytes). +`context` can be an [`IOContext`](@ref) whose properties will be used, a `Pair` +specifying a property and its value, or a tuple of `Pair` specifying multiple +properties and their values. `sizehint` suggests the capacity of the buffer (in +bytes). + +The optional keyword argument `context` can be set to a `:key=>value` pair, a +tuple of `:key=>value` pairs, or an `IO` or [`IOContext`](@ref) object whose +attributes are used for the I/O stream passed to `f`. The optional `sizehint` +is a suggested size (in bytes) to allocate for the buffer used to write the +string. -The optional keyword argument `context` can be set to `:key=>value` pair -or an `IO` or [`IOContext`](@ref) object whose attributes are used for the I/O -stream passed to `f`. The optional `sizehint` is a suggested size (in bytes) -to allocate for the buffer used to write the string. +!!! compat "Julia 1.7" + Passing a tuple to keyword `context` requires Julia 1.7 or later. # Examples ```jldoctest @@ -99,7 +106,9 @@ julia> sprint(showerror, BoundsError([1], 100)) """ function sprint(f::Function, args...; context=nothing, sizehint::Integer=0) s = IOBuffer(sizehint=sizehint) - if context !== nothing + if context isa Tuple + f(IOContext(s, context...), args...) + elseif context !== nothing f(IOContext(s, context), args...) else f(s, args...) @@ -165,6 +174,8 @@ highly efficient, then it may make sense to add a method to `string` and define `print(io::IO, x::MyType) = print(io, string(x))` to ensure the functions are consistent. +See also: [`String`](@ref), [`repr`](@ref), [`sprint`](@ref), [`show`](@ref @show). + # Examples ```jldoctest julia> string("a", 1, true) @@ -181,35 +192,77 @@ print(io::IO, s::AbstractString) = for c in s; print(io, c); end write(io::IO, s::AbstractString) = (len = 0; for c in s; len += Int(write(io, c))::Int; end; len) show(io::IO, s::AbstractString) = print_quoted(io, s) +# show elided string if more than `limit` characters +function show( + io :: IO, + mime :: MIME"text/plain", + str :: AbstractString; + limit :: Union{Int, Nothing} = nothing, +) + # compute limit in default case + if limit === nothing + get(io, :limit, false) || return show(io, str) + limit = max(20, displaysize(io)[2]) + # one line in collection, seven otherwise + get(io, :typeinfo, nothing) === nothing && (limit *= 7) + end + + # early out for short strings + len = ncodeunits(str) + len ≤ limit - 2 && # quote chars + return show(io, str) + + # these don't depend on string data + units = codeunit(str) == UInt8 ? "bytes" : "code units" + skip_text(skip) = " ⋯ $skip $units ⋯ " + short = length(skip_text("")) + 4 # quote chars + chars = max(limit, short + 1) - short # at least 1 digit + + # figure out how many characters to print in elided case + chars -= d = ndigits(len - chars) # first adjustment + chars += d - ndigits(len - chars) # second if needed + chars = max(0, chars) + + # find head & tail, avoiding O(length(str)) computation + head = nextind(str, 0, 1 + (chars + 1) ÷ 2) + tail = prevind(str, len + 1, chars ÷ 2) + + # threshold: min chars skipped to make elision worthwhile + t = short + ndigits(len - chars) - 1 + n = tail - head # skipped code units + if 4t ≤ n || t ≤ n && t ≤ length(str, head, tail-1) + skip = skip_text(n) + show(io, SubString(str, 1:prevind(str, head))) + print(io, skip) # TODO: bold styled + show(io, SubString(str, tail)) + else + show(io, str) + end +end + # optimized methods to avoid iterating over chars write(io::IO, s::Union{String,SubString{String}}) = GC.@preserve s Int(unsafe_write(io, pointer(s), reinterpret(UInt, sizeof(s))))::Int print(io::IO, s::Union{String,SubString{String}}) = (write(io, s); nothing) -## printing literal quoted string data ## - -# this is the inverse of print_unescaped_chars(io, s, "\\\") - -function print_quoted_literal(io, s::AbstractString) - print(io, '"') - for c = s; c == '"' ? print(io, "\\\"") : print(io, c); end - print(io, '"') -end - """ repr(x; context=nothing) Create a string from any value using the [`show`](@ref) function. You should not add methods to `repr`; define a `show` method instead. -The optional keyword argument `context` can be set to an `IO` or [`IOContext`](@ref) -object whose attributes are used for the I/O stream passed to `show`. +The optional keyword argument `context` can be set to a `:key=>value` pair, a +tuple of `:key=>value` pairs, or an `IO` or [`IOContext`](@ref) object whose +attributes are used for the I/O stream passed to `show`. Note that `repr(x)` is usually similar to how the value of `x` would be entered in Julia. See also [`repr(MIME("text/plain"), x)`](@ref) to instead return a "pretty-printed" version of `x` designed more for human consumption, equivalent to the REPL display of `x`. +!!! compat "Julia 1.7" + Passing a tuple to keyword `context` requires Julia 1.7 or later. + # Examples ```jldoctest julia> repr(1) @@ -254,15 +307,12 @@ IOBuffer(s::SubString{String}) = IOBuffer(view(unsafe_wrap(Vector{UInt8}, s.stri # join is implemented using IO """ - join([io::IO,] strings [, delim [, last]]) + join([io::IO,] iterator [, delim [, last]]) -Join an array of `strings` into a single string, inserting the given delimiter (if any) between -adjacent strings. If `last` is given, it will be used instead of `delim` between the last -two strings. If `io` is given, the result is written to `io` rather than returned -as a `String`. - -`strings` can be any iterable over elements `x` which are convertible to strings -via `print(io::IOBuffer, x)`. `strings` will be printed to `io`. +Join any `iterator` into a single string, inserting the given delimiter (if any) between +adjacent items. If `last` is given, it will be used instead of `delim` between the last +two items. Each item of `iterator` is converted to a string via `print(io::IOBuffer, x)`. +If `io` is given, the result is written to `io` rather than returned as a `String`. # Examples ```jldoctest @@ -273,15 +323,15 @@ julia> join([1,2,3,4,5]) "12345" ``` """ -function join(io::IO, strings, delim, last) +function join(io::IO, iterator, delim, last) first = true local prev - for str in strings + for item in iterator if @isdefined prev first ? (first = false) : print(io, delim) print(io, prev) end - prev = str + prev = item end if @isdefined prev first || print(io, last) @@ -289,19 +339,19 @@ function join(io::IO, strings, delim, last) end nothing end -function join(io::IO, strings, delim="") +function join(io::IO, iterator, delim="") # Specialization of the above code when delim==last, # which lets us emit (compile) less code first = true - for str in strings + for item in iterator first ? (first = false) : print(io, delim) - print(io, str) + print(io, item) end end -join(strings) = sprint(join, strings) -join(strings, delim) = sprint(join, strings, delim) -join(strings, delim, last) = sprint(join, strings, delim, last) +join(iterator) = sprint(join, iterator) +join(iterator, delim) = sprint(join, iterator, delim) +join(iterator, delim, last) = sprint(join, iterator, delim, last) ## string escaping & unescaping ## @@ -310,8 +360,8 @@ escape_nul(c::Union{Nothing, AbstractChar}) = (c !== nothing && '0' <= c <= '7') ? "\\x00" : "\\0" """ - escape_string(str::AbstractString[, esc])::AbstractString - escape_string(io, str::AbstractString[, esc::])::Nothing + escape_string(str::AbstractString[, esc]; keep = ())::AbstractString + escape_string(io, str::AbstractString[, esc]; keep = ())::Nothing General escaping of traditional C and Unicode escape sequences. The first form returns the escaped string, the second prints the result to `io`. @@ -323,11 +373,22 @@ unambiguous), unicode code point (`"\\u"` prefix) or hex (`"\\x"` prefix). The optional `esc` argument specifies any additional characters that should also be escaped by a prepending backslash (`\"` is also escaped by default in the first form). +The argument `keep` specifies a collection of characters which are to be kept as +they are. Notice that `esc` has precedence here. + +See also [`unescape_string`](@ref) for the reverse operation. + +!!! compat "Julia 1.7" + The `keep` argument is available as of Julia 1.7. + # Examples ```jldoctest julia> escape_string("aaa\\nbbb") "aaa\\\\nbbb" +julia> escape_string("aaa\\nbbb"; keep = '\\n') +"aaa\\nbbb" + julia> escape_string("\\xfe\\xff") # invalid utf-8 "\\\\xfe\\\\xff" @@ -337,15 +398,14 @@ julia> escape_string(string('\\u2135','\\0')) # unambiguous julia> escape_string(string('\\u2135','\\0','0')) # \\0 would be ambiguous "ℵ\\\\x000" ``` - -## See also -[`unescape_string`](@ref) for the reverse operation. """ -function escape_string(io::IO, s::AbstractString, esc="") +function escape_string(io::IO, s::AbstractString, esc=""; keep = ()) a = Iterators.Stateful(s) for c::AbstractChar in a if c in esc print(io, '\\', c) + elseif c in keep + print(io, c) elseif isascii(c) c == '\0' ? print(io, escape_nul(peek(a)::Union{AbstractChar,Nothing})) : c == '\e' ? print(io, "\\e") : @@ -368,7 +428,8 @@ function escape_string(io::IO, s::AbstractString, esc="") end end -escape_string(s::AbstractString, esc=('\"',)) = sprint(escape_string, s, esc, sizehint=lastindex(s)) +escape_string(s::AbstractString, esc=('\"',); keep = ()) = + sprint((io)->escape_string(io, s, esc; keep = keep), sizehint=lastindex(s)) function print_quoted(io, s::AbstractString) print(io, '"') @@ -397,6 +458,8 @@ The following escape sequences are recognised: - Hex bytes (`\\x` with 1-2 trailing hex digits) - Octal bytes (`\\` with 1-3 trailing octal digits) +See also [`escape_string`](@ref). + # Examples ```jldoctest julia> unescape_string("aaa\\\\nbbb") # C escape sequence @@ -411,9 +474,6 @@ julia> unescape_string("\\\\101") # octal julia> unescape_string("aaa \\\\g \\\\n", ['g']) # using `keep` argument "aaa \\\\g \\n" ``` - -## See also -[`escape_string`](@ref). """ function unescape_string(io::IO, s::AbstractString, keep = ()) a = Iterators.Stateful(s) @@ -427,7 +487,7 @@ function unescape_string(io::IO, s::AbstractString, keep = ()) m = c == 'x' ? 2 : c == 'u' ? 4 : 8 while (k += 1) <= m && !isempty(a) - nc = peek(a) + nc = peek(a)::AbstractChar n = '0' <= nc <= '9' ? n<<4 + (nc-'0') : 'a' <= nc <= 'f' ? n<<4 + (nc-'a'+10) : 'A' <= nc <= 'F' ? n<<4 + (nc-'A'+10) : break @@ -447,7 +507,7 @@ function unescape_string(io::IO, s::AbstractString, keep = ()) k = 1 n = c-'0' while (k += 1) <= 3 && !isempty(a) - c = peek(a) + c = peek(a)::AbstractChar n = ('0' <= c <= '7') ? n<<3 + c-'0' : break popfirst!(a) end @@ -531,7 +591,7 @@ macro raw_str(s); s; end Escape a string in the manner used for parsing raw string literals. For each double-quote (`"`) character in input string `s`, this -function counts the number _n_ of preceeding backslash (`\\`) characters, +function counts the number _n_ of preceding backslash (`\\`) characters, and then increases there the number of backslashes from _n_ to 2_n_+1 (even for _n_ = 0). It also doubles a sequence of backslashes at the end of the string. @@ -541,7 +601,7 @@ string literals. (It also happens to be the escaping convention expected by the Microsoft C/C++ compiler runtime when it parses a command-line string into the argv[] array.) -See also: [`escape_string`](@ref) +See also [`escape_string`](@ref). """ function escape_raw_string(io, str::AbstractString) escapes = 0 @@ -613,6 +673,8 @@ end Remove leading indentation from string. +See also `indent` from the [`MultilineStrings` package](https://github.com/invenia/MultilineStrings.jl). + # Examples ```jldoctest julia> Base.unindent(" a\\n b", 2) diff --git a/base/strings/lazy.jl b/base/strings/lazy.jl new file mode 100644 index 00000000000000..3510afc9b4f116 --- /dev/null +++ b/base/strings/lazy.jl @@ -0,0 +1,101 @@ +""" + LazyString <: AbstractString + +A lazy representation of string interpolation. This is useful when a string +needs to be constructed in a context where performing the actual interpolation +and string construction is unnecessary or undesirable (e.g. in error paths +of functions). + +This type is designed to be cheap to construct at runtime, trying to offload +as much work as possible to either the macro or later printing operations. + +# Examples + +```jldoctest +julia> n = 5; str = LazyString("n is ", n) +"n is 5" +``` + +See also [`@lazy_str`](@ref). + +!!! compat "Julia 1.8" + `LazyString` requires Julia 1.8 or later. + +# Extended help +## Safety properties for concurrent programs + +A lazy string itself does not introduce any concurrency problems even if it is printed in +multiple Julia tasks. However, if `print` methods on a captured value can have a +concurrency issue when invoked without synchronizations, printing the lazy string may cause +an issue. Furthermore, the `print` methods on the captured values may be invoked multiple +times, though only exactly one result will be returned. + +!!! compat "Julia 1.9" + `LazyString` is safe in the above sense in Julia 1.9 and later. +""" +mutable struct LazyString <: AbstractString + const parts::Tuple + # Created on first access + @atomic str::Union{String,Nothing} + global _LazyString(parts, str) = new(parts, str) + LazyString(args...) = new(args, nothing) +end + +""" + lazy"str" + +Create a [`LazyString`](@ref) using regular string interpolation syntax. +Note that interpolations are *evaluated* at LazyString construction time, +but *printing* is delayed until the first access to the string. + +See [`LazyString`](@ref) documentation for the safety properties for concurrent programs. + +# Examples + +``` +julia> n = 5; str = lazy"n is \$n" +"n is 5" + +julia> typeof(str) +LazyString +``` + +!!! compat "Julia 1.8" + `lazy"str"` requires Julia 1.8 or later. +""" +macro lazy_str(text) + parts = Any[] + lastidx = idx = 1 + while (idx = findnext('$', text, idx)) !== nothing + lastidx < idx && push!(parts, text[lastidx:idx-1]) + idx += 1 + expr, idx = Meta.parseatom(text, idx; filename=string(__source__.file)) + push!(parts, esc(expr)) + lastidx = idx + end + lastidx <= lastindex(text) && push!(parts, text[lastidx:end]) + :(LazyString($(parts...))) +end + +function String(l::LazyString) + old = @atomic :acquire l.str + old === nothing || return old + str = sprint() do io + for p in l.parts + print(io, p) + end + end + old, ok = @atomicreplace :acquire_release :acquire l.str nothing => str + return ok ? str : (old::String) +end + +hash(s::LazyString, h::UInt64) = hash(String(s), h) +lastindex(s::LazyString) = lastindex(String(s)) +iterate(s::LazyString) = iterate(String(s)) +iterate(s::LazyString, i::Integer) = iterate(String(s), i) +isequal(a::LazyString, b::LazyString) = isequal(String(a), String(b)) +==(a::LazyString, b::LazyString) = (String(a) == String(b)) +ncodeunits(s::LazyString) = ncodeunits(String(s)) +codeunit(s::LazyString) = codeunit(String(s)) +codeunit(s::LazyString, i::Integer) = codeunit(String(s), i) +isvalid(s::LazyString, i::Integer) = isvalid(String(s), i) diff --git a/base/strings/search.jl b/base/strings/search.jl index b1908ac99c8600..938ed8d527d997 100644 --- a/base/strings/search.jl +++ b/base/strings/search.jl @@ -25,6 +25,9 @@ findfirst(pred::Fix2{<:Union{typeof(isequal),typeof(==)},<:Union{Int8,UInt8}}, a findnext(pred::Fix2{<:Union{typeof(isequal),typeof(==)},<:Union{Int8,UInt8}}, a::ByteArray, i::Integer) = nothing_sentinel(_search(a, pred.x, i)) +findfirst(::typeof(iszero), a::ByteArray) = nothing_sentinel(_search(a, zero(UInt8))) +findnext(::typeof(iszero), a::ByteArray, i::Integer) = nothing_sentinel(_search(a, zero(UInt8), i)) + function _search(a::Union{String,ByteArray}, b::Union{Int8,UInt8}, i::Integer = 1) if i < 1 throw(BoundsError(a, i)) @@ -65,6 +68,9 @@ findlast(pred::Fix2{<:Union{typeof(isequal),typeof(==)},<:Union{Int8,UInt8}}, a: findprev(pred::Fix2{<:Union{typeof(isequal),typeof(==)},<:Union{Int8,UInt8}}, a::ByteArray, i::Integer) = nothing_sentinel(_rsearch(a, pred.x, i)) +findlast(::typeof(iszero), a::ByteArray) = nothing_sentinel(_rsearch(a, zero(UInt8))) +findprev(::typeof(iszero), a::ByteArray, i::Integer) = nothing_sentinel(_rsearch(a, zero(UInt8), i)) + function _rsearch(a::Union{String,ByteArray}, b::Union{Int8,UInt8}, i::Integer = sizeof(a)) if i < 1 return i == 0 ? 0 : throw(BoundsError(a, i)) @@ -88,7 +94,7 @@ end """ findfirst(pattern::AbstractString, string::AbstractString) - findfirst(pattern::Regex, string::String) + findfirst(pattern::AbstractPattern, string::String) Find the first occurrence of `pattern` in `string`. Equivalent to [`findnext(pattern, string, firstindex(s))`](@ref). @@ -123,6 +129,25 @@ true """ findfirst(ch::AbstractChar, string::AbstractString) = findfirst(==(ch), string) +""" + findfirst(pattern::AbstractVector{<:Union{Int8,UInt8}}, + A::AbstractVector{<:Union{Int8,UInt8}}) + +Find the first occurrence of sequence `pattern` in vector `A`. + +!!! compat "Julia 1.6" + This method requires at least Julia 1.6. + +# Examples +```jldoctest +julia> findfirst([0x52, 0x62], [0x40, 0x52, 0x62, 0x63]) +2:3 +``` +""" +findfirst(pattern::AbstractVector{<:Union{Int8,UInt8}}, + A::AbstractVector{<:Union{Int8,UInt8}}) = + _search(A, pattern, firstindex(A)) + # AbstractString implementation of the generic findnext interface function findnext(testf::Function, s::AbstractString, i::Integer) i = Int(i) @@ -143,11 +168,12 @@ in(c::AbstractChar, s::AbstractString) = (findfirst(isequal(c),s)!==nothing) function _searchindex(s::Union{AbstractString,ByteArray}, t::Union{AbstractString,AbstractChar,Int8,UInt8}, i::Integer) - if isempty(t) + x = Iterators.peel(t) + if isnothing(x) return 1 <= i <= nextind(s,lastindex(s))::Int ? i : throw(BoundsError(s, i)) end - t1, trest = Iterators.peel(t) + t1, trest = x while true i = findnext(isequal(t1),s,i) if i === nothing return 0 end @@ -166,7 +192,7 @@ function _search_bloom_mask(c) end _nthbyte(s::String, i) = codeunit(s, i) -_nthbyte(a::Union{AbstractVector{UInt8},AbstractVector{Int8}}, i) = a[i] +_nthbyte(t::AbstractVector, index) = t[index + (firstindex(t)-1)] function _searchindex(s::String, t::String, i::Integer) # Check for fast case of a single byte @@ -174,21 +200,26 @@ function _searchindex(s::String, t::String, i::Integer) _searchindex(unsafe_wrap(Vector{UInt8},s), unsafe_wrap(Vector{UInt8},t), i) end -function _searchindex(s::ByteArray, t::ByteArray, i::Integer) - n = sizeof(t) - m = sizeof(s) +function _searchindex(s::AbstractVector{<:Union{Int8,UInt8}}, + t::AbstractVector{<:Union{Int8,UInt8}}, + _i::Integer) + sentinel = firstindex(s) - 1 + n = length(t) + m = length(s) + i = Int(_i) - sentinel + (i < 1 || i > m+1) && throw(BoundsError(s, _i)) if n == 0 - return 1 <= i <= m+1 ? max(1, i) : 0 + return 1 <= i <= m+1 ? max(1, i) : sentinel elseif m == 0 - return 0 + return sentinel elseif n == 1 - return something(findnext(isequal(_nthbyte(t,1)), s, i), 0) + return something(findnext(isequal(_nthbyte(t,1)), s, i), sentinel) end w = m - n if w < 0 || i - 1 > w - return 0 + return sentinel end bloom_mask = UInt64(0) @@ -215,7 +246,8 @@ function _searchindex(s::ByteArray, t::ByteArray, i::Integer) # match found if j == n - 1 - return i+1 + # restore in case `s` is an OffSetArray + return i+firstindex(s) end # no match, try to rule out the next character @@ -232,16 +264,16 @@ function _searchindex(s::ByteArray, t::ByteArray, i::Integer) i += 1 end - 0 + sentinel end -function _search(s::Union{AbstractString,ByteArray}, - t::Union{AbstractString,AbstractChar,Int8,UInt8}, +function _search(s::Union{AbstractString,AbstractVector{<:Union{Int8,UInt8}}}, + t::Union{AbstractString,AbstractChar,AbstractVector{<:Union{Int8,UInt8}}}, i::Integer) idx = _searchindex(s,t,i) if isempty(t) idx:idx-1 - elseif idx > 0 + elseif idx >= firstindex(s) idx:(idx + lastindex(t) - 1) else nothing @@ -250,7 +282,7 @@ end """ findnext(pattern::AbstractString, string::AbstractString, start::Integer) - findnext(pattern::Regex, string::String, start::Integer) + findnext(pattern::AbstractPattern, string::String, start::Integer) Find the next occurrence of `pattern` in `string` starting at position `start`. `pattern` can be either a string, or a regular expression, in which case `string` @@ -274,7 +306,7 @@ julia> findnext("Lang", "JuliaLang", 2) 6:9 ``` """ -findnext(t::AbstractString, s::AbstractString, i::Integer) = _search(s, t, Int(i)) +findnext(t::AbstractString, s::AbstractString, start::Integer) = _search(s, t, Int(start)) """ findnext(ch::AbstractChar, string::AbstractString, start::Integer) @@ -293,8 +325,32 @@ julia> findnext('o', "Hello to the world", 6) 8 ``` """ -findnext(ch::AbstractChar, string::AbstractString, ind::Integer) = - findnext(==(ch), string, ind) +findnext(ch::AbstractChar, string::AbstractString, start::Integer) = + findnext(==(ch), string, start) + +""" + findnext(pattern::AbstractVector{<:Union{Int8,UInt8}}, + A::AbstractVector{<:Union{Int8,UInt8}}, + start::Integer) + +Find the next occurrence of the sequence `pattern` in vector `A` starting at position `start`. + +!!! compat "Julia 1.6" + This method requires at least Julia 1.6. + +# Examples +```jldoctest +julia> findnext([0x52, 0x62], [0x52, 0x62, 0x72], 3) === nothing +true + +julia> findnext([0x52, 0x62], [0x40, 0x52, 0x62, 0x52, 0x62], 3) +4:5 +``` +""" +findnext(pattern::AbstractVector{<:Union{Int8,UInt8}}, + A::AbstractVector{<:Union{Int8,UInt8}}, + start::Integer) = + _search(A, pattern, start) """ findlast(pattern::AbstractString, string::AbstractString) @@ -314,6 +370,23 @@ julia> findfirst("Julia", "JuliaLang") findlast(pattern::AbstractString, string::AbstractString) = findprev(pattern, string, lastindex(string)) +""" + findlast(pattern::AbstractVector{<:Union{Int8,UInt8}}, + A::AbstractVector{<:Union{Int8,UInt8}}) + +Find the last occurrence of `pattern` in array `A`. Equivalent to +[`findprev(pattern, A, lastindex(A))`](@ref). + +# Examples +```jldoctest +julia> findlast([0x52, 0x62], [0x52, 0x62, 0x52, 0x62]) +3:4 +``` +""" +findlast(pattern::AbstractVector{<:Union{Int8,UInt8}}, + A::AbstractVector{<:Union{Int8,UInt8}}) = + findprev(pattern, A, lastindex(A)) + """ findlast(ch::AbstractChar, string::AbstractString) @@ -354,7 +427,7 @@ function _rsearchindex(s::AbstractString, return 1 <= i <= nextind(s, lastindex(s))::Int ? i : throw(BoundsError(s, i)) end - t1, trest = Iterators.peel(Iterators.reverse(t)) + t1, trest = Iterators.peel(Iterators.reverse(t))::NTuple{2,Any} while true i = findprev(isequal(t1), s, i) i === nothing && return 0 @@ -387,21 +460,24 @@ function _rsearchindex(s::String, t::String, i::Integer) end end -function _rsearchindex(s::ByteArray, t::ByteArray, k::Integer) - n = sizeof(t) - m = sizeof(s) +function _rsearchindex(s::AbstractVector{<:Union{Int8,UInt8}}, t::AbstractVector{<:Union{Int8,UInt8}}, _k::Integer) + sentinel = firstindex(s) - 1 + n = length(t) + m = length(s) + k = Int(_k) - sentinel + k < 0 && throw(BoundsError(s, _k)) if n == 0 - return 0 <= k <= m ? max(k, 1) : 0 + return 0 <= k <= m ? max(k, 1) : sentinel elseif m == 0 - return 0 + return sentinel elseif n == 1 - return something(findprev(isequal(_nthbyte(t,1)), s, k), 0) + return something(findprev(isequal(_nthbyte(t,1)), s, k), sentinel) end w = m - n if w < 0 || k <= 0 - return 0 + return sentinel end bloom_mask = UInt64(0) @@ -426,9 +502,9 @@ function _rsearchindex(s::ByteArray, t::ByteArray, k::Integer) j += 1 end - # match found + # match found, restore in case `s` is an OffsetArray if j == n - return i + return i + sentinel end # no match, try to rule out the next character @@ -445,16 +521,16 @@ function _rsearchindex(s::ByteArray, t::ByteArray, k::Integer) i -= 1 end - 0 + sentinel end -function _rsearch(s::Union{AbstractString,ByteArray}, - t::Union{AbstractString,AbstractChar,Int8,UInt8}, +function _rsearch(s::Union{AbstractString,AbstractVector{<:Union{Int8,UInt8}}}, + t::Union{AbstractString,AbstractChar,AbstractVector{<:Union{Int8,UInt8}}}, i::Integer) idx = _rsearchindex(s,t,i) if isempty(t) idx:idx-1 - elseif idx > 0 + elseif idx > firstindex(s) - 1 idx:(idx + lastindex(t) - 1) else nothing @@ -503,11 +579,31 @@ julia> findprev('o', "Hello to the world", 18) 15 ``` """ -findprev(ch::AbstractChar, string::AbstractString, ind::Integer) = - findprev(==(ch), string, ind) +findprev(ch::AbstractChar, string::AbstractString, start::Integer) = + findprev(==(ch), string, start) """ - occursin(needle::Union{AbstractString,Regex,AbstractChar}, haystack::AbstractString) + findprev(pattern::AbstractVector{<:Union{Int8,UInt8}}, + A::AbstractVector{<:Union{Int8,UInt8}}, + start::Integer) + +Find the previous occurrence of the sequence `pattern` in vector `A` starting at position `start`. + +!!! compat "Julia 1.6" + This method requires at least Julia 1.6. + +# Examples +```jldoctest +julia> findprev([0x52, 0x62], [0x40, 0x52, 0x62, 0x52, 0x62], 3) +2:3 +``` +""" +findprev(pattern::AbstractVector{<:Union{Int8,UInt8}}, + A::AbstractVector{<:Union{Int8,UInt8}}, + start::Integer) = + _rsearch(A, pattern, start) +""" + occursin(needle::Union{AbstractString,AbstractPattern,AbstractChar}, haystack::AbstractString) Determine whether the first argument is a substring of the second. If `needle` is a regular expression, checks whether `haystack` contains a match. @@ -527,9 +623,22 @@ julia> occursin(r"a.a", "abba") false ``` -See also: [`contains`](@ref). +See also [`contains`](@ref). """ occursin(needle::Union{AbstractString,AbstractChar}, haystack::AbstractString) = _searchindex(haystack, needle, firstindex(haystack)) != 0 -in(::AbstractString, ::AbstractString) = error("use occursin(x, y) for string containment") +""" + occursin(haystack) + +Create a function that checks whether its argument occurs in `haystack`, i.e. +a function equivalent to `needle -> occursin(needle, haystack)`. + +The returned function is of type `Base.Fix2{typeof(occursin)}`. + +!!! compat "Julia 1.6" + This method requires Julia 1.6 or later. +""" +occursin(haystack) = Base.Fix2(occursin, haystack) + +in(::AbstractString, ::AbstractString) = error("use occursin(needle, haystack) for string containment") diff --git a/base/strings/string.jl b/base/strings/string.jl index 1ebb85ff78dd22..e44746f9834d93 100644 --- a/base/strings/string.jl +++ b/base/strings/string.jl @@ -25,10 +25,22 @@ function Base.showerror(io::IO, exc::StringIndexError) end end -const ByteArray = Union{Vector{UInt8},Vector{Int8}} +const ByteArray = Union{CodeUnits{UInt8,String}, Vector{UInt8},Vector{Int8}, FastContiguousSubArray{UInt8,1,CodeUnits{UInt8,String}}, FastContiguousSubArray{UInt8,1,Vector{UInt8}}, FastContiguousSubArray{Int8,1,Vector{Int8}}} @inline between(b::T, lo::T, hi::T) where {T<:Integer} = (lo ≤ b) & (b ≤ hi) +""" + String <: AbstractString + +The default string type in Julia, used by e.g. string literals. + +`String`s are immutable sequences of `Char`s. A `String` is stored internally as +a contiguous byte array, and while they are interpreted as being UTF-8 encoded, +they can be composed of any byte sequence. Use [`isvalid`](@ref) to validate +that the underlying byte sequence is valid as UTF-8. +""" +String + ## constructors and conversions ## # String constructor docstring from boot.jl, workaround for #16730 @@ -36,10 +48,11 @@ const ByteArray = Union{Vector{UInt8},Vector{Int8}} """ String(v::AbstractVector{UInt8}) -Create a new `String` object from a byte vector `v` containing UTF-8 encoded -characters. If `v` is `Vector{UInt8}` it will be truncated to zero length and -future modification of `v` cannot affect the contents of the resulting string. -To avoid truncation use `String(copy(v))`. +Create a new `String` object using the data buffer from byte vector `v`. +If `v` is a `Vector{UInt8}` it will be truncated to zero length and future +modification of `v` cannot affect the contents of the resulting string. +To avoid truncation of `Vector{UInt8}` data, use `String(copy(v))`; for other +`AbstractVector` types, `String(v)` already makes a copy. When possible, the memory of `v` will be used without copying when the `String` object is created. This is guaranteed to be the case for byte vectors returned @@ -70,16 +83,17 @@ function unsafe_string(p::Union{Ptr{UInt8},Ptr{Int8}}) ccall(:jl_cstr_to_string, Ref{String}, (Ptr{UInt8},), p) end -_string_n(n::Integer) = ccall(:jl_alloc_string, Ref{String}, (Csize_t,), n) +# This is @assume_effects :effect_free :nothrow :terminates_globally @ccall jl_alloc_string(n::Csize_t)::Ref{String}, +# but the macro is not available at this time in bootstrap, so we write it manually. +@eval _string_n(n::Integer) = $(Expr(:foreigncall, QuoteNode(:jl_alloc_string), Ref{String}, Expr(:call, Expr(:core, :svec), :Csize_t), 1, QuoteNode((:ccall,0xe)), :(convert(Csize_t, n)))) """ String(s::AbstractString) -Convert a string to a contiguous byte array representation encoded as UTF-8 bytes. -This representation is often appropriate for passing strings to C. +Create a new `String` from an existing `AbstractString`. """ String(s::AbstractString) = print_to_string(s) -@pure String(s::Symbol) = unsafe_string(unsafe_convert(Ptr{UInt8}, s)) +@assume_effects :total String(s::Symbol) = unsafe_string(unsafe_convert(Ptr{UInt8}, s)) unsafe_wrap(::Type{Vector{UInt8}}, s::String) = ccall(:jl_string_to_array, Ref{Vector{UInt8}}, (Any,), s) @@ -92,9 +106,9 @@ String(s::CodeUnits{UInt8,String}) = s.s ## low-level functions ## pointer(s::String) = unsafe_convert(Ptr{UInt8}, s) -pointer(s::String, i::Integer) = pointer(s)+(i-1) +pointer(s::String, i::Integer) = pointer(s) + Int(i)::Int - 1 -@pure ncodeunits(s::String) = Core.sizeof(s) +ncodeunits(s::String) = Core.sizeof(s) codeunit(s::String) = UInt8 @inline function codeunit(s::String, i::Integer) @@ -233,7 +247,7 @@ function getindex_continued(s::String, i::Int, u::UInt32) end n = ncodeunits(s) - (i += 1) > n && @goto ret + (i += 1) > n && @goto ret @inbounds b = codeunit(s, i) # cont byte 1 b & 0xc0 == 0x80 || @goto ret u |= UInt32(b) << 16 @@ -251,7 +265,7 @@ function getindex_continued(s::String, i::Int, u::UInt32) return reinterpret(Char, u) end -getindex(s::String, r::UnitRange{<:Integer}) = s[Int(first(r)):Int(last(r))] +getindex(s::String, r::AbstractUnitRange{<:Integer}) = s[Int(first(r)):Int(last(r))] @inline function getindex(s::String, r::UnitRange{Int}) isempty(r) && return "" @@ -273,7 +287,7 @@ length(s::String) = length_continued(s, 1, ncodeunits(s), ncodeunits(s)) @inline function length(s::String, i::Int, j::Int) @boundscheck begin 0 < i ≤ ncodeunits(s)+1 || throw(BoundsError(s, i)) - 0 ≤ j < ncodeunits(s)+1 || throw(BoundsError(s, j)) + 0 ≤ j < ncodeunits(s)+1 || throw(BoundsError(s, j)) end j < i && return 0 @inbounds i, k = thisind(s, i), i @@ -286,8 +300,8 @@ end @inbounds b = codeunit(s, i) @inbounds while true while true - (i += 1) ≤ n || return c - 0xc0 ≤ b ≤ 0xf7 && break + (i += 1) ≤ n || return c + 0xc0 ≤ b ≤ 0xf7 && break b = codeunit(s, i) end l = b @@ -295,12 +309,12 @@ end c -= (x = b & 0xc0 == 0x80) x & (l ≥ 0xe0) || continue - (i += 1) ≤ n || return c + (i += 1) ≤ n || return c b = codeunit(s, i) # cont byte 2 c -= (x = b & 0xc0 == 0x80) x & (l ≥ 0xf0) || continue - (i += 1) ≤ n || return c + (i += 1) ≤ n || return c b = codeunit(s, i) # cont byte 3 c -= (b & 0xc0 == 0x80) end diff --git a/base/strings/substring.jl b/base/strings/substring.jl index 3e99cc7477446d..b8a0de19483269 100644 --- a/base/strings/substring.jl +++ b/base/strings/substring.jl @@ -7,6 +7,9 @@ Like [`getindex`](@ref), but returns a view into the parent string `s` within range `i:j` or `r` respectively instead of making a copy. +The [`@views`](@ref) macro converts any string slices `s[i:j]` into +substrings `SubString(s, i, j)` in a block of code. + # Examples ```jldoctest julia> SubString("abc", 1, 2) @@ -25,7 +28,7 @@ struct SubString{T<:AbstractString} <: AbstractString ncodeunits::Int function SubString{T}(s::T, i::Int, j::Int) where T<:AbstractString - i ≤ j || return new(s, 0, 0) + i ≤ j || return new(s, 0, 0) @boundscheck begin checkbounds(s, i:j) @inbounds isvalid(s, i) || string_index_err(s, i) @@ -55,6 +58,11 @@ convert(::Type{SubString{S}}, s::AbstractString) where {S<:AbstractString} = SubString(convert(S, s)) convert(::Type{T}, s::T) where {T<:SubString} = s +# Regex match allows only Union{String, SubString{String}} so define conversion to this type +convert(::Type{Union{String, SubString{String}}}, s::String) = s +convert(::Type{Union{String, SubString{String}}}, s::SubString{String}) = s +convert(::Type{Union{String, SubString{String}}}, s::AbstractString) = convert(String, s) + function String(s::SubString{String}) parent = s.string copy = GC.@preserve parent unsafe_string(pointer(parent, s.offset+1), s.ncodeunits) @@ -205,19 +213,37 @@ end return n end -function string(a::Union{Char, String, SubString{String}}...) +@inline function __unsafe_string!(out, s::Symbol, offs::Integer) + n = sizeof(s) + GC.@preserve s out unsafe_copyto!(pointer(out, offs), unsafe_convert(Ptr{UInt8},s), n) + return n +end + +function string(a::Union{Char, String, SubString{String}, Symbol}...) n = 0 for v in a + # 4 types is too many for automatic Union-splitting, so we split manually + # and allow one specializable call site per concrete type if v isa Char n += ncodeunits(v) - else + elseif v isa String + n += sizeof(v) + elseif v isa SubString{String} n += sizeof(v) + else + n += sizeof(v::Symbol) end end out = _string_n(n) offs = 1 for v in a - offs += __unsafe_string!(out, v, offs) + if v isa Char + offs += __unsafe_string!(out, v, offs) + elseif v isa String || v isa SubString{String} + offs += __unsafe_string!(out, v, offs) + else + offs += __unsafe_string!(out, v::Symbol, offs) + end end return out end @@ -252,4 +278,4 @@ function filter(f, s::Union{String, SubString{String}}) return String(out) end -getindex(s::AbstractString, r::UnitRange{<:Integer}) = SubString(s, r) +getindex(s::AbstractString, r::AbstractUnitRange{<:Integer}) = SubString(s, r) diff --git a/base/strings/unicode.jl b/base/strings/unicode.jl index 235f85184d43e5..902a27b942d4e8 100644 --- a/base/strings/unicode.jl +++ b/base/strings/unicode.jl @@ -145,20 +145,43 @@ const UTF8PROC_STRIPMARK = (1<<13) utf8proc_error(result) = error(unsafe_string(ccall(:utf8proc_errmsg, Cstring, (Cssize_t,), result))) -function utf8proc_map(str::String, options::Integer) - nwords = ccall(:utf8proc_decompose, Int, (Ptr{UInt8}, Int, Ptr{UInt8}, Int, Cint), - str, sizeof(str), C_NULL, 0, options) - nwords < 0 && utf8proc_error(nwords) +# static wrapper around user callback function +utf8proc_custom_func(codepoint::UInt32, callback::Any) = + UInt32(callback(codepoint))::UInt32 + +function utf8proc_decompose(str, options, buffer, nwords, chartransform::typeof(identity)) + ret = ccall(:utf8proc_decompose, Int, (Ptr{UInt8}, Int, Ptr{UInt8}, Int, Cint), + str, sizeof(str), buffer, nwords, options) + ret < 0 && utf8proc_error(ret) + return ret +end +function utf8proc_decompose(str, options, buffer, nwords, chartransform::T) where T + ret = ccall(:utf8proc_decompose_custom, Int, (Ptr{UInt8}, Int, Ptr{UInt8}, Int, Cint, Ptr{Cvoid}, Ref{T}), + str, sizeof(str), buffer, nwords, options, + @cfunction(utf8proc_custom_func, UInt32, (UInt32, Ref{T})), chartransform) + ret < 0 && utf8proc_error(ret) + return ret +end + +function utf8proc_map(str::Union{String,SubString{String}}, options::Integer, chartransform=identity) + nwords = utf8proc_decompose(str, options, C_NULL, 0, chartransform) buffer = Base.StringVector(nwords*4) - nwords = ccall(:utf8proc_decompose, Int, (Ptr{UInt8}, Int, Ptr{UInt8}, Int, Cint), - str, sizeof(str), buffer, nwords, options) - nwords < 0 && utf8proc_error(nwords) + nwords = utf8proc_decompose(str, options, buffer, nwords, chartransform) nbytes = ccall(:utf8proc_reencode, Int, (Ptr{UInt8}, Int, Cint), buffer, nwords, options) nbytes < 0 && utf8proc_error(nbytes) return String(resize!(buffer, nbytes)) end -utf8proc_map(s::AbstractString, flags::Integer) = utf8proc_map(String(s), flags) +# from julia_charmap.h, used by julia_chartransform in the Unicode stdlib +const _julia_charmap = Dict{UInt32,UInt32}( + 0x025B => 0x03B5, + 0x00B5 => 0x03BC, + 0x00B7 => 0x22C5, + 0x0387 => 0x22C5, + 0x2212 => 0x002D, +) + +utf8proc_map(s::AbstractString, flags::Integer, chartransform=identity) = utf8proc_map(String(s), flags, chartransform) # Documented in Unicode module function normalize( @@ -176,6 +199,7 @@ function normalize( casefold::Bool=false, lump::Bool=false, stripmark::Bool=false, + chartransform=identity, ) flags = 0 stable && (flags = flags | UTF8PROC_STABLE) @@ -198,7 +222,7 @@ function normalize( casefold && (flags = flags | UTF8PROC_CASEFOLD) lump && (flags = flags | UTF8PROC_LUMP) stripmark && (flags = flags | UTF8PROC_STRIPMARK) - utf8proc_map(s, flags) + utf8proc_map(s, flags, chartransform) end function normalize(s::AbstractString, nf::Symbol) @@ -246,10 +270,64 @@ julia> textwidth("March") """ textwidth(s::AbstractString) = mapreduce(textwidth, +, s; init=0) +""" + lowercase(c::AbstractChar) + +Convert `c` to lowercase. + +See also [`uppercase`](@ref), [`titlecase`](@ref). + +# Examples +```jldoctest +julia> lowercase('A') +'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase) + +julia> lowercase('Ö') +'ö': Unicode U+00F6 (category Ll: Letter, lowercase) +``` +""" lowercase(c::T) where {T<:AbstractChar} = isascii(c) ? ('A' <= c <= 'Z' ? c + 0x20 : c) : T(ccall(:utf8proc_tolower, UInt32, (UInt32,), c)) + +""" + uppercase(c::AbstractChar) + +Convert `c` to uppercase. + +See also [`lowercase`](@ref), [`titlecase`](@ref). + +# Examples +```jldoctest +julia> uppercase('a') +'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase) + +julia> uppercase('ê') +'Ê': Unicode U+00CA (category Lu: Letter, uppercase) +``` +""" uppercase(c::T) where {T<:AbstractChar} = isascii(c) ? ('a' <= c <= 'z' ? c - 0x20 : c) : T(ccall(:utf8proc_toupper, UInt32, (UInt32,), c)) + +""" + titlecase(c::AbstractChar) + +Convert `c` to titlecase. This may differ from uppercase for digraphs, +compare the example below. + +See also [`uppercase`](@ref), [`lowercase`](@ref). + +# Examples +```jldoctest +julia> titlecase('a') +'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase) + +julia> titlecase('dž') +'Dž': Unicode U+01C5 (category Lt: Letter, titlecase) + +julia> uppercase('dž') +'DŽ': Unicode U+01C4 (category Lu: Letter, uppercase) +``` +""" titlecase(c::T) where {T<:AbstractChar} = isascii(c) ? ('a' <= c <= 'z' ? c - 0x20 : c) : T(ccall(:utf8proc_totitle, UInt32, (UInt32,), c)) @@ -280,11 +358,10 @@ isassigned(c) = UTF8PROC_CATEGORY_CN < category_code(c) <= UTF8PROC_CATEGORY_CO """ islowercase(c::AbstractChar) -> Bool -Tests whether a character is a lowercase letter. -A character is classified as lowercase if it belongs to Unicode category Ll, -Letter: Lowercase. +Tests whether a character is a lowercase letter (according to the Unicode +standard's `Lowercase` derived property). -See also: [`isuppercase`](@ref). +See also [`isuppercase`](@ref). # Examples ```jldoctest @@ -298,18 +375,17 @@ julia> islowercase('❤') false ``` """ -islowercase(c::AbstractChar) = category_code(c) == UTF8PROC_CATEGORY_LL +islowercase(c::AbstractChar) = ismalformed(c) ? false : Bool(ccall(:utf8proc_islower, Cint, (UInt32,), UInt32(c))) # true for Unicode upper and mixed case """ isuppercase(c::AbstractChar) -> Bool -Tests whether a character is an uppercase letter. -A character is classified as uppercase if it belongs to Unicode category Lu, -Letter: Uppercase, or Lt, Letter: Titlecase. +Tests whether a character is an uppercase letter (according to the Unicode +standard's `Uppercase` derived property). -See also: [`islowercase`](@ref). +See also [`islowercase`](@ref). # Examples ```jldoctest @@ -323,17 +399,14 @@ julia> isuppercase('❤') false ``` """ -function isuppercase(c::AbstractChar) - cat = category_code(c) - cat == UTF8PROC_CATEGORY_LU || cat == UTF8PROC_CATEGORY_LT -end +isuppercase(c::AbstractChar) = ismalformed(c) ? false : Bool(ccall(:utf8proc_isupper, Cint, (UInt32,), UInt32(c))) """ iscased(c::AbstractChar) -> Bool Tests whether a character is cased, i.e. is lower-, upper- or title-cased. -See also: [`islowercase`](@ref), [`isuppercase`](@ref). +See also [`islowercase`](@ref), [`isuppercase`](@ref). """ function iscased(c::AbstractChar) cat = category_code(c) @@ -519,7 +592,7 @@ isxdigit(c::AbstractChar) = '0'<=c<='9' || 'a'<=c<='f' || 'A'<=c<='F' Return `s` with all characters converted to uppercase. -See also: [`lowercase`](@ref), [`titlecase`](@ref), [`uppercasefirst`](@ref). +See also [`lowercase`](@ref), [`titlecase`](@ref), [`uppercasefirst`](@ref). # Examples ```jldoctest @@ -534,7 +607,7 @@ uppercase(s::AbstractString) = map(uppercase, s) Return `s` with all characters converted to lowercase. -See also: [`uppercase`](@ref), [`titlecase`](@ref), [`lowercasefirst`](@ref). +See also [`uppercase`](@ref), [`titlecase`](@ref), [`lowercasefirst`](@ref). # Examples ```jldoctest @@ -550,13 +623,13 @@ lowercase(s::AbstractString) = map(lowercase, s) Capitalize the first character of each word in `s`; if `strict` is true, every other character is converted to lowercase, otherwise they are left unchanged. -By default, all non-letters are considered as word separators; +By default, all non-letters beginning a new grapheme are considered as word separators; a predicate can be passed as the `wordsep` keyword to determine which characters should be considered as word separators. See also [`uppercasefirst`](@ref) to capitalize only the first character in `s`. -See also: [`uppercase`](@ref), [`lowercase`](@ref), [`uppercasefirst`](@ref). +See also [`uppercase`](@ref), [`lowercase`](@ref), [`uppercasefirst`](@ref). # Examples ```jldoctest @@ -570,17 +643,23 @@ julia> titlecase("a-a b-b", wordsep = c->c==' ') "A-a B-b" ``` """ -function titlecase(s::AbstractString; wordsep::Function = !iscased, strict::Bool=true) +function titlecase(s::AbstractString; wordsep::Function = !isletter, strict::Bool=true) startword = true + state = Ref{Int32}(0) + c0 = eltype(s)(0x00000000) b = IOBuffer() for c in s - if wordsep(c) + # Note: It would be better to have a word iterator following UAX#29, + # similar to our grapheme iterator, but utf8proc does not yet have + # this information. At the very least we shouldn't break inside graphemes. + if isgraphemebreak!(state, c0, c) && wordsep(c) print(b, c) startword = true else print(b, startword ? titlecase(c) : strict ? lowercase(c) : c) startword = false end + c0 = c end return String(take!(b)) end @@ -592,8 +671,8 @@ Return `s` with the first character converted to uppercase (technically "title case" for Unicode). See also [`titlecase`](@ref) to capitalize the first character of every word in `s`. -See also: [`lowercasefirst`](@ref), [`uppercase`](@ref), [`lowercase`](@ref), -[`titlecase`](@ref) +See also [`lowercasefirst`](@ref), [`uppercase`](@ref), [`lowercase`](@ref), +[`titlecase`](@ref). # Examples ```jldoctest @@ -614,8 +693,8 @@ end Return `s` with the first character converted to lowercase. -See also: [`uppercasefirst`](@ref), [`uppercase`](@ref), [`lowercase`](@ref), -[`titlecase`](@ref) +See also [`uppercasefirst`](@ref), [`uppercase`](@ref), [`lowercase`](@ref), +[`titlecase`](@ref). # Examples ```jldoctest @@ -680,7 +759,7 @@ function iterate(g::GraphemeIterator, i_=(Int32(0),firstindex(g.s))) y === nothing && return nothing c0, k = y while k <= ncodeunits(s) # loop until next grapheme is s[i:j] - c, ℓ = iterate(s, k) + c, ℓ = iterate(s, k)::NTuple{2,Any} isgraphemebreak!(state, c0, c) && break j = k k = ℓ diff --git a/base/strings/util.jl b/base/strings/util.jl index 24dfaa72e9767c..fb89303e557e48 100644 --- a/base/strings/util.jl +++ b/base/strings/util.jl @@ -1,6 +1,6 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -const Chars = Union{AbstractChar,Tuple{Vararg{<:AbstractChar}},AbstractVector{<:AbstractChar},Set{<:AbstractChar}} +const Chars = Union{AbstractChar,Tuple{Vararg{AbstractChar}},AbstractVector{<:AbstractChar},Set{<:AbstractChar}} # starts with and ends with predicates @@ -10,7 +10,7 @@ const Chars = Union{AbstractChar,Tuple{Vararg{<:AbstractChar}},AbstractVector{<: Return `true` if `s` starts with `prefix`. If `prefix` is a vector or set of characters, test whether the first character of `s` belongs to that set. -See also [`endswith`](@ref). +See also [`endswith`](@ref), [`contains`](@ref). # Examples ```jldoctest @@ -19,10 +19,15 @@ true ``` """ function startswith(a::AbstractString, b::AbstractString) - a, b = Iterators.Stateful(a), Iterators.Stateful(b) - all(splat(==), zip(a, b)) && isempty(b) + i, j = iterate(a), iterate(b) + while true + j === nothing && return true # ran out of prefix: success! + i === nothing && return false # ran out of source: failure + i[1] == j[1] || return false # mismatch: failure + i, j = iterate(a, i[2]), iterate(b, j[2]) + end end -startswith(str::AbstractString, chars::Chars) = !isempty(str) && first(str) in chars +startswith(str::AbstractString, chars::Chars) = !isempty(str) && first(str)::AbstractChar in chars """ endswith(s::AbstractString, suffix::AbstractString) @@ -30,7 +35,7 @@ startswith(str::AbstractString, chars::Chars) = !isempty(str) && first(str) in c Return `true` if `s` ends with `suffix`. If `suffix` is a vector or set of characters, test whether the last character of `s` belongs to that set. -See also [`startswith`](@ref). +See also [`startswith`](@ref), [`contains`](@ref). # Examples ```jldoctest @@ -39,9 +44,14 @@ true ``` """ function endswith(a::AbstractString, b::AbstractString) - a = Iterators.Stateful(Iterators.reverse(a)) - b = Iterators.Stateful(Iterators.reverse(b)) - all(splat(==), zip(a, b)) && isempty(b) + a, b = Iterators.Reverse(a), Iterators.Reverse(b) + i, j = iterate(a), iterate(b) + while true + j === nothing && return true # ran out of suffix: success! + i === nothing && return false # ran out of source: failure + i[1] == j[1] || return false # mismatch: failure + i, j = iterate(a, i[2]), iterate(b, j[2]) + end end endswith(str::AbstractString, chars::Chars) = !isempty(str) && last(str) in chars @@ -51,7 +61,7 @@ function startswith(a::Union{String, SubString{String}}, if ncodeunits(a) < cub false elseif _memcmp(a, b, sizeof(b)) == 0 - nextind(a, cub) == cub + 1 + nextind(a, cub) == cub + 1 # check that end of `b` doesn't match a partial character in `a` else false end @@ -64,7 +74,7 @@ function endswith(a::Union{String, SubString{String}}, if astart < 1 false elseif GC.@preserve(a, _memcmp(pointer(a, astart), b, sizeof(b))) == 0 - thisind(a, astart) == astart + thisind(a, astart) == astart # check that end of `b` doesn't match a partial character in `a` else false end @@ -77,6 +87,8 @@ Return `true` if `haystack` contains `needle`. This is the same as `occursin(needle, haystack)`, but is provided for consistency with `startswith(haystack, needle)` and `endswith(haystack, needle)`. +See also [`occursin`](@ref), [`in`](@ref), [`issubset`](@ref). + # Examples ```jldoctest julia> contains("JuliaLang is pretty cool!", "Julia") @@ -109,6 +121,14 @@ used to implement specialized methods. !!! compat "Julia 1.5" The single argument `endswith(suffix)` requires at least Julia 1.5. +# Examples +```jldoctest +julia> endswith("Julia")("Ends with Julia") +true + +julia> endswith("Julia")("JuliaLang") +false +``` """ endswith(s) = Base.Fix2(endswith, s) @@ -124,6 +144,14 @@ used to implement specialized methods. !!! compat "Julia 1.5" The single argument `startswith(prefix)` requires at least Julia 1.5. +# Examples +```jldoctest +julia> startswith("Julia")("JuliaLang") +true + +julia> startswith("Julia")("Ends with Julia") +false +``` """ startswith(s) = Base.Fix2(startswith, s) @@ -146,6 +174,8 @@ The call `chop(s)` removes the last character from `s`. If it is requested to remove more characters than `length(s)` then an empty string is returned. +See also [`chomp`](@ref), [`startswith`](@ref), [`first`](@ref). + # Examples ```jldoctest julia> a = "March" @@ -171,11 +201,98 @@ end # TODO: optimization for the default case based on # chop(s::AbstractString) = SubString(s, firstindex(s), prevind(s, lastindex(s))) +""" + chopprefix(s::AbstractString, prefix::Union{AbstractString,Regex}) -> SubString + +Remove the prefix `prefix` from `s`. If `s` does not start with `prefix`, a string equal to `s` is returned. + +See also [`chopsuffix`](@ref). + +!!! compat "Julia 1.8" + This function is available as of Julia 1.8. + +# Examples +```jldoctest +julia> chopprefix("Hamburger", "Ham") +"burger" + +julia> chopprefix("Hamburger", "hotdog") +"Hamburger" +``` +""" +function chopprefix(s::AbstractString, prefix::AbstractString) + k = firstindex(s) + i, j = iterate(s), iterate(prefix) + while true + j === nothing && i === nothing && return SubString(s, 1, 0) # s == prefix: empty result + j === nothing && return @inbounds SubString(s, k) # ran out of prefix: success! + i === nothing && return SubString(s) # ran out of source: failure + i[1] == j[1] || return SubString(s) # mismatch: failure + k = i[2] + i, j = iterate(s, k), iterate(prefix, j[2]) + end +end + +function chopprefix(s::Union{String, SubString{String}}, + prefix::Union{String, SubString{String}}) + if startswith(s, prefix) + SubString(s, 1 + ncodeunits(prefix)) + else + SubString(s) + end +end + +""" + chopsuffix(s::AbstractString, suffix::Union{AbstractString,Regex}) -> SubString + +Remove the suffix `suffix` from `s`. If `s` does not end with `suffix`, a string equal to `s` is returned. + +See also [`chopprefix`](@ref). + +!!! compat "Julia 1.8" + This function is available as of Julia 1.8. + +# Examples +```jldoctest +julia> chopsuffix("Hamburger", "er") +"Hamburg" + +julia> chopsuffix("Hamburger", "hotdog") +"Hamburger" +``` +""" +function chopsuffix(s::AbstractString, suffix::AbstractString) + a, b = Iterators.Reverse(s), Iterators.Reverse(suffix) + k = lastindex(s) + i, j = iterate(a), iterate(b) + while true + j === nothing && i === nothing && return SubString(s, 1, 0) # s == suffix: empty result + j === nothing && return @inbounds SubString(s, firstindex(s), k) # ran out of suffix: success! + i === nothing && return SubString(s) # ran out of source: failure + i[1] == j[1] || return SubString(s) # mismatch: failure + k = i[2] + i, j = iterate(a, k), iterate(b, j[2]) + end +end + +function chopsuffix(s::Union{String, SubString{String}}, + suffix::Union{String, SubString{String}}) + if !isempty(suffix) && endswith(s, suffix) + astart = ncodeunits(s) - ncodeunits(suffix) + 1 + @inbounds SubString(s, firstindex(s), prevind(s, astart)) + else + SubString(s) + end +end + + """ chomp(s::AbstractString) -> SubString Remove a single trailing newline from a string. +See also [`chop`](@ref). + # Examples ```jldoctest julia> chomp("Hello\\n") @@ -213,6 +330,8 @@ The default behaviour is to remove leading whitespace and delimiters: see The optional `chars` argument specifies which characters to remove: it can be a single character, or a vector or set of characters. +See also [`strip`](@ref) and [`rstrip`](@ref). + # Examples ```jldoctest julia> a = lpad("March", 20) @@ -245,6 +364,8 @@ The default behaviour is to remove trailing whitespace and delimiters: see The optional `chars` argument specifies which characters to remove: it can be a single character, or a vector or set of characters. +See also [`strip`](@ref) and [`lstrip`](@ref). + # Examples ```jldoctest julia> a = rpad("March", 20) @@ -270,12 +391,14 @@ rstrip(s::AbstractString, chars::Chars) = rstrip(in(chars), s) Remove leading and trailing characters from `str`, either those specified by `chars` or those for which the function `pred` returns `true`. -The default behaviour is to remove leading whitespace and delimiters: see +The default behaviour is to remove leading and trailing whitespace and delimiters: see [`isspace`](@ref) for precise details. The optional `chars` argument specifies which characters to remove: it can be a single character, vector or set of characters. +See also [`lstrip`](@ref) and [`rstrip`](@ref). + !!! compat "Julia 1.2" The method which accepts a predicate function requires Julia 1.2 or later. @@ -295,7 +418,7 @@ strip(f, s::AbstractString) = lstrip(f, rstrip(f, s)) lpad(s, n::Integer, p::Union{AbstractChar,AbstractString}=' ') -> String Stringify `s` and pad the resulting string on the left with `p` to make it `n` -characters (code points) long. If `s` is already `n` characters long, an equal +characters (in [`textwidth`](@ref)) long. If `s` is already `n` characters long, an equal string is returned. Pad with spaces by default. # Examples @@ -303,6 +426,8 @@ string is returned. Pad with spaces by default. julia> lpad("March", 10) " March" ``` +!!! compat "Julia 1.7" + In Julia 1.7, this function was changed to use `textwidth` rather than a raw character (codepoint) count. """ lpad(s, n::Integer, p::Union{AbstractChar,AbstractString}=' ') = lpad(string(s)::AbstractString, n, string(p)) @@ -312,9 +437,9 @@ function lpad( p::Union{AbstractChar,AbstractString}=' ', ) :: String n = Int(n)::Int - m = signed(n) - Int(length(s))::Int + m = signed(n) - Int(textwidth(s))::Int m ≤ 0 && return string(s) - l = length(p) + l = textwidth(p) q, r = divrem(m, l) r == 0 ? string(p^q, s) : string(p^q, first(p, r), s) end @@ -323,7 +448,7 @@ end rpad(s, n::Integer, p::Union{AbstractChar,AbstractString}=' ') -> String Stringify `s` and pad the resulting string on the right with `p` to make it `n` -characters (code points) long. If `s` is already `n` characters long, an equal +characters (in [`textwidth`](@ref)) long. If `s` is already `n` characters long, an equal string is returned. Pad with spaces by default. # Examples @@ -331,6 +456,8 @@ string is returned. Pad with spaces by default. julia> rpad("March", 20) "March " ``` +!!! compat "Julia 1.7" + In Julia 1.7, this function was changed to use `textwidth` rather than a raw character (codepoint) count. """ rpad(s, n::Integer, p::Union{AbstractChar,AbstractString}=' ') = rpad(string(s)::AbstractString, n, string(p)) @@ -340,13 +467,95 @@ function rpad( p::Union{AbstractChar,AbstractString}=' ', ) :: String n = Int(n)::Int - m = signed(n) - Int(length(s))::Int + m = signed(n) - Int(textwidth(s))::Int m ≤ 0 && return string(s) - l = length(p) + l = textwidth(p) q, r = divrem(m, l) r == 0 ? string(s, p^q) : string(s, p^q, first(p, r)) end +""" + eachsplit(str::AbstractString, dlm; limit::Integer=0, keepempty::Bool=true) + eachsplit(str::AbstractString; limit::Integer=0, keepempty::Bool=false) + +Split `str` on occurrences of the delimiter(s) `dlm` and return an iterator over the +substrings. `dlm` can be any of the formats allowed by [`findnext`](@ref)'s first argument +(i.e. as a string, regular expression or a function), or as a single character or collection +of characters. + +If `dlm` is omitted, it defaults to [`isspace`](@ref). + +The optional keyword arguments are: + - `limit`: the maximum size of the result. `limit=0` implies no maximum (default) + - `keepempty`: whether empty fields should be kept in the result. Default is `false` without + a `dlm` argument, `true` with a `dlm` argument. + +See also [`split`](@ref). + +!!! compat "Julia 1.8" + The `eachsplit` function requires at least Julia 1.8. + +# Examples +```jldoctest +julia> a = "Ma.rch" +"Ma.rch" + +julia> collect(eachsplit(a, ".")) +2-element Vector{SubString}: + "Ma" + "rch" +``` +""" +function eachsplit end + +# Forcing specialization on `splitter` improves performance (roughly 30% decrease in runtime) +# and prevents a major invalidation risk (1550 MethodInstances) +struct SplitIterator{S<:AbstractString,F} + str::S + splitter::F + limit::Int + keepempty::Bool +end + +eltype(::Type{<:SplitIterator}) = SubString + +IteratorSize(::Type{<:SplitIterator}) = SizeUnknown() + +# i: the starting index of the substring to be extracted +# k: the starting index of the next substring to be extracted +# n: the number of splits returned so far; always less than iter.limit - 1 (1 for the rest) +function iterate(iter::SplitIterator, (i, k, n)=(firstindex(iter.str), firstindex(iter.str), 0)) + i - 1 > ncodeunits(iter.str)::Int && return nothing + r = findnext(iter.splitter, iter.str, k)::Union{Nothing,Int,UnitRange{Int}} + while r !== nothing && n != iter.limit - 1 && first(r) <= ncodeunits(iter.str) + j, k = first(r), nextind(iter.str, last(r))::Int + k_ = k <= j ? nextind(iter.str, j) : k + if i < k + substr = @inbounds SubString(iter.str, i, prevind(iter.str, j)::Int) + (iter.keepempty || i < j) && return (substr, (k, k_, n + 1)) + i = k + end + k = k_ + r = findnext(iter.splitter, iter.str, k)::Union{Nothing,Int,UnitRange{Int}} + end + iter.keepempty || i <= ncodeunits(iter.str) || return nothing + @inbounds SubString(iter.str, i), (ncodeunits(iter.str) + 2, k, n + 1) +end + +eachsplit(str::T, splitter; limit::Integer=0, keepempty::Bool=true) where {T<:AbstractString} = + SplitIterator(str, splitter, limit, keepempty) + +eachsplit(str::T, splitter::Union{Tuple{Vararg{AbstractChar}},AbstractVector{<:AbstractChar},Set{<:AbstractChar}}; + limit::Integer=0, keepempty=true) where {T<:AbstractString} = + eachsplit(str, in(splitter); limit, keepempty) + +eachsplit(str::T, splitter::AbstractChar; limit::Integer=0, keepempty=true) where {T<:AbstractString} = + eachsplit(str, isequal(splitter); limit, keepempty) + +# a bit oddball, but standard behavior in Perl, Ruby & Python: +eachsplit(str::AbstractString; limit::Integer=0, keepempty=false) = + eachsplit(str, isspace; limit, keepempty) + """ split(str::AbstractString, dlm; limit::Integer=0, keepempty::Bool=true) split(str::AbstractString; limit::Integer=0, keepempty::Bool=false) @@ -363,7 +572,7 @@ The optional keyword arguments are: - `keepempty`: whether empty fields should be kept in the result. Default is `false` without a `dlm` argument, `true` with a `dlm` argument. -See also [`rsplit`](@ref). +See also [`rsplit`](@ref), [`eachsplit`](@ref). # Examples ```jldoctest @@ -376,50 +585,16 @@ julia> split(a, ".") "rch" ``` """ -function split end - function split(str::T, splitter; limit::Integer=0, keepempty::Bool=true) where {T<:AbstractString} - _split(str, splitter, limit, keepempty, T <: SubString ? T[] : SubString{T}[]) -end -function split(str::T, splitter::Union{Tuple{Vararg{<:AbstractChar}},AbstractVector{<:AbstractChar},Set{<:AbstractChar}}; - limit::Integer=0, keepempty::Bool=true) where {T<:AbstractString} - _split(str, in(splitter), limit, keepempty, T <: SubString ? T[] : SubString{T}[]) -end -function split(str::T, splitter::AbstractChar; - limit::Integer=0, keepempty::Bool=true) where {T<:AbstractString} - _split(str, isequal(splitter), limit, keepempty, T <: SubString ? T[] : SubString{T}[]) -end - -function _split(str::AbstractString, splitter, limit::Integer, keepempty::Bool, strs::Vector) - i = 1 # firstindex(str) - n = lastindex(str)::Int - r = findfirst(splitter,str)::Union{Nothing,Int,UnitRange{Int}} - if !isnothing(r) - j, k = first(r), nextind(str,last(r))::Int - while 0 < j <= n && length(strs) != limit-1 - if i < k - if keepempty || i < j - push!(strs, @inbounds SubString(str,i,prevind(str,j)::Int)) - end - i = k - end - (k <= j) && (k = nextind(str,j)::Int) - r = findnext(splitter,str,k)::Union{Nothing,Int,UnitRange{Int}} - isnothing(r) && break - j, k = first(r), nextind(str,last(r))::Int - end - end - if keepempty || i <= ncodeunits(str)::Int - push!(strs, @inbounds SubString(str,i)) - end - return strs + itr = eachsplit(str, splitter; limit, keepempty) + collect(T <: SubString ? T : SubString{T}, itr) end # a bit oddball, but standard behavior in Perl, Ruby & Python: split(str::AbstractString; limit::Integer=0, keepempty::Bool=false) = - split(str, isspace; limit=limit, keepempty=keepempty) + split(str, isspace; limit, keepempty) """ rsplit(s::AbstractString; limit::Integer=0, keepempty::Bool=false) @@ -456,7 +631,7 @@ function rsplit(str::T, splitter; limit::Integer=0, keepempty::Bool=true) where {T<:AbstractString} _rsplit(str, splitter, limit, keepempty, T <: SubString ? T[] : SubString{T}[]) end -function rsplit(str::T, splitter::Union{Tuple{Vararg{<:AbstractChar}},AbstractVector{<:AbstractChar},Set{<:AbstractChar}}; +function rsplit(str::T, splitter::Union{Tuple{Vararg{AbstractChar}},AbstractVector{<:AbstractChar},Set{<:AbstractChar}}; limit::Integer=0, keepempty::Bool=true) where {T<:AbstractString} _rsplit(str, in(splitter), limit, keepempty, T <: SubString ? T[] : SubString{T}[]) end @@ -488,68 +663,93 @@ _replace(io, repl::Function, str, r, pattern) = _replace(io, repl::Function, str, r, pattern::Function) = print(io, repl(str[first(r)])) -replace(str::String, pat_repl::Pair{<:AbstractChar}; count::Integer=typemax(Int)) = - replace(str, isequal(first(pat_repl)) => last(pat_repl); count=count) - -replace(str::String, pat_repl::Pair{<:Union{Tuple{Vararg{<:AbstractChar}}, - AbstractVector{<:AbstractChar},Set{<:AbstractChar}}}; - count::Integer=typemax(Int)) = - replace(str, in(first(pat_repl)) => last(pat_repl), count=count) - _pat_replacer(x) = x _free_pat_replacer(x) = nothing -function replace(str::String, pat_repl::Pair; count::Integer=typemax(Int)) - pattern, repl = pat_repl +_pat_replacer(x::AbstractChar) = isequal(x) +_pat_replacer(x::Union{Tuple{Vararg{AbstractChar}},AbstractVector{<:AbstractChar},Set{<:AbstractChar}}) = in(x) + +function replace(str::String, pat_repl::Vararg{Pair,N}; count::Integer=typemax(Int)) where N count == 0 && return str count < 0 && throw(DomainError(count, "`count` must be non-negative.")) n = 1 - e = lastindex(str) + e1 = nextind(str, lastindex(str)) # sizeof(str) i = a = firstindex(str) - pattern = _pat_replacer(pattern) - r = something(findnext(pattern,str,i), 0) - j, k = first(r), last(r) - if j == 0 - _free_pat_replacer(pattern) + patterns = map(p -> _pat_replacer(first(p)), pat_repl) + replaces = map(last, pat_repl) + rs = map(patterns) do p + r = findnext(p, str, a) + if r === nothing || first(r) == 0 + return e1+1:0 + end + r isa Int && (r = r:r) # findnext / performance fix + return r + end + if all(>(e1), map(first, rs)) + foreach(_free_pat_replacer, patterns) return str end out = IOBuffer(sizehint=floor(Int, 1.2sizeof(str))) - while j != 0 + while true + p = argmin(map(first, rs)) # TODO: or argmin(rs), to pick the shortest first match ? + r = rs[p] + j, k = first(r), last(r) + j > e1 && break if i == a || i <= k + # copy out preserved portion GC.@preserve str unsafe_write(out, pointer(str, i), UInt(j-i)) - _replace(out, repl, str, r, pattern) + # copy out replacement string + _replace(out, replaces[p], str, r, patterns[p]) end if k < j i = j - j > e && break + j == e1 && break k = nextind(str, j) else i = k = nextind(str, k) end - r = something(findnext(pattern,str,k), 0) - r === 0:-1 || n == count && break - j, k = first(r), last(r) + n == count && break + let k = k + rs = map(patterns, rs) do p, r + if first(r) < k + r = findnext(p, str, k) + if r === nothing || first(r) == 0 + return e1+1:0 + end + r isa Int && (r = r:r) # findnext / performance fix + end + return r + end + end n += 1 end - _free_pat_replacer(pattern) - write(out, SubString(str,i)) - String(take!(out)) + foreach(_free_pat_replacer, patterns) + write(out, SubString(str, i)) + return String(take!(out)) end + """ - replace(s::AbstractString, pat=>r; [count::Integer]) + replace(s::AbstractString, pat=>r, [pat2=>r2, ...]; [count::Integer]) Search for the given pattern `pat` in `s`, and replace each occurrence with `r`. If `count` is provided, replace at most `count` occurrences. `pat` may be a single character, a vector or a set of characters, a string, or a regular expression. If `r` is a function, each occurrence is replaced with `r(s)` -where `s` is the matched substring (when `pat` is a `Regex` or `AbstractString`) or +where `s` is the matched substring (when `pat` is a `AbstractPattern` or `AbstractString`) or character (when `pat` is an `AbstractChar` or a collection of `AbstractChar`). If `pat` is a regular expression and `r` is a [`SubstitutionString`](@ref), then capture group references in `r` are replaced with the corresponding matched text. To remove instances of `pat` from `string`, set `r` to the empty `String` (`""`). +Multiple patterns can be specified, and they will be applied left-to-right +simultaneously, so only one pattern will be applied to any character, and the +patterns will only be applied to the input text, not the replacements. + +!!! compat "Julia 1.7" + Support for multiple patterns requires version 1.7. + # Examples ```jldoctest julia> replace("Python is a programming language.", "Python" => "Julia") @@ -563,25 +763,33 @@ julia> replace("The quick foxes run quickly.", "quick" => "", count=1) julia> replace("The quick foxes run quickly.", r"fox(es)?" => s"bus\\1") "The quick buses run quickly." + +julia> replace("abcabc", "a" => "b", "b" => "c", r".+" => "a") +"bca" ``` """ -replace(s::AbstractString, pat_f::Pair; count=typemax(Int)) = - replace(String(s), pat_f, count=count) +replace(s::AbstractString, pat_f::Pair...; count=typemax(Int)) = + replace(String(s), pat_f..., count=count) # TODO: allow transform as the first argument to replace? # hex <-> bytes conversion """ - hex2bytes(s::Union{AbstractString,AbstractVector{UInt8}}) + hex2bytes(itr) -Given a string or array `s` of ASCII codes for a sequence of hexadecimal digits, returns a +Given an iterable `itr` of ASCII codes for a sequence of hexadecimal digits, returns a `Vector{UInt8}` of bytes corresponding to the binary representation: each successive pair -of hexadecimal digits in `s` gives the value of one byte in the return vector. +of hexadecimal digits in `itr` gives the value of one byte in the return vector. -The length of `s` must be even, and the returned array has half of the length of `s`. +The length of `itr` must be even, and the returned array has half of the length of `itr`. See also [`hex2bytes!`](@ref) for an in-place version, and [`bytes2hex`](@ref) for the inverse. +!!! compat "Julia 1.7" + Calling `hex2bytes` with iterators producing `UInt8` values requires + Julia 1.7 or later. In earlier versions, you can `collect` the iterator + before calling `hex2bytes`. + # Examples ```jldoctest julia> s = string(12345, base = 16) @@ -610,46 +818,64 @@ julia> hex2bytes(a) """ function hex2bytes end -hex2bytes(s::AbstractString) = hex2bytes(String(s)) -hex2bytes(s::Union{String,AbstractVector{UInt8}}) = hex2bytes!(Vector{UInt8}(undef, length(s) >> 1), s) +hex2bytes(s) = hex2bytes!(Vector{UInt8}(undef, length(s) >> 1), s) -_firstbyteidx(s::String) = 1 -_firstbyteidx(s::AbstractVector{UInt8}) = first(eachindex(s)) -_lastbyteidx(s::String) = sizeof(s) -_lastbyteidx(s::AbstractVector{UInt8}) = lastindex(s) +# special case - valid bytes are checked in the generic implementation +function hex2bytes!(dest::AbstractArray{UInt8}, s::String) + sizeof(s) != length(s) && throw(ArgumentError("input string must consist of hexadecimal characters only")) + + hex2bytes!(dest, transcode(UInt8, s)) +end """ - hex2bytes!(d::AbstractVector{UInt8}, s::Union{String,AbstractVector{UInt8}}) + hex2bytes!(dest::AbstractVector{UInt8}, itr) -Convert an array `s` of bytes representing a hexadecimal string to its binary +Convert an iterable `itr` of bytes representing a hexadecimal string to its binary representation, similar to [`hex2bytes`](@ref) except that the output is written in-place -in `d`. The length of `s` must be exactly twice the length of `d`. -""" -function hex2bytes!(d::AbstractVector{UInt8}, s::Union{String,AbstractVector{UInt8}}) - if 2length(d) != sizeof(s) - isodd(sizeof(s)) && throw(ArgumentError("input hex array must have even length")) - throw(ArgumentError("output array must be half length of input array")) - end - j = first(eachindex(d)) - 1 - for i = _firstbyteidx(s):2:_lastbyteidx(s) - @inbounds d[j += 1] = number_from_hex(_nthbyte(s,i)) << 4 + number_from_hex(_nthbyte(s,i+1)) +to `dest`. The length of `dest` must be half the length of `itr`. + +!!! compat "Julia 1.7" + Calling hex2bytes! with iterators producing UInt8 requires + version 1.7. In earlier versions, you can collect the iterable + before calling instead. +""" +function hex2bytes!(dest::AbstractArray{UInt8}, itr) + isodd(length(itr)) && throw(ArgumentError("length of iterable must be even")) + @boundscheck 2*length(dest) != length(itr) && throw(ArgumentError("length of output array must be half of the length of input iterable")) + iszero(length(itr)) && return dest + + next = iterate(itr) + @inbounds for i in eachindex(dest) + x,state = next::NTuple{2,Any} + y,state = iterate(itr, state)::NTuple{2,Any} + next = iterate(itr, state) + dest[i] = number_from_hex(x) << 4 + number_from_hex(y) end - return d + + return dest end -@inline number_from_hex(c) = - (UInt8('0') <= c <= UInt8('9')) ? c - UInt8('0') : - (UInt8('A') <= c <= UInt8('F')) ? c - (UInt8('A') - 0x0a) : - (UInt8('a') <= c <= UInt8('f')) ? c - (UInt8('a') - 0x0a) : +@inline number_from_hex(c::AbstractChar) = number_from_hex(Char(c)) +@inline number_from_hex(c::Char) = number_from_hex(UInt8(c)) +@inline function number_from_hex(c::UInt8) + UInt8('0') <= c <= UInt8('9') && return c - UInt8('0') + c |= 0b0100000 + UInt8('a') <= c <= UInt8('f') && return c - UInt8('a') + 0x0a throw(ArgumentError("byte is not an ASCII hexadecimal digit")) +end """ - bytes2hex(a::AbstractArray{UInt8}) -> String - bytes2hex(io::IO, a::AbstractArray{UInt8}) + bytes2hex(itr) -> String + bytes2hex(io::IO, itr) -Convert an array `a` of bytes to its hexadecimal string representation, either -returning a `String` via `bytes2hex(a)` or writing the string to an `io` stream -via `bytes2hex(io, a)`. The hexadecimal characters are all lowercase. +Convert an iterator `itr` of bytes to its hexadecimal string representation, either +returning a `String` via `bytes2hex(itr)` or writing the string to an `io` stream +via `bytes2hex(io, itr)`. The hexadecimal characters are all lowercase. + +!!! compat "Julia 1.7" + Calling `bytes2hex` with arbitrary iterators producing `UInt8` values requires + Julia 1.7 or later. In earlier versions, you can `collect` the iterator + before calling `bytes2hex`. # Examples ```jldoctest @@ -667,19 +893,22 @@ julia> bytes2hex(b) """ function bytes2hex end -function bytes2hex(a::AbstractArray{UInt8}) - b = Base.StringVector(2*length(a)) - @inbounds for (i, x) in enumerate(a) +function bytes2hex(itr) + eltype(itr) === UInt8 || throw(ArgumentError("eltype of iterator not UInt8")) + b = Base.StringVector(2*length(itr)) + @inbounds for (i, x) in enumerate(itr) b[2i - 1] = hex_chars[1 + x >> 4] b[2i ] = hex_chars[1 + x & 0xf] end return String(b) end -bytes2hex(io::IO, a::AbstractArray{UInt8}) = - for x in a +function bytes2hex(io::IO, itr) + eltype(itr) === UInt8 || throw(ArgumentError("eltype of iterator not UInt8")) + for x in itr print(io, Char(hex_chars[1 + x >> 4]), Char(hex_chars[1 + x & 0xf])) end +end # check for pure ASCII-ness function ascii(s::String) @@ -696,6 +925,8 @@ end Convert a string to `String` type and check that it contains only ASCII data, otherwise throwing an `ArgumentError` indicating the position of the first non-ASCII byte. +See also the [`isascii`](@ref) predicate to filter or replace non-ASCII characters. + # Examples ```jldoctest julia> ascii("abcdeγfgh") @@ -708,3 +939,12 @@ julia> ascii("abcdefgh") ``` """ ascii(x::AbstractString) = ascii(String(x)) + +Base.rest(s::Union{String,SubString{String}}, i=1) = SubString(s, i) +function Base.rest(s::AbstractString, st...) + io = IOBuffer() + for c in Iterators.rest(s, st...) + print(io, c) + end + return String(take!(io)) +end diff --git a/base/subarray.jl b/base/subarray.jl index de99ba48f275d8..ff2408bb48534a 100644 --- a/base/subarray.jl +++ b/base/subarray.jl @@ -17,22 +17,22 @@ struct SubArray{T,N,P,I,L} <: AbstractArray{T,N} offset1::Int # for linear indexing and pointer, only valid when L==true stride1::Int # used only for linear indexing function SubArray{T,N,P,I,L}(parent, indices, offset1, stride1) where {T,N,P,I,L} - @_inline_meta + @inline check_parent_index_match(parent, indices) new(parent, indices, offset1, stride1) end end # Compute the linear indexability of the indices, and combine it with the linear indexing of the parent function SubArray(parent::AbstractArray, indices::Tuple) - @_inline_meta + @inline SubArray(IndexStyle(viewindexing(indices), IndexStyle(parent)), parent, ensure_indexable(indices), index_dimsum(indices...)) end function SubArray(::IndexCartesian, parent::P, indices::I, ::NTuple{N,Any}) where {P,I,N} - @_inline_meta + @inline SubArray{eltype(P), N, P, I, false}(parent, indices, 0, 0) end function SubArray(::IndexLinear, parent::P, indices::I, ::NTuple{N,Any}) where {P,I,N} - @_inline_meta + @inline # Compute the stride and offset stride1 = compute_stride1(parent, indices) SubArray{eltype(P), N, P, I, true}(parent, indices, compute_offset1(parent, stride1, indices), stride1) @@ -46,9 +46,9 @@ check_parent_index_match(parent, ::NTuple{N, Bool}) where {N} = # This computes the linear indexing compatibility for a given tuple of indices viewindexing(I::Tuple{}) = IndexLinear() # Leading scalar indices simply increase the stride -viewindexing(I::Tuple{ScalarIndex, Vararg{Any}}) = (@_inline_meta; viewindexing(tail(I))) +viewindexing(I::Tuple{ScalarIndex, Vararg{Any}}) = (@inline; viewindexing(tail(I))) # Slices may begin a section which may be followed by any number of Slices -viewindexing(I::Tuple{Slice, Slice, Vararg{Any}}) = (@_inline_meta; viewindexing(tail(I))) +viewindexing(I::Tuple{Slice, Slice, Vararg{Any}}) = (@inline; viewindexing(tail(I))) # A UnitRange can follow Slices, but only if all other indices are scalar viewindexing(I::Tuple{Slice, AbstractUnitRange, Vararg{ScalarIndex}}) = IndexLinear() viewindexing(I::Tuple{Slice, Slice, Vararg{ScalarIndex}}) = IndexLinear() # disambiguate @@ -60,16 +60,20 @@ viewindexing(I::Tuple{Vararg{Any}}) = IndexCartesian() viewindexing(I::Tuple{AbstractArray, Vararg{Any}}) = IndexCartesian() # Simple utilities -size(V::SubArray) = (@_inline_meta; map(n->Int(unsafe_length(n)), axes(V))) +size(V::SubArray) = (@inline; map(length, axes(V))) similar(V::SubArray, T::Type, dims::Dims) = similar(V.parent, T, dims) sizeof(V::SubArray) = length(V) * sizeof(eltype(V)) sizeof(V::SubArray{<:Any,<:Any,<:Array}) = length(V) * elsize(V.parent) -elsize(::Type{<:SubArray{<:Any,<:Any,P}}) where {P<:Array} = elsize(P) - -copy(V::SubArray) = V.parent[V.indices...] +function Base.copy(V::SubArray) + v = V.parent[V.indices...] + ndims(V) == 0 || return v + x = similar(V) # ensure proper type of x + x[] = v + return x +end parent(V::SubArray) = V.parent parentindices(V::SubArray) = V.indices @@ -92,7 +96,7 @@ julia> parentindices(V) (1, Base.Slice(Base.OneTo(2))) ``` """ -parentindices(a::AbstractArray) = map(OneTo, size(a)) +parentindices(a::AbstractArray) = map(oneto, size(a)) ## Aliasing detection dataids(A::SubArray) = (dataids(A.parent)..., _splatmap(dataids, A.indices)...) @@ -109,7 +113,7 @@ function unaliascopy(V::SubArray{T,N,A,I,LD}) where {T,N,A<:Array,I<:Tuple{Varar end # Transform indices to be "dense" _trimmedindex(i::Real) = oftype(i, 1) -_trimmedindex(i::AbstractUnitRange) = oftype(i, OneTo(length(i))) +_trimmedindex(i::AbstractUnitRange) = oftype(i, oneto(length(i))) _trimmedindex(i::AbstractArray) = oftype(i, reshape(eachindex(IndexLinear(), i), axes(i))) ## SubArray creation @@ -124,10 +128,22 @@ _maybe_reshape_parent(A::AbstractArray, ::NTuple{N, Bool}) where {N} = reshape(A """ view(A, inds...) -Like [`getindex`](@ref), but returns a view into the parent array `A` with the -given indices instead of making a copy. Calling [`getindex`](@ref) or -[`setindex!`](@ref) on the returned `SubArray` computes the -indices to the parent array on the fly without checking bounds. +Like [`getindex`](@ref), but returns a lightweight array that lazily references +(or is effectively a _view_ into) the parent array `A` at the given index or indices +`inds` instead of eagerly extracting elements or constructing a copied subset. +Calling [`getindex`](@ref) or [`setindex!`](@ref) on the returned value +(often a [`SubArray`](@ref)) computes the indices to access or modify the +parent array on the fly. The behavior is undefined if the shape of the parent array is +changed after `view` is called because there is no bound check for the parent array; e.g., +it may cause a segmentation fault. + +Some immutable parent arrays (like ranges) may choose to simply +recompute a new array in some circumstances instead of returning +a `SubArray` if doing so is efficient and provides compatible semantics. + +!!! compat "Julia 1.6" + In Julia 1.6 or later, `view` can be called on an `AbstractString`, returning a + `SubString`. # Examples ```jldoctest @@ -150,10 +166,13 @@ julia> A # Note A has changed even though we modified b 2×2 Matrix{Int64}: 0 2 0 4 + +julia> view(2:5, 2:3) # returns a range as type is immutable +3:4 ``` """ function view(A::AbstractArray, I::Vararg{Any,N}) where {N} - @_inline_meta + @inline J = map(i->unalias(A,i), to_indices(A, I)) @boundscheck checkbounds(A, J...) unsafe_view(_maybe_reshape_parent(A, index_ndims(J...)), J...) @@ -185,8 +204,14 @@ function view(r1::LinRange, r2::OrdinalRange{<:Integer}) getindex(r1, r2) end +# getindex(r::AbstractRange, ::Colon) returns a copy of the range, and we may do the same for a view +function view(r1::AbstractRange, c::Colon) + @_propagate_inbounds_meta + getindex(r1, c) +end + function unsafe_view(A::AbstractArray, I::Vararg{ViewIndex,N}) where {N} - @_inline_meta + @inline SubArray(A, I) end # When we take the view of a view, it's often possible to "reindex" the parent @@ -196,16 +221,16 @@ end # So we use _maybe_reindex to figure out if there are any arrays of # `CartesianIndex`, and if so, we punt and keep two layers of indirection. unsafe_view(V::SubArray, I::Vararg{ViewIndex,N}) where {N} = - (@_inline_meta; _maybe_reindex(V, I)) -_maybe_reindex(V, I) = (@_inline_meta; _maybe_reindex(V, I, I)) + (@inline; _maybe_reindex(V, I)) +_maybe_reindex(V, I) = (@inline; _maybe_reindex(V, I, I)) _maybe_reindex(V, I, ::Tuple{AbstractArray{<:AbstractCartesianIndex}, Vararg{Any}}) = - (@_inline_meta; SubArray(V, I)) + (@inline; SubArray(V, I)) # But allow arrays of CartesianIndex{1}; they behave just like arrays of Ints _maybe_reindex(V, I, A::Tuple{AbstractArray{<:AbstractCartesianIndex{1}}, Vararg{Any}}) = - (@_inline_meta; _maybe_reindex(V, I, tail(A))) -_maybe_reindex(V, I, A::Tuple{Any, Vararg{Any}}) = (@_inline_meta; _maybe_reindex(V, I, tail(A))) + (@inline; _maybe_reindex(V, I, tail(A))) +_maybe_reindex(V, I, A::Tuple{Any, Vararg{Any}}) = (@inline; _maybe_reindex(V, I, tail(A))) function _maybe_reindex(V, I, ::Tuple{}) - @_inline_meta + @inline @inbounds idxs = to_indices(V.parent, reindex(V.indices, I)) SubArray(V.parent, idxs) end @@ -252,7 +277,7 @@ end # In general, we simply re-index the parent indices by the provided ones SlowSubArray{T,N,P,I} = SubArray{T,N,P,I,false} function getindex(V::SubArray{T,N}, I::Vararg{Int,N}) where {T,N} - @_inline_meta + @inline @boundscheck checkbounds(V, I...) @inbounds r = V.parent[reindex(V.indices, I)...] r @@ -261,7 +286,7 @@ end # But SubArrays with fast linear indexing pre-compute a stride and offset FastSubArray{T,N,P,I} = SubArray{T,N,P,I,true} function getindex(V::FastSubArray, i::Int) - @_inline_meta + @inline @boundscheck checkbounds(V, i) @inbounds r = V.parent[V.offset1 + V.stride1*i] r @@ -271,7 +296,7 @@ end FastContiguousSubArray{T,N,P,I<:Union{Tuple{Union{Slice, AbstractUnitRange}, Vararg{Any}}, Tuple{Vararg{ScalarIndex}}}} = SubArray{T,N,P,I,true} function getindex(V::FastContiguousSubArray, i::Int) - @_inline_meta + @inline @boundscheck checkbounds(V, i) @inbounds r = V.parent[V.offset1 + i] r @@ -279,13 +304,13 @@ end # For vector views with linear indexing, we disambiguate to favor the stride/offset # computation as that'll generally be faster than (or just as fast as) re-indexing into a range. function getindex(V::FastSubArray{<:Any, 1}, i::Int) - @_inline_meta + @inline @boundscheck checkbounds(V, i) @inbounds r = V.parent[V.offset1 + V.stride1*i] r end function getindex(V::FastContiguousSubArray{<:Any, 1}, i::Int) - @_inline_meta + @inline @boundscheck checkbounds(V, i) @inbounds r = V.parent[V.offset1 + i] r @@ -293,31 +318,31 @@ end # Indexed assignment follows the same pattern as `getindex` above function setindex!(V::SubArray{T,N}, x, I::Vararg{Int,N}) where {T,N} - @_inline_meta + @inline @boundscheck checkbounds(V, I...) @inbounds V.parent[reindex(V.indices, I)...] = x V end function setindex!(V::FastSubArray, x, i::Int) - @_inline_meta + @inline @boundscheck checkbounds(V, i) @inbounds V.parent[V.offset1 + V.stride1*i] = x V end function setindex!(V::FastContiguousSubArray, x, i::Int) - @_inline_meta + @inline @boundscheck checkbounds(V, i) @inbounds V.parent[V.offset1 + i] = x V end function setindex!(V::FastSubArray{<:Any, 1}, x, i::Int) - @_inline_meta + @inline @boundscheck checkbounds(V, i) @inbounds V.parent[V.offset1 + V.stride1*i] = x V end function setindex!(V::FastContiguousSubArray{<:Any, 1}, x, i::Int) - @_inline_meta + @inline @boundscheck checkbounds(V, i) @inbounds V.parent[V.offset1 + i] = x V @@ -339,11 +364,11 @@ substrides(strds, I::Tuple{Any, Vararg{Any}}) = throw(ArgumentError("strides is stride(V::SubArray, d::Integer) = d <= ndims(V) ? strides(V)[d] : strides(V)[end] * size(V)[end] compute_stride1(parent::AbstractArray, I::NTuple{N,Any}) where {N} = - (@_inline_meta; compute_stride1(1, fill_to_length(axes(parent), OneTo(1), Val(N)), I)) + (@inline; compute_stride1(1, fill_to_length(axes(parent), OneTo(1), Val(N)), I)) compute_stride1(s, inds, I::Tuple{}) = s compute_stride1(s, inds, I::Tuple{Vararg{ScalarIndex}}) = s compute_stride1(s, inds, I::Tuple{ScalarIndex, Vararg{Any}}) = - (@_inline_meta; compute_stride1(s*unsafe_length(inds[1]), tail(inds), tail(I))) + (@inline; compute_stride1(s*length(inds[1]), tail(inds), tail(I))) compute_stride1(s, inds, I::Tuple{AbstractRange, Vararg{Any}}) = s*step(I[1]) compute_stride1(s, inds, I::Tuple{Slice, Vararg{Any}}) = s compute_stride1(s, inds, I::Tuple{Any, Vararg{Any}}) = throw(ArgumentError("invalid strided index type $(typeof(I[1]))")) @@ -366,42 +391,42 @@ end # The running sum is `f`; the cumulative stride product is `s`. # If the parent is a vector, then we offset the parent's own indices with parameters of I compute_offset1(parent::AbstractVector, stride1::Integer, I::Tuple{AbstractRange}) = - (@_inline_meta; first(I[1]) - stride1*first(axes1(I[1]))) + (@inline; first(I[1]) - stride1*first(axes1(I[1]))) # If the result is one-dimensional and it's a Colon, then linear # indexing uses the indices along the given dimension. # If the result is one-dimensional and it's a range, then linear # indexing might be offset if the index itself is offset -# Otherwise linear indexing always starts with 1. +# Otherwise linear indexing always matches the parent. compute_offset1(parent, stride1::Integer, I::Tuple) = - (@_inline_meta; compute_offset1(parent, stride1, find_extended_dims(1, I...), find_extended_inds(I...), I)) + (@inline; compute_offset1(parent, stride1, find_extended_dims(1, I...), find_extended_inds(I...), I)) compute_offset1(parent, stride1::Integer, dims::Tuple{Int}, inds::Tuple{Slice}, I::Tuple) = - (@_inline_meta; compute_linindex(parent, I) - stride1*first(axes(parent, dims[1]))) # index-preserving case + (@inline; compute_linindex(parent, I) - stride1*first(axes(parent, dims[1]))) # index-preserving case compute_offset1(parent, stride1::Integer, dims, inds::Tuple{AbstractRange}, I::Tuple) = - (@_inline_meta; compute_linindex(parent, I) - stride1*first(axes1(inds[1]))) # potentially index-offsetting case + (@inline; compute_linindex(parent, I) - stride1*first(axes1(inds[1]))) # potentially index-offsetting case compute_offset1(parent, stride1::Integer, dims, inds, I::Tuple) = - (@_inline_meta; compute_linindex(parent, I) - stride1) # linear indexing starts with 1 + (@inline; compute_linindex(parent, I) - stride1) function compute_linindex(parent, I::NTuple{N,Any}) where N - @_inline_meta + @inline IP = fill_to_length(axes(parent), OneTo(1), Val(N)) - compute_linindex(1, 1, IP, I) + compute_linindex(first(LinearIndices(parent)), 1, IP, I) end function compute_linindex(f, s, IP::Tuple, I::Tuple{ScalarIndex, Vararg{Any}}) - @_inline_meta + @inline Δi = I[1]-first(IP[1]) - compute_linindex(f + Δi*s, s*unsafe_length(IP[1]), tail(IP), tail(I)) + compute_linindex(f + Δi*s, s*length(IP[1]), tail(IP), tail(I)) end function compute_linindex(f, s, IP::Tuple, I::Tuple{Any, Vararg{Any}}) - @_inline_meta + @inline Δi = first(I[1])-first(IP[1]) - compute_linindex(f + Δi*s, s*unsafe_length(IP[1]), tail(IP), tail(I)) + compute_linindex(f + Δi*s, s*length(IP[1]), tail(IP), tail(I)) end compute_linindex(f, s, IP::Tuple, I::Tuple{}) = f -find_extended_dims(dim, ::ScalarIndex, I...) = (@_inline_meta; find_extended_dims(dim + 1, I...)) -find_extended_dims(dim, i1, I...) = (@_inline_meta; (dim, find_extended_dims(dim + 1, I...)...)) +find_extended_dims(dim, ::ScalarIndex, I...) = (@inline; find_extended_dims(dim + 1, I...)) +find_extended_dims(dim, i1, I...) = (@inline; (dim, find_extended_dims(dim + 1, I...)...)) find_extended_dims(dim) = () -find_extended_inds(::ScalarIndex, I...) = (@_inline_meta; find_extended_inds(I...)) -find_extended_inds(i1, I...) = (@_inline_meta; (i1, find_extended_inds(I...)...)) +find_extended_inds(::ScalarIndex, I...) = (@inline; find_extended_inds(I...)) +find_extended_inds(i1, I...) = (@inline; (i1, find_extended_inds(I...)...)) find_extended_inds() = () function unsafe_convert(::Type{Ptr{T}}, V::SubArray{T,N,P,<:Tuple{Vararg{RangeIndex}}}) where {T,N,P} @@ -423,10 +448,10 @@ end # indices are taken from the range/vector # Since bounds-checking is performance-critical and uses # indices, it's worth optimizing these implementations thoroughly -axes(S::SubArray) = (@_inline_meta; _indices_sub(S.indices...)) -_indices_sub(::Real, I...) = (@_inline_meta; _indices_sub(I...)) +axes(S::SubArray) = (@inline; _indices_sub(S.indices...)) +_indices_sub(::Real, I...) = (@inline; _indices_sub(I...)) _indices_sub() = () function _indices_sub(i1::AbstractArray, I...) - @_inline_meta - (unsafe_indices(i1)..., _indices_sub(I...)...) + @inline + (axes(i1)..., _indices_sub(I...)...) end diff --git a/base/summarysize.jl b/base/summarysize.jl index 1c83089c325bac..849edee2064541 100644 --- a/base/summarysize.jl +++ b/base/summarysize.jl @@ -17,6 +17,20 @@ Compute the amount of memory, in bytes, used by all unique objects reachable fro - `exclude`: specifies the types of objects to exclude from the traversal. - `chargeall`: specifies the types of objects to always charge the size of all of their fields, even if those fields would normally be excluded. + +See also [`sizeof`](@ref). + +# Examples +```jldoctest +julia> Base.summarysize(1.0) +8 + +julia> Base.summarysize(Ref(rand(100))) +848 + +julia> sizeof(Ref(rand(100))) +8 +``` """ function summarysize(obj; exclude = Union{DataType, Core.TypeName, Core.MethodInstance}, @@ -120,12 +134,13 @@ function (ss::SummarySize)(obj::Array) if !haskey(ss.seen, datakey) ss.seen[datakey] = true dsize = Core.sizeof(obj) - if isbitsunion(eltype(obj)) + T = eltype(obj) + if isbitsunion(T) # add 1 union selector byte for each element dsize += length(obj) end size += dsize - if !isempty(obj) && !Base.allocatedinline(eltype(obj)) + if !isempty(obj) && T !== Symbol && (!Base.allocatedinline(T) || (T isa DataType && !Base.datatype_pointerfree(T))) push!(ss.frontier_x, obj) push!(ss.frontier_i, 1) end @@ -173,8 +188,9 @@ function (ss::SummarySize)(obj::Task) end size += ss(obj.storage)::Int size += ss(obj.donenotify)::Int - size += ss(obj.exception)::Int size += ss(obj.result)::Int # TODO: add stack size, and possibly traverse stack roots return size end + +(ss::SummarySize)(obj::BigInt) = _summarysize(ss, obj) + obj.alloc*sizeof(Base.GMP.Limb) diff --git a/base/sysimg.jl b/base/sysimg.jl index 893af845771771..36c40e4ae748a2 100644 --- a/base/sysimg.jl +++ b/base/sysimg.jl @@ -19,45 +19,62 @@ Base.init_load_path() if Base.is_primary_base_module # load some stdlib packages but don't put their names in Main let - # Stdlibs manually sorted in top down order + # Stdlibs sorted in dependency, then alphabetical, order by contrib/print_sorted_stdlibs.jl + # Run with the `--exclude-jlls` option to filter out all JLL packages stdlibs = [ - # No deps - :Base64, - :CRC32c, - :SHA, - :FileWatching, - :Unicode, - :Mmap, - :Serialization, - :Libdl, - :Printf, - :Markdown, - :LibGit2, - :Logging, - :Sockets, - :Profile, - :Dates, - :DelimitedFiles, - :Random, - :UUIDs, - :Future, - :LinearAlgebra, - :SparseArrays, - :SuiteSparse, - :Distributed, - :SharedArrays, - :TOML, - :Artifacts, - :Pkg, - :Test, - :REPL, - :Statistics, - :MozillaCACerts_jll, - :LibCURL_jll, - :LibCURL, - :Downloads, - ] - + # No dependencies + :ArgTools, + :Artifacts, + :Base64, + :CRC32c, + :FileWatching, + :Libdl, + :Logging, + :Mmap, + :NetworkOptions, + :SHA, + :Serialization, + :Sockets, + :Unicode, + + # 1-depth packages + :DelimitedFiles, + :LinearAlgebra, + :Markdown, + :Printf, + :Random, + :Tar, + + # 2-depth packages + :Dates, + :Distributed, + :Future, + :InteractiveUtils, + :LibGit2, + :Profile, + :SparseArrays, + :UUIDs, + + # 3-depth packages + :REPL, + :SharedArrays, + :Statistics, + :SuiteSparse, + :TOML, + :Test, + + # 4-depth packages + :LibCURL, + + # 5-depth packages + :Downloads, + + # 6-depth packages + :Pkg, + + # 7-depth packages + :LazyArtifacts, + ] maxlen = reduce(max, textwidth.(string.(stdlibs)); init=0) tot_time_stdlib = 0.0 @@ -108,6 +125,8 @@ let empty!(DEPOT_PATH) end +empty!(Base.TOML_CACHE.d) +Base.TOML.reinit!(Base.TOML_CACHE.p, "") @eval Sys begin BINDIR = "" STDLIB = "" diff --git a/base/sysinfo.jl b/base/sysinfo.jl index 4a16d4b17bf078..f0852f32fc17df 100644 --- a/base/sysinfo.jl +++ b/base/sysinfo.jl @@ -35,20 +35,19 @@ export BINDIR, import ..Base: show -global BINDIR = ccall(:jl_get_julia_bindir, Any, ())::String """ - Sys.BINDIR + Sys.BINDIR::String A string containing the full path to the directory containing the `julia` executable. """ -:BINDIR +global BINDIR::String = ccall(:jl_get_julia_bindir, Any, ())::String """ - Sys.STDLIB + Sys.STDLIB::String A string containing the full path to the directory containing the `stdlib` packages. """ -STDLIB = "$BINDIR/../share/julia/stdlib/v$(VERSION.major).$(VERSION.minor)" # for bootstrap +global STDLIB::String = "$BINDIR/../share/julia/stdlib/v$(VERSION.major).$(VERSION.minor)" # for bootstrap # In case STDLIB change after julia is built, the variable below can be used # to update cached method locations to updated ones. const BUILD_STDLIB_PATH = STDLIB @@ -56,7 +55,7 @@ const BUILD_STDLIB_PATH = STDLIB # helper to avoid triggering precompile warnings """ - Sys.CPU_THREADS + Sys.CPU_THREADS::Int The number of logical CPU cores available in the system, i.e. the number of threads that the CPU can run concurrently. Note that this is not necessarily the number of @@ -65,37 +64,39 @@ CPU cores, for example, in the presence of See Hwloc.jl or CpuId.jl for extended information, including number of physical cores. """ -CPU_THREADS = 1 # for bootstrap, changed on startup +global CPU_THREADS::Int = 1 # for bootstrap, changed on startup """ - Sys.ARCH + Sys.ARCH::Symbol A symbol representing the architecture of the build configuration. """ -const ARCH = ccall(:jl_get_ARCH, Any, ()) +const ARCH = ccall(:jl_get_ARCH, Any, ())::Symbol """ - Sys.KERNEL + Sys.KERNEL::Symbol A symbol representing the name of the operating system, as returned by `uname` of the build configuration. """ -const KERNEL = ccall(:jl_get_UNAME, Any, ()) +const KERNEL = ccall(:jl_get_UNAME, Any, ())::Symbol """ - Sys.MACHINE + Sys.MACHINE::String A string containing the build triple. """ -const MACHINE = Base.MACHINE +const MACHINE = Base.MACHINE::String """ - Sys.WORD_SIZE + Sys.WORD_SIZE::Int Standard word size on the current machine, in bits. """ const WORD_SIZE = Core.sizeof(Int) * 8 +global SC_CLK_TCK::Clong, CPU_NAME::String, JIT::String + function __init__() env_threads = nothing if haskey(ENV, "JULIA_CPU_THREADS") @@ -286,8 +287,8 @@ end Get the maximum resident set size utilized in bytes. See also: - - man page of getrusage(2) on Linux and FreeBSD. - - windows api `GetProcessMemoryInfo` + - man page of `getrusage`(2) on Linux and FreeBSD. + - Windows API `GetProcessMemoryInfo`. """ maxrss() = ccall(:jl_maxrss, Csize_t, ()) @@ -499,7 +500,7 @@ function which(program_name::String) # If we have been given just a program name (not a relative or absolute # path) then we should search `PATH` for it here: pathsep = iswindows() ? ';' : ':' - path_dirs = abspath.(split(get(ENV, "PATH", ""), pathsep)) + path_dirs = map(abspath, eachsplit(get(ENV, "PATH", ""), pathsep)) # On windows we always check the current directory as well if iswindows() @@ -516,7 +517,7 @@ function which(program_name::String) program_path = joinpath(path_dir, pname) # If we find something that matches our name and we can execute if isfile(program_path) && isexecutable(program_path) - return realpath(program_path) + return program_path end end end diff --git a/base/task.jl b/base/task.jl index 7d74e2988fc788..6b7f5747f12740 100644 --- a/base/task.jl +++ b/base/task.jl @@ -25,6 +25,16 @@ function showerror(io::IO, ce::CapturedException) showerror(io, ce.ex, ce.processed_bt, backtrace=true) end +""" + capture_exception(ex, bt) -> Exception + +Returns an exception, possibly incorporating information from a backtrace `bt`. Defaults to returning [`CapturedException(ex, bt)`](@ref). + +Used in [`asyncmap`](@ref) and [`asyncmap!`](@ref) to capture exceptions thrown during +the user-supplied function call. +""" +capture_exception(ex, bt) = CapturedException(ex, bt) + """ CompositeException @@ -40,6 +50,7 @@ struct CompositeException <: Exception end length(c::CompositeException) = length(c.exceptions) push!(c::CompositeException, ex) = push!(c.exceptions, ex) +pushfirst!(c::CompositeException, ex) = pushfirst!(c.exceptions, ex) isempty(c::CompositeException) = isempty(c.exceptions) iterate(c::CompositeException, state...) = iterate(c.exceptions, state...) eltype(::Type{CompositeException}) = Any @@ -49,7 +60,7 @@ function showerror(io::IO, ex::CompositeException) showerror(io, ex.exceptions[1]) remaining = length(ex) - 1 if remaining > 0 - print(io, string("\n\n...and ", remaining, " more exception(s).\n")) + print(io, "\n\n...and ", remaining, " more exception", remaining > 1 ? "s" : "", ".\n") end else print(io, "CompositeException()\n") @@ -77,9 +88,14 @@ function showerror(io::IO, ex::TaskFailedException, bt = nothing; backtrace=true end function show_task_exception(io::IO, t::Task; indent = true) - stack = catch_stack(t) + stack = current_exceptions(t) b = IOBuffer() - show_exception_stack(IOContext(b, io), stack) + if isempty(stack) + # exception stack buffer not available; probably a serialized task + showerror(IOContext(b, io), t.result) + else + show_exception_stack(IOContext(b, io), stack) + end str = String(take!(b)) if indent str = replace(str, "\n" => "\n ") @@ -131,10 +147,21 @@ const task_state_runnable = UInt8(0) const task_state_done = UInt8(1) const task_state_failed = UInt8(2) +const _state_index = findfirst(==(:_state), fieldnames(Task)) +@eval function load_state_acquire(t) + # TODO: Replace this by proper atomic operations when available + @GC.preserve t llvmcall($(""" + %ptr = inttoptr i$(Sys.WORD_SIZE) %0 to i8* + %rv = load atomic i8, i8* %ptr acquire, align 8 + ret i8 %rv + """), UInt8, Tuple{Ptr{UInt8}}, + Ptr{UInt8}(pointer_from_objref(t) + fieldoffset(Task, _state_index))) +end + @inline function getproperty(t::Task, field::Symbol) if field === :state # TODO: this field name should be deprecated in 2.0 - st = getfield(t, :_state) + st = load_state_acquire(t) if st === task_state_runnable return :runnable elseif st === task_state_done @@ -146,7 +173,10 @@ const task_state_failed = UInt8(2) end elseif field === :backtrace # TODO: this field name should be deprecated in 2.0 - return catch_stack(t)[end][2] + return current_exceptions(t)[end][2] + elseif field === :exception + # TODO: this field name should be deprecated in 2.0 + return t._isexception ? t.result : nothing else return getfield(t, field) end @@ -174,7 +204,7 @@ julia> istaskdone(b) true ``` """ -istaskdone(t::Task) = t._state !== task_state_runnable +istaskdone(t::Task) = load_state_acquire(t) !== task_state_runnable """ istaskstarted(t::Task) -> Bool @@ -218,9 +248,13 @@ true !!! compat "Julia 1.3" This function requires at least Julia 1.3. """ -istaskfailed(t::Task) = (t._state === task_state_failed) +istaskfailed(t::Task) = (load_state_acquire(t) === task_state_failed) Threads.threadid(t::Task) = Int(ccall(:jl_get_task_tid, Int16, (Any,), t)+1) +function Threads.threadpool(t::Task) + tpid = ccall(:jl_get_task_threadpoolid, Int8, (Any,), t) + return tpid == 0 ? :default : :interactive +end task_result(t::Task) = t.result @@ -287,6 +321,18 @@ function _wait2(t::Task, waiter::Task) if !istaskdone(t) push!(t.donenotify.waitq, waiter) unlock(t.donenotify) + # since _wait2 is similar to schedule, we should observe the sticky + # bit, even if we aren't calling `schedule` due to this early-return + if waiter.sticky && Threads.threadid(waiter) == 0 + # Issue #41324 + # t.sticky && tid == 0 is a task that needs to be co-scheduled with + # the parent task. If the parent (current_task) is not sticky we must + # set it to be sticky. + # XXX: Ideally we would be able to unset this + current_task().sticky = true + tid = Threads.threadid() + ccall(:jl_set_task_tid, Cint, (Any, Cint), waiter, tid-1) + end return nothing else unlock(t.donenotify) @@ -322,6 +368,29 @@ end ## lexically-scoped waiting for multiple items +struct ScheduledAfterSyncException <: Exception + values::Vector{Any} +end + +function showerror(io::IO, ex::ScheduledAfterSyncException) + print(io, "ScheduledAfterSyncException: ") + if isempty(ex.values) + print(io, "(no values)") + return + end + show(io, ex.values[1]) + if length(ex.values) == 1 + print(io, " is") + elseif length(ex.values) == 2 + print(io, " and one more ") + print(io, nameof(typeof(ex.values[2]))) + print(io, " are") + else + print(io, " and ", length(ex.values) - 1, " more objects are") + end + print(io, " registered after the end of a `@sync` block") +end + function sync_end(c::Channel{Any}) local c_ex while isready(c) @@ -346,6 +415,25 @@ function sync_end(c::Channel{Any}) end end close(c) + + # Capture all waitable objects scheduled after the end of `@sync` and + # include them in the exception. This way, the user can check what was + # scheduled by examining at the exception object. + local racy + for r in c + if !@isdefined(racy) + racy = [] + end + push!(racy, r) + end + if @isdefined(racy) + if !@isdefined(c_ex) + c_ex = CompositeException() + end + # Since this is a clear programming error, show this exception first: + pushfirst!(c_ex, ScheduledAfterSyncException(racy)) + end + if @isdefined(c_ex) throw(c_ex) end @@ -383,10 +471,24 @@ Values can be interpolated into `@async` via `\$`, which copies the value direct constructed underlying closure. This allows you to insert the _value_ of a variable, isolating the asynchronous code from changes to the variable's value in the current task. +!!! warning + It is strongly encouraged to favor `Threads.@spawn` over `@async` always **even when no + parallelism is required** especially in publicly distributed libraries. This is + because a use of `@async` disables the migration of the *parent* task across worker + threads in the current implementation of Julia. Thus, seemingly innocent use of + `@async` in a library function can have a large impact on the performance of very + different parts of user applications. + !!! compat "Julia 1.4" Interpolating values via `\$` is available as of Julia 1.4. """ macro async(expr) + do_async_macro(expr) +end + +# generate the code for @async, possibly wrapping the task in something before +# pushing it to the wait queue. +function do_async_macro(expr; wrap=identity) letargs = Base._lift_one_interp!(expr) thunk = esc(:(()->($expr))) @@ -395,7 +497,7 @@ macro async(expr) let $(letargs...) local task = Task($thunk) if $(Expr(:islocal, var)) - put!($var, task) + put!($var, $(wrap(:task))) end schedule(task) task @@ -403,6 +505,73 @@ macro async(expr) end end +# task wrapper that doesn't create exceptions wrapped in TaskFailedException +struct UnwrapTaskFailedException + task::Task +end + +# common code for wait&fetch for UnwrapTaskFailedException +function unwrap_task_failed(f::Function, t::UnwrapTaskFailedException) + try + f(t.task) + catch ex + if ex isa TaskFailedException + throw(ex.task.exception) + else + rethrow() + end + end +end + +# the unwrapping for above task wrapper (gets triggered in sync_end()) +wait(t::UnwrapTaskFailedException) = unwrap_task_failed(wait, t) + +# same for fetching the tasks, for convenience +fetch(t::UnwrapTaskFailedException) = unwrap_task_failed(fetch, t) + +# macro for running async code that doesn't throw wrapped exceptions +macro async_unwrap(expr) + do_async_macro(expr, wrap=task->:(Base.UnwrapTaskFailedException($task))) +end + +""" + errormonitor(t::Task) + +Print an error log to `stderr` if task `t` fails. +""" +function errormonitor(t::Task) + t2 = Task() do + if istaskfailed(t) + local errs = stderr + try # try to display the failure atomically + errio = IOContext(PipeBuffer(), errs::IO) + emphasize(errio, "Unhandled Task ") + display_error(errio, scrub_repl_backtrace(current_exceptions(t))) + write(errs, errio) + catch + try # try to display the secondary error atomically + errio = IOContext(PipeBuffer(), errs::IO) + print(errio, "\nSYSTEM: caught exception while trying to print a failed Task notice: ") + display_error(errio, scrub_repl_backtrace(current_exceptions())) + write(errs, errio) + flush(errs) + # and then the actual error, as best we can + Core.print(Core.stderr, "while handling: ") + Core.println(Core.stderr, current_exceptions(t)[end][1]) + catch e + # give up + Core.print(Core.stderr, "\nSYSTEM: caught exception of type ", typeof(e).name.name, + " while trying to print a failed Task notice; giving up\n") + end + end + end + nothing + end + t2.sticky = false + _wait2(t, t2) + return t +end + # Capture interpolated variables in $() and move them to let-block function _lift_one_interp!(e) letargs = Any[] # store the new gensymed arguments @@ -491,14 +660,14 @@ end ## scheduler and work queue -struct InvasiveLinkedListSynchronized{T} - queue::InvasiveLinkedList{T} +struct IntrusiveLinkedListSynchronized{T} + queue::IntrusiveLinkedList{T} lock::Threads.SpinLock - InvasiveLinkedListSynchronized{T}() where {T} = new(InvasiveLinkedList{T}(), Threads.SpinLock()) + IntrusiveLinkedListSynchronized{T}() where {T} = new(IntrusiveLinkedList{T}(), Threads.SpinLock()) end -isempty(W::InvasiveLinkedListSynchronized) = isempty(W.queue) -length(W::InvasiveLinkedListSynchronized) = length(W.queue) -function push!(W::InvasiveLinkedListSynchronized{T}, t::T) where T +isempty(W::IntrusiveLinkedListSynchronized) = isempty(W.queue) +length(W::IntrusiveLinkedListSynchronized) = length(W.queue) +function push!(W::IntrusiveLinkedListSynchronized{T}, t::T) where T lock(W.lock) try push!(W.queue, t) @@ -507,7 +676,7 @@ function push!(W::InvasiveLinkedListSynchronized{T}, t::T) where T end return W end -function pushfirst!(W::InvasiveLinkedListSynchronized{T}, t::T) where T +function pushfirst!(W::IntrusiveLinkedListSynchronized{T}, t::T) where T lock(W.lock) try pushfirst!(W.queue, t) @@ -516,7 +685,7 @@ function pushfirst!(W::InvasiveLinkedListSynchronized{T}, t::T) where T end return W end -function pop!(W::InvasiveLinkedListSynchronized) +function pop!(W::IntrusiveLinkedListSynchronized) lock(W.lock) try return pop!(W.queue) @@ -524,7 +693,7 @@ function pop!(W::InvasiveLinkedListSynchronized) unlock(W.lock) end end -function popfirst!(W::InvasiveLinkedListSynchronized) +function popfirst!(W::IntrusiveLinkedListSynchronized) lock(W.lock) try return popfirst!(W.queue) @@ -532,7 +701,7 @@ function popfirst!(W::InvasiveLinkedListSynchronized) unlock(W.lock) end end -function list_deletefirst!(W::InvasiveLinkedListSynchronized{T}, t::T) where T +function list_deletefirst!(W::IntrusiveLinkedListSynchronized{T}, t::T) where T lock(W.lock) try list_deletefirst!(W.queue, t) @@ -542,41 +711,50 @@ function list_deletefirst!(W::InvasiveLinkedListSynchronized{T}, t::T) where T return W end -const StickyWorkqueue = InvasiveLinkedListSynchronized{Task} -global const Workqueues = [StickyWorkqueue()] -global const Workqueue = Workqueues[1] # default work queue is thread 1 -function __preinit_threads__() - if length(Workqueues) < Threads.nthreads() - resize!(Workqueues, Threads.nthreads()) - for i = 2:length(Workqueues) - Workqueues[i] = StickyWorkqueue() +const StickyWorkqueue = IntrusiveLinkedListSynchronized{Task} +global Workqueues::Vector{StickyWorkqueue} = [StickyWorkqueue()] +const Workqueues_lock = Threads.SpinLock() +const Workqueue = Workqueues[1] # default work queue is thread 1 // TODO: deprecate this variable + +function workqueue_for(tid::Int) + qs = Workqueues + if length(qs) >= tid && isassigned(qs, tid) + return @inbounds qs[tid] + end + # slow path to allocate it + l = Workqueues_lock + @lock l begin + qs = Workqueues + if length(qs) < tid + nt = Threads.nthreads() + @assert tid <= nt + global Workqueues = qs = copyto!(typeof(qs)(undef, length(qs) + nt - 1), qs) end + if !isassigned(qs, tid) + @inbounds qs[tid] = StickyWorkqueue() + end + return @inbounds qs[tid] end - nothing end function enq_work(t::Task) (t._state === task_state_runnable && t.queue === nothing) || error("schedule: Task not runnable") - tid = Threads.threadid(t) - # Note there are three reasons a Task might be put into a sticky queue - # even if t.sticky == false: - # 1. The Task's stack is currently being used by the scheduler for a certain thread. - # 2. There is only 1 thread. - # 3. The multiq is full (can be fixed by making it growable). - if t.sticky || tid != 0 || Threads.nthreads() == 1 + if t.sticky || Threads.nthreads() == 1 + tid = Threads.threadid(t) if tid == 0 + # Issue #41324 + # t.sticky && tid == 0 is a task that needs to be co-scheduled with + # the parent task. If the parent (current_task) is not sticky we must + # set it to be sticky. + # XXX: Ideally we would be able to unset this + current_task().sticky = true tid = Threads.threadid() - ccall(:jl_set_task_tid, Cvoid, (Any, Cint), t, tid-1) + ccall(:jl_set_task_tid, Cint, (Any, Cint), t, tid-1) end - push!(Workqueues[tid], t) + push!(workqueue_for(tid), t) else + Partr.multiq_insert(t, t.priority) tid = 0 - if ccall(:jl_enqueue_task, Cint, (Any,), t) != 0 - # if multiq is full, give to a random thread (TODO fix) - tid = mod(time_ns() % Int, Threads.nthreads()) + 1 - ccall(:jl_set_task_tid, Cvoid, (Any, Cint), t, tid-1) - push!(Workqueues[tid], t) - end end ccall(:jl_wakeup_thread, Cvoid, (Int16,), (tid - 1) % Int16) return t @@ -594,6 +772,10 @@ If a second argument `val` is provided, it will be passed to the task (via the r [`yieldto`](@ref)) when it runs again. If `error` is `true`, the value is raised as an exception in the woken task. +!!! warning + It is incorrect to use `schedule` on an arbitrary `Task` that has already been started. + See [the API reference](@ref low-level-schedule-wait) for more information. + # Examples ```jldoctest julia> a5() = sum(i for i in 1:1000); @@ -619,7 +801,8 @@ function schedule(t::Task, @nospecialize(arg); error=false) t._state === task_state_runnable || Base.error("schedule: Task not runnable") if error t.queue === nothing || Base.list_deletefirst!(t.queue, t) - setfield!(t, :exception, arg) + setfield!(t, :result, arg) + setfield!(t, :_isexception, true) else t.queue === nothing || Base.error("schedule: Task not runnable") setfield!(t, :result, arg) @@ -655,6 +838,7 @@ A fast, unfair-scheduling version of `schedule(t, arg); yield()` which immediately yields to `t` before calling the scheduler. """ function yield(t::Task, @nospecialize(x=nothing)) + (t._state === task_state_runnable && t.queue === nothing) || error("yield: Task not runnable") t.result = x enq_work(current_task()) set_next_task(t) @@ -670,6 +854,13 @@ call to `yieldto`. This is a low-level call that only switches tasks, not consid or scheduling in any way. Its use is discouraged. """ function yieldto(t::Task, @nospecialize(x=nothing)) + # TODO: these are legacy behaviors; these should perhaps be a scheduler + # state error instead. + if t._state === task_state_done + return x + elseif t._state === task_state_failed + throw(t.result) + end t.result = x set_next_task(t) return try_yieldto(identity) @@ -683,9 +874,10 @@ function try_yieldto(undo) rethrow() end ct = current_task() - exc = ct.exception - if exc !== nothing - ct.exception = nothing + if ct._isexception + exc = ct.result + ct.result = nothing + ct._isexception = false throw(exc) end result = ct.result @@ -695,18 +887,20 @@ end # yield to a task, throwing an exception in it function throwto(t::Task, @nospecialize exc) - t.exception = exc - return yieldto(t) + t.result = exc + t._isexception = true + set_next_task(t) + return try_yieldto(identity) end function ensure_rescheduled(othertask::Task) ct = current_task() - W = Workqueues[Threads.threadid()] + W = workqueue_for(Threads.threadid()) if ct !== othertask && othertask._state === task_state_runnable # we failed to yield to othertask # return it to the head of a queue to be retried later tid = Threads.threadid(othertask) - Wother = tid == 0 ? W : Workqueues[tid] + Wother = tid == 0 ? W : workqueue_for(tid) pushfirst!(Wother, othertask) end # if the current task was queued, @@ -717,31 +911,36 @@ function ensure_rescheduled(othertask::Task) end function trypoptask(W::StickyWorkqueue) - isempty(W) && return - t = popfirst!(W) - if t._state !== task_state_runnable - # assume this somehow got queued twice, - # probably broken now, but try discarding this switch and keep going - # can't throw here, because it's probably not the fault of the caller to wait - # and don't want to use print() here, because that may try to incur a task switch - ccall(:jl_safe_printf, Cvoid, (Ptr{UInt8}, Int32...), - "\nWARNING: Workqueue inconsistency detected: popfirst!(Workqueue).state != :runnable\n") - return + while !isempty(W) + t = popfirst!(W) + if t._state !== task_state_runnable + # assume this somehow got queued twice, + # probably broken now, but try discarding this switch and keep going + # can't throw here, because it's probably not the fault of the caller to wait + # and don't want to use print() here, because that may try to incur a task switch + ccall(:jl_safe_printf, Cvoid, (Ptr{UInt8}, Int32...), + "\nWARNING: Workqueue inconsistency detected: popfirst!(Workqueue).state != :runnable\n") + continue + end + return t end - return t + return Partr.multiq_deletemin() end +checktaskempty = Partr.multiq_check_empty + @noinline function poptask(W::StickyWorkqueue) task = trypoptask(W) if !(task isa Task) - task = ccall(:jl_task_get_next, Ref{Task}, (Any, Any), trypoptask, W) + task = ccall(:jl_task_get_next, Ref{Task}, (Any, Any, Any), trypoptask, W, checktaskempty) end set_next_task(task) nothing end function wait() - W = Workqueues[Threads.threadid()] + GC.safepoint() + W = workqueue_for(Threads.threadid()) poptask(W) result = try_yieldto(ensure_rescheduled) process_events() diff --git a/base/threadcall.jl b/base/threadcall.jl index 2267e4ea2228c3..45965fdbc6c651 100644 --- a/base/threadcall.jl +++ b/base/threadcall.jl @@ -9,7 +9,7 @@ const threadcall_restrictor = Semaphore(max_ccall_threads) The `@threadcall` macro is called in the same way as [`ccall`](@ref) but does the work in a different thread. This is useful when you want to call a blocking C -function without causing the main `julia` thread to become blocked. Concurrency +function without causing the current `julia` thread to become blocked. Concurrency is limited by size of the libuv thread pool, which defaults to 4 threads but can be increased by setting the `UV_THREADPOOL_SIZE` environment variable and restarting the `julia` process. @@ -30,7 +30,7 @@ macro threadcall(f, rettype, argtypes, argvals...) argvals = map(esc, argvals) # construct non-allocating wrapper to call C function - wrapper = :(function (args_ptr::Ptr{Cvoid}, retval_ptr::Ptr{Cvoid}) + wrapper = :(function (fptr::Ptr{Cvoid}, args_ptr::Ptr{Cvoid}, retval_ptr::Ptr{Cvoid}) p = args_ptr # the rest of the body is created below end) @@ -42,18 +42,19 @@ macro threadcall(f, rettype, argtypes, argvals...) push!(body, :(p += Core.sizeof($T))) push!(args, arg) end - push!(body, :(ret = ccall($f, $rettype, ($(argtypes...),), $(args...)))) + push!(body, :(ret = ccall(fptr, $rettype, ($(argtypes...),), $(args...)))) push!(body, :(unsafe_store!(convert(Ptr{$rettype}, retval_ptr), ret))) push!(body, :(return Int(Core.sizeof($rettype)))) # return code to generate wrapper function and send work request thread queue wrapper = Expr(Symbol("hygienic-scope"), wrapper, @__MODULE__) - return :(let fun_ptr = @cfunction($wrapper, Int, (Ptr{Cvoid}, Ptr{Cvoid})) - do_threadcall(fun_ptr, $rettype, Any[$(argtypes...)], Any[$(argvals...)]) + return :(let fun_ptr = @cfunction($wrapper, Int, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid})) + # use cglobal to look up the function on the calling thread + do_threadcall(fun_ptr, cglobal($f), $rettype, Any[$(argtypes...)], Any[$(argvals...)]) end) end -function do_threadcall(fun_ptr::Ptr{Cvoid}, rettype::Type, argtypes::Vector, argvals::Vector) +function do_threadcall(fun_ptr::Ptr{Cvoid}, cfptr::Ptr{Cvoid}, rettype::Type, argtypes::Vector, argvals::Vector) # generate function pointer c_notify_fun = @cfunction( function notify_fun(idx) @@ -86,8 +87,8 @@ function do_threadcall(fun_ptr::Ptr{Cvoid}, rettype::Type, argtypes::Vector, arg GC.@preserve args_arr ret_arr roots begin # queue up the work to be done ccall(:jl_queue_work, Cvoid, - (Ptr{Cvoid}, Ptr{UInt8}, Ptr{UInt8}, Ptr{Cvoid}, Cint), - fun_ptr, args_arr, ret_arr, c_notify_fun, idx) + (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{UInt8}, Ptr{UInt8}, Ptr{Cvoid}, Cint), + fun_ptr, cfptr, args_arr, ret_arr, c_notify_fun, idx) # wait for a result & return it wait(thread_notifiers[idx]) diff --git a/base/threadingconstructs.jl b/base/threadingconstructs.jl index 27096e1ba8be66..2f0d40f3d980e8 100644 --- a/base/threadingconstructs.jl +++ b/base/threadingconstructs.jl @@ -1,31 +1,71 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -export threadid, nthreads, @threads +export threadid, nthreads, @threads, @spawn, + threadpool, nthreadpools """ - Threads.threadid() + Threads.threadid() -> Int -Get the ID number of the current thread of execution. The master thread has ID `1`. +Get the ID number of the current thread of execution. The master thread has +ID `1`. """ threadid() = Int(ccall(:jl_threadid, Int16, ())+1) -# Inclusive upper bound on threadid() """ - Threads.nthreads() + Threads.nthreads([:default|:interactive]) -> Int -Get the number of threads available to the Julia process. This is the inclusive upper bound -on [`threadid()`](@ref). +Get the number of threads (across all thread pools or within the specified +thread pool) available to Julia. The number of threads across all thread +pools is the inclusive upper bound on [`threadid()`](@ref). + +See also: `BLAS.get_num_threads` and `BLAS.set_num_threads` in the +[`LinearAlgebra`](@ref man-linalg) standard library, and `nprocs()` in the +[`Distributed`](@ref man-distributed) standard library. """ +function nthreads end + nthreads() = Int(unsafe_load(cglobal(:jl_n_threads, Cint))) +function nthreads(pool::Symbol) + if pool == :default + tpid = Int8(0) + elseif pool == :interactive + tpid = Int8(1) + else + error("invalid threadpool specified") + end + return _nthreads_in_pool(tpid) +end +function _nthreads_in_pool(tpid::Int8) + p = unsafe_load(cglobal(:jl_n_threads_per_pool, Ptr{Cint})) + return Int(unsafe_load(p, tpid + 1)) +end + +""" + Threads.threadpool(tid = threadid()) -> Symbol + +Returns the specified thread's threadpool; either `:default` or `:interactive`. +""" +function threadpool(tid = threadid()) + tpid = ccall(:jl_threadpoolid, Int8, (Int16,), tid-1) + return tpid == 0 ? :default : :interactive +end -function threading_run(func) +""" + Threads.nthreadpools() -> Int + +Returns the number of threadpools currently configured. +""" +nthreadpools() = Int(unsafe_load(cglobal(:jl_n_threadpools, Cint))) + + +function threading_run(fun, static) ccall(:jl_enter_threaded_region, Cvoid, ()) n = nthreads() tasks = Vector{Task}(undef, n) for i = 1:n - t = Task(func) - t.sticky = true - ccall(:jl_set_task_tid, Cvoid, (Any, Cint), t, i-1) + t = Task(() -> fun(i)) # pass in tid + t.sticky = static + static && ccall(:jl_set_task_tid, Cint, (Any, Cint), t, i-1) tasks[i] = t schedule(t) end @@ -44,7 +84,7 @@ function _threadsfor(iter, lbody, schedule) quote local threadsfor_fun let range = $(esc(range)) - function threadsfor_fun(onethread=false) + function threadsfor_fun(tid = 1; onethread = false) r = range # Load into local variable lenr = length(r) # divide loop iterations among threads @@ -52,7 +92,6 @@ function _threadsfor(iter, lbody, schedule) tid = 1 len, rem = lenr, 0 else - tid = threadid() len, rem = divrem(lenr, nthreads()) end # not enough iterations for all the threads? @@ -82,15 +121,12 @@ function _threadsfor(iter, lbody, schedule) end end end - if threadid() != 1 || ccall(:jl_in_threaded_region, Cint, ()) != 0 - $(if schedule === :static - :(error("`@threads :static` can only be used from thread 1 and not nested")) - else - # only use threads when called from thread 1, outside @threads - :(Base.invokelatest(threadsfor_fun, true)) - end) - else - threading_run(threadsfor_fun) + if $(schedule === :dynamic || schedule === :default) + threading_run(threadsfor_fun, false) + elseif ccall(:jl_in_threaded_region, Cint, ()) != 0 # :static + error("`@threads :static` cannot be used concurrently or nested") + else # :static + threading_run(threadsfor_fun, true) end nothing end @@ -99,21 +135,109 @@ end """ Threads.@threads [schedule] for ... end -A macro to parallelize a `for` loop to run with multiple threads. Splits the iteration -space among multiple tasks and runs those tasks on threads according to a scheduling -policy. -A barrier is placed at the end of the loop which waits for all tasks to finish -execution. +A macro to execute a `for` loop in parallel. The iteration space is distributed to +coarse-grained tasks. This policy can be specified by the `schedule` argument. The +execution of the loop waits for the evaluation of all iterations. + +See also: [`@spawn`](@ref Threads.@spawn) and +`pmap` in [`Distributed`](@ref man-distributed). + +# Extended help + +## Semantics + +Unless stronger guarantees are specified by the scheduling option, the loop executed by +`@threads` macro have the following semantics. + +The `@threads` macro executes the loop body in an unspecified order and potentially +concurrently. It does not specify the exact assignments of the tasks and the worker threads. +The assignments can be different for each execution. The loop body code (including any code +transitively called from it) must not make any assumptions about the distribution of +iterations to tasks or the worker thread in which they are executed. The loop body for each +iteration must be able to make forward progress independent of other iterations and be free +from data races. As such, invalid synchronizations across iterations may deadlock while +unsynchronized memory accesses may result in undefined behavior. -The `schedule` argument can be used to request a particular scheduling policy. -The only currently supported value is `:static`, which creates one task per thread -and divides the iterations equally among them. Specifying `:static` is an error -if used from inside another `@threads` loop or from a thread other than 1. +For example, the above conditions imply that: -The default schedule (used when no `schedule` argument is present) is subject to change. +- The lock taken in an iteration *must* be released within the same iteration. +- Communicating between iterations using blocking primitives like `Channel`s is incorrect. +- Write only to locations not shared across iterations (unless a lock or atomic operation is + used). +- The value of [`threadid()`](@ref Threads.threadid) may change even within a single + iteration. + +## Schedulers + +Without the scheduler argument, the exact scheduling is unspecified and varies across Julia +releases. Currently, `:dynamic` is used when the scheduler is not specified. !!! compat "Julia 1.5" The `schedule` argument is available as of Julia 1.5. + +### `:dynamic` (default) + +`:dynamic` scheduler executes iterations dynamically to available worker threads. Current +implementation assumes that the workload for each iteration is uniform. However, this +assumption may be removed in the future. + +This scheduling option is merely a hint to the underlying execution mechanism. However, a +few properties can be expected. The number of `Task`s used by `:dynamic` scheduler is +bounded by a small constant multiple of the number of available worker threads +([`nthreads()`](@ref Threads.nthreads)). Each task processes contiguous regions of the +iteration space. Thus, `@threads :dynamic for x in xs; f(x); end` is typically more +efficient than `@sync for x in xs; @spawn f(x); end` if `length(xs)` is significantly +larger than the number of the worker threads and the run-time of `f(x)` is relatively +smaller than the cost of spawning and synchronizing a task (typically less than 10 +microseconds). + +!!! compat "Julia 1.8" + The `:dynamic` option for the `schedule` argument is available and the default as of Julia 1.8. + +### `:static` + +`:static` scheduler creates one task per thread and divides the iterations equally among +them, assigning each task specifically to each thread. In particular, the value of +[`threadid()`](@ref Threads.threadid) is guranteed to be constant within one iteration. +Specifying `:static` is an error if used from inside another `@threads` loop or from a +thread other than 1. + +!!! note + `:static` scheduling exists for supporting transition of code written before Julia 1.3. + In newly written library functions, `:static` scheduling is discouraged because the + functions using this option cannot be called from arbitrary worker threads. + +## Example + +To illustrate of the different scheduling strategies, consider the following function +`busywait` containing a non-yielding timed loop that runs for a given number of seconds. + +```julia-repl +julia> function busywait(seconds) + tstart = time_ns() + while (time_ns() - tstart) / 1e9 < seconds + end + end + +julia> @time begin + Threads.@spawn busywait(5) + Threads.@threads :static for i in 1:Threads.nthreads() + busywait(1) + end + end +6.003001 seconds (16.33 k allocations: 899.255 KiB, 0.25% compilation time) + +julia> @time begin + Threads.@spawn busywait(5) + Threads.@threads :dynamic for i in 1:Threads.nthreads() + busywait(1) + end + end +2.012056 seconds (16.05 k allocations: 883.919 KiB, 0.66% compilation time) +``` + +The `:dynamic` example takes 2 seconds since one of the non-occupied threads is able +to run two of the 1-second iterations to complete the for loop. """ macro threads(args...) na = length(args) @@ -125,7 +249,7 @@ macro threads(args...) # for now only allow quoted symbols sched = nothing end - if sched !== :static + if sched !== :static && sched !== :dynamic throw(ArgumentError("unsupported schedule argument in @threads")) end elseif na == 1 @@ -144,34 +268,63 @@ macro threads(args...) end """ - Threads.@spawn expr + Threads.@spawn [:default|:interactive] expr -Create and run a [`Task`](@ref) on any available thread. To wait for the task to -finish, call [`wait`](@ref) on the result of this macro, or call [`fetch`](@ref) -to wait and then obtain its return value. +Create a [`Task`](@ref) and [`schedule`](@ref) it to run on any available +thread in the specified threadpool (`:default` if unspecified). The task is +allocated to a thread once one becomes available. To wait for the task to +finish, call [`wait`](@ref) on the result of this macro, or call +[`fetch`](@ref) to wait and then obtain its return value. -Values can be interpolated into `@spawn` via `\$`, which copies the value directly into the -constructed underlying closure. This allows you to insert the _value_ of a variable, -isolating the asynchronous code from changes to the variable's value in the current task. +Values can be interpolated into `@spawn` via `\$`, which copies the value +directly into the constructed underlying closure. This allows you to insert +the _value_ of a variable, isolating the asynchronous code from changes to +the variable's value in the current task. !!! note - See the manual chapter on threading for important caveats. + See the manual chapter on [multi-threading](@ref man-multithreading) + for important caveats. See also the chapter on [threadpools](@ref man-threadpools). !!! compat "Julia 1.3" This macro is available as of Julia 1.3. !!! compat "Julia 1.4" Interpolating values via `\$` is available as of Julia 1.4. + +!!! compat "Julia 1.9" + A threadpool may be specified as of Julia 1.9. """ -macro spawn(expr) - letargs = Base._lift_one_interp!(expr) +macro spawn(args...) + tpid = Int8(0) + na = length(args) + if na == 2 + ttype, ex = args + if ttype isa QuoteNode + ttype = ttype.value + elseif ttype isa Symbol + # TODO: allow unquoted symbols + ttype = nothing + end + if ttype === :interactive + tpid = Int8(1) + elseif ttype !== :default + throw(ArgumentError("unsupported threadpool in @spawn: $ttype")) + end + elseif na == 1 + ex = args[1] + else + throw(ArgumentError("wrong number of arguments in @spawn")) + end + + letargs = Base._lift_one_interp!(ex) - thunk = esc(:(()->($expr))) + thunk = esc(:(()->($ex))) var = esc(Base.sync_varname) quote let $(letargs...) local task = Task($thunk) task.sticky = false + ccall(:jl_set_task_threadpoolid, Cint, (Any, Int8), task, $tpid) if $(Expr(:islocal, var)) put!($var, task) end diff --git a/base/threads_overloads.jl b/base/threads_overloads.jl index 3e6ad06760747e..a0d4bbeda22888 100644 --- a/base/threads_overloads.jl +++ b/base/threads_overloads.jl @@ -35,7 +35,7 @@ function Threads.foreach(f, channel::Channel; # do `stop[] && break` after `f(item)` to avoid losing `item`. # this isn't super comprehensive since a task could still get # stuck on `take!` at `for item in channel`. We should think - # about a more robust mechanism to avoid dropping items. See also: + # about a more robust mechanism to avoid dropping items. See also # https://github.com/JuliaLang/julia/pull/34543#discussion_r422695217 stop[] && break end diff --git a/base/timing.jl b/base/timing.jl index ac19e9a2e2ba8b..539e08e885a167 100644 --- a/base/timing.jl +++ b/base/timing.jl @@ -16,6 +16,8 @@ struct GC_Num collect ::Csize_t # GC internal pause ::Cint full_sweep ::Cint + max_pause ::Int64 + max_memory ::Int64 end gc_num() = ccall(:jl_gc_num, GC_Num, ()) @@ -40,7 +42,7 @@ function GC_Diff(new::GC_Num, old::GC_Num) # logic from `src/gc.c:jl_gc_total_bytes` old_allocd = gc_total_bytes(old) new_allocd = gc_total_bytes(new) - return GC_Diff(new_allocd - old_allocd, + return GC_Diff(new_allocd - old_allocd, new.malloc - old.malloc, new.realloc - old.realloc, new.poolalloc - old.poolalloc, @@ -55,6 +57,21 @@ function gc_alloc_count(diff::GC_Diff) diff.malloc + diff.realloc + diff.poolalloc + diff.bigalloc end +# cumulative total time spent on compilation and recompilation, in nanoseconds +function cumulative_compile_time_ns() + comp = ccall(:jl_cumulative_compile_time_ns, UInt64, ()) + recomp = ccall(:jl_cumulative_recompile_time_ns, UInt64, ()) + return comp, recomp +end + +function cumulative_compile_timing(b::Bool) + if b + ccall(:jl_cumulative_compile_timing_enable, Cvoid, ()) + else + ccall(:jl_cumulative_compile_timing_disable, Cvoid, ()) + end + return +end # total time spend in garbage collection, in nanoseconds gc_time_ns() = ccall(:jl_gc_total_hrtime, UInt64, ()) @@ -72,6 +89,16 @@ function gc_live_bytes() Int(ccall(:jl_gc_live_bytes, Int64, ())) + num.allocd + num.deferred_alloc end +""" + Base.jit_total_bytes() + +Return the total amount (in bytes) allocated by the just-in-time compiler +for e.g. native code and data. +""" +function jit_total_bytes() + return Int(ccall(:jl_jit_total_bytes, Csize_t, ())) +end + # print elapsed time, return expression value const _mem_units = ["byte", "KiB", "MiB", "GiB", "TiB", "PiB"] const _cnt_units = ["", " k", " M", " G", " T", " P"] @@ -85,14 +112,13 @@ function prettyprint_getunits(value, numunits, factor) return number, unit end -function padded_nonzero_print(value, str) - if value != 0 - blanks = " "[1:(18 - length(str))] +function padded_nonzero_print(value, str, always_print = true) + if always_print || value != 0 + blanks = " "[1:(19 - length(str))] println(str, ":", blanks, value) end end - function format_bytes(bytes) # also used by InteractiveUtils bytes, mb = prettyprint_getunits(bytes, length(_mem_units), Int64(1024)) if mb == 1 @@ -102,51 +128,90 @@ function format_bytes(bytes) # also used by InteractiveUtils end end -function time_print(elapsedtime, bytes=0, gctime=0, allocs=0) +function time_print(elapsedtime, bytes=0, gctime=0, allocs=0, compile_time=0, recompile_time=0, newline=false, _lpad=true) timestr = Ryu.writefixed(Float64(elapsedtime/1e9), 6) - length(timestr) < 10 && print(" "^(10 - length(timestr))) - print(timestr, " seconds") - if bytes != 0 || allocs != 0 - allocs, ma = prettyprint_getunits(allocs, length(_cnt_units), Int64(1000)) - if ma == 1 - print(" (", Int(allocs), _cnt_units[ma], allocs==1 ? " allocation: " : " allocations: ") - else - print(" (", Ryu.writefixed(Float64(allocs), 2), _cnt_units[ma], " allocations: ") + str = sprint() do io + _lpad && print(io, length(timestr) < 10 ? (" "^(10 - length(timestr))) : "") + print(io, timestr, " seconds") + parens = bytes != 0 || allocs != 0 || gctime > 0 || compile_time > 0 + parens && print(io, " (") + if bytes != 0 || allocs != 0 + allocs, ma = prettyprint_getunits(allocs, length(_cnt_units), Int64(1000)) + if ma == 1 + print(io, Int(allocs), _cnt_units[ma], allocs==1 ? " allocation: " : " allocations: ") + else + print(io, Ryu.writefixed(Float64(allocs), 2), _cnt_units[ma], " allocations: ") + end + print(io, format_bytes(bytes)) end - print(format_bytes(bytes)) - end - if gctime > 0 - print(", ", Ryu.writefixed(Float64(100*gctime/elapsedtime), 2), "% gc time") - end - if bytes != 0 || allocs != 0 - print(")") + if gctime > 0 + if bytes != 0 || allocs != 0 + print(io, ", ") + end + print(io, Ryu.writefixed(Float64(100*gctime/elapsedtime), 2), "% gc time") + end + if compile_time > 0 + if bytes != 0 || allocs != 0 || gctime > 0 + print(io, ", ") + end + print(io, Ryu.writefixed(Float64(100*compile_time/elapsedtime), 2), "% compilation time") + end + if recompile_time > 0 + perc = Float64(100 * recompile_time / compile_time) + # use "<1" to avoid the confusing UX of reporting 0% when it's >0% + print(io, ": ", perc < 1 ? "<1" : Ryu.writefixed(perc, 0), "% of which was recompilation") + end + parens && print(io, ")") end + newline ? println(str) : print(str) nothing end -function timev_print(elapsedtime, diff::GC_Diff) +function timev_print(elapsedtime, diff::GC_Diff, compile_times, _lpad) allocs = gc_alloc_count(diff) - time_print(elapsedtime, diff.allocd, diff.total_time, allocs) - print("\nelapsed time (ns): $elapsedtime\n") + compile_time = first(compile_times) + recompile_time = last(compile_times) + time_print(elapsedtime, diff.allocd, diff.total_time, allocs, compile_time, recompile_time, true, _lpad) + padded_nonzero_print(elapsedtime, "elapsed time (ns)") padded_nonzero_print(diff.total_time, "gc time (ns)") padded_nonzero_print(diff.allocd, "bytes allocated") padded_nonzero_print(diff.poolalloc, "pool allocs") padded_nonzero_print(diff.bigalloc, "non-pool GC allocs") - padded_nonzero_print(diff.malloc, "malloc() calls") - padded_nonzero_print(diff.realloc, "realloc() calls") - padded_nonzero_print(diff.freecall, "free() calls") - padded_nonzero_print(diff.pause, "GC pauses") + padded_nonzero_print(diff.malloc, "malloc() calls", false) + padded_nonzero_print(diff.realloc, "realloc() calls", false) + # always print number of frees if there are mallocs + padded_nonzero_print(diff.freecall, "free() calls", diff.malloc > 0) + minor_collects = diff.pause - diff.full_sweep + padded_nonzero_print(minor_collects, "minor collections") padded_nonzero_print(diff.full_sweep, "full collections") end +# Like a try-finally block, except without introducing the try scope +# NOTE: This is deprecated and should not be used from user logic. A proper solution to +# this problem will be introduced in https://github.com/JuliaLang/julia/pull/39217 +macro __tryfinally(ex, fin) + Expr(:tryfinally, + :($(esc(ex))), + :($(esc(fin))) + ) +end + """ - @time + @time expr + @time "description" expr A macro to execute an expression, printing the time it took to execute, the number of allocations, and the total number of bytes its execution caused to be allocated, before -returning the value of the expression. +returning the value of the expression. Any time spent garbage collecting (gc), compiling +new code, or recompiling invalidated code is shown as a percentage. -See also [`@timev`](@ref), [`@timed`](@ref), [`@elapsed`](@ref), and +Optionally provide a description string to print before the time report. + +In some cases the system will look inside the `@time` expression and compile some of the +called code before execution of the top-level expression begins. When that happens, some +compilation time will not be counted. To include this time you can run `@time @eval ...`. + +See also [`@showtime`](@ref), [`@timev`](@ref), [`@timed`](@ref), [`@elapsed`](@ref), and [`@allocated`](@ref). !!! note @@ -154,9 +219,20 @@ See also [`@timev`](@ref), [`@timed`](@ref), [`@elapsed`](@ref), and package which among other things evaluates the function multiple times in order to reduce noise. +!!! compat "Julia 1.8" + The option to add a description was introduced in Julia 1.8. + +!!! compat "Julia 1.9" + Recompilation time being shown separately from compilation time was introduced in Julia 1.9 + ```julia-repl -julia> @time rand(10^6); - 0.001525 seconds (7 allocations: 7.630 MiB) +julia> x = rand(10,10); + +julia> @time x * x; + 0.606588 seconds (2.19 M allocations: 116.555 MiB, 3.75% gc time, 99.94% compilation time) + +julia> @time x * x; + 0.000009 seconds (1 allocation: 896 bytes) julia> @time begin sleep(0.3) @@ -164,50 +240,122 @@ julia> @time begin end 0.301395 seconds (8 allocations: 336 bytes) 2 + +julia> @time "A one second sleep" sleep(1) +A one second sleep: 1.005750 seconds (5 allocations: 144 bytes) + +julia> for loop in 1:3 + @time loop sleep(1) + end +1: 1.006760 seconds (5 allocations: 144 bytes) +2: 1.001263 seconds (5 allocations: 144 bytes) +3: 1.003676 seconds (5 allocations: 144 bytes) ``` """ macro time(ex) quote - while false; end # compiler heuristic: compile this block (alter this if the heuristic changes) + @time nothing $(esc(ex)) + end +end +macro time(msg, ex) + quote + Experimental.@force_compile local stats = gc_num() local elapsedtime = time_ns() - local val = $(esc(ex)) - elapsedtime = time_ns() - elapsedtime + cumulative_compile_timing(true) + local compile_elapsedtimes = cumulative_compile_time_ns() + local val = @__tryfinally($(esc(ex)), + (elapsedtime = time_ns() - elapsedtime; + cumulative_compile_timing(false); + compile_elapsedtimes = cumulative_compile_time_ns() .- compile_elapsedtimes) + ) local diff = GC_Diff(gc_num(), stats) - time_print(elapsedtime, diff.allocd, diff.total_time, - gc_alloc_count(diff)) - println() + local _msg = $(esc(msg)) + local has_msg = !isnothing(_msg) + has_msg && print(_msg, ": ") + time_print(elapsedtime, diff.allocd, diff.total_time, gc_alloc_count(diff), first(compile_elapsedtimes), last(compile_elapsedtimes), true, !has_msg) val end end """ - @timev + @showtime expr + +Like `@time` but also prints the expression being evaluated for reference. + +!!! compat "Julia 1.8" + This macro was added in Julia 1.8. + +See also [`@time`](@ref). + +```julia-repl +julia> @showtime sleep(1) +sleep(1): 1.002164 seconds (4 allocations: 128 bytes) +``` +""" +macro showtime(ex) + quote + @time $(sprint(show_unquoted,ex)) $(esc(ex)) + end +end + +""" + @timev expr + @timev "description" expr This is a verbose version of the `@time` macro. It first prints the same information as `@time`, then any non-zero memory allocation counters, and then returns the value of the expression. +Optionally provide a description string to print before the time report. + +!!! compat "Julia 1.8" + The option to add a description was introduced in Julia 1.8. + See also [`@time`](@ref), [`@timed`](@ref), [`@elapsed`](@ref), and [`@allocated`](@ref). ```julia-repl -julia> @timev rand(10^6); - 0.001006 seconds (7 allocations: 7.630 MiB) -elapsed time (ns): 1005567 -bytes allocated: 8000256 -pool allocs: 6 -malloc() calls: 1 +julia> x = rand(10,10); + +julia> @timev x * x; + 0.546770 seconds (2.20 M allocations: 116.632 MiB, 4.23% gc time, 99.94% compilation time) +elapsed time (ns): 546769547 +gc time (ns): 23115606 +bytes allocated: 122297811 +pool allocs: 2197930 +non-pool GC allocs:1327 +malloc() calls: 36 +realloc() calls: 5 +GC pauses: 3 + +julia> @timev x * x; + 0.000010 seconds (1 allocation: 896 bytes) +elapsed time (ns): 9848 +bytes allocated: 896 +pool allocs: 1 ``` """ macro timev(ex) quote - while false; end # compiler heuristic: compile this block (alter this if the heuristic changes) + @timev nothing $(esc(ex)) + end +end +macro timev(msg, ex) + quote + Experimental.@force_compile local stats = gc_num() local elapsedtime = time_ns() - local val = $(esc(ex)) - elapsedtime = time_ns() - elapsedtime - timev_print(elapsedtime, GC_Diff(gc_num(), stats)) + local compile_elapsedtimes = cumulative_compile_time_ns() + local val = @__tryfinally($(esc(ex)), + (elapsedtime = time_ns() - elapsedtime; + compile_elapsedtimes = cumulative_compile_time_ns() .- compile_elapsedtimes) + ) + local diff = GC_Diff(gc_num(), stats) + local _msg = $(esc(msg)) + local has_msg = !isnothing(_msg) + has_msg && print(_msg, ": ") + timev_print(elapsedtime, diff, compile_elapsedtimes, !has_msg) val end end @@ -218,6 +366,10 @@ end A macro to evaluate an expression, discarding the resulting value, instead returning the number of seconds it took to execute as a floating-point number. +In some cases the system will look inside the `@elapsed` expression and compile some of the +called code before execution of the top-level expression begins. When that happens, some +compilation time will not be counted. To include this time you can run `@elapsed @eval ...`. + See also [`@time`](@ref), [`@timev`](@ref), [`@timed`](@ref), and [`@allocated`](@ref). @@ -228,7 +380,7 @@ julia> @elapsed sleep(0.3) """ macro elapsed(ex) quote - while false; end # compiler heuristic: compile this block (alter this if the heuristic changes) + Experimental.@force_compile local t0 = time_ns() $(esc(ex)) (time_ns() - t0) / 1e9 @@ -260,7 +412,7 @@ julia> @allocated rand(10^6) """ macro allocated(ex) quote - while false; end # compiler heuristic: compile this block (alter this if the heuristic changes) + Experimental.@force_compile local b0 = Ref{Int64}(0) local b1 = Ref{Int64}(0) gc_bytes(b0) @@ -277,6 +429,10 @@ A macro to execute an expression, and return the value of the expression, elapse total bytes allocated, garbage collection time, and an object with various memory allocation counters. +In some cases the system will look inside the `@timed` expression and compile some of the +called code before execution of the top-level expression begins. When that happens, some +compilation time will not be counted. To include this time you can run `@timed @eval ...`. + See also [`@time`](@ref), [`@timev`](@ref), [`@elapsed`](@ref), and [`@allocated`](@ref). @@ -304,7 +460,7 @@ julia> stats.gcstats.total_time """ macro timed(ex) quote - while false; end # compiler heuristic: compile this block (alter this if the heuristic changes) + Experimental.@force_compile local stats = gc_num() local elapsedtime = time_ns() local val = $(esc(ex)) diff --git a/base/toml_parser.jl b/base/toml_parser.jl index d3a0df082c3a7a..7f4662bddc4dda 100644 --- a/base/toml_parser.jl +++ b/base/toml_parser.jl @@ -1,3 +1,5 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + module TOML using Base: IdSet @@ -102,7 +104,7 @@ function Parser(str::String; filepath=nothing) IdSet{TOMLDict}(), # defined_tables root, filepath, - get(Base.loaded_modules, DATES_PKGID, nothing), + isdefined(Base, :maybe_root_module) ? Base.maybe_root_module(DATES_PKGID) : nothing, ) startup(l) return l @@ -240,7 +242,7 @@ const err_message = Dict( ErrExpectedEqualAfterKey => "expected equal sign after key", ErrNoTrailingDigitAfterDot => "expected digit after dot", ErrOverflowError => "overflowed when parsing integer", - ErrInvalidUnicodeScalar => "invalid uncidode scalar", + ErrInvalidUnicodeScalar => "invalid unicode scalar", ErrInvalidEscapeCharacter => "invalid escape character", ErrUnexpectedEofExpectedValue => "unexpected end of file, expected a value" ) @@ -278,7 +280,7 @@ const Err{T} = Union{T, ParserError} function format_error_message_for_err_type(error::ParserError) msg = err_message[error.type] if error.type == ErrInvalidBareKeyCharacter - c_escaped = escape_string(string(error.data)) + c_escaped = escape_string(string(error.data)::String) msg *= ": '$c_escaped'" end return msg @@ -319,9 +321,9 @@ function Base.showerror(io::IO, err::ParserError) printstyled(io, " error: "; color=Base.error_color()) println(io, format_error_message_for_err_type(err)) # In this case we want the arrow to point one character - pos = err.pos + pos = err.pos::Int err.type == ErrUnexpectedEofExpectedValue && (pos += 1) - str1, err1 = point_to_line(err.str, pos, pos, io) + str1, err1 = point_to_line(err.str::String, pos, pos, io) @static if VERSION <= v"1.6.0-DEV.121" # See https://github.com/JuliaLang/julia/issues/36015 format_fixer = get(io, :color, false) == true ? "\e[0m" : "" @@ -482,6 +484,7 @@ end function recurse_dict!(l::Parser, d::Dict, dotted_keys::AbstractVector{String}, check=true)::Err{TOMLDict} for i in 1:length(dotted_keys) + d = d::TOMLDict key = dotted_keys[i] d = get!(TOMLDict, d, key) if d isa Vector @@ -489,7 +492,7 @@ function recurse_dict!(l::Parser, d::Dict, dotted_keys::AbstractVector{String}, end check && @try check_allowed_add_key(l, d, i == length(dotted_keys)) end - return d + return d::TOMLDict end function check_allowed_add_key(l::Parser, d, check_defined=true)::Err{Nothing} @@ -654,12 +657,22 @@ end ######### function push!!(v::Vector, el) + # Since these types are typically non-inferrable, they are a big invalidation risk, + # and since it's used by the package-loading infrastructure the cost of invalidation + # is high. Therefore, this is written to reduce the "exposed surface area": e.g., rather + # than writing `T[el]` we write it as `push!(Vector{T}(undef, 1), el)` so that there + # is no ambiguity about what types of objects will be created. T = eltype(v) - if el isa T || typeof(el) === T + t = typeof(el) + if el isa T || t === T push!(v, el::T) return v + elseif T === Union{} + out = Vector{t}(undef, 1) + out[1] = el + return out else - if typeof(T) === Union + if T isa Union newT = Any else newT = Union{T, typeof(el)} @@ -672,7 +685,7 @@ end function parse_array(l::Parser)::Err{Vector} skip_ws_nl(l) - array = Union{}[] + array = Vector{Union{}}() empty_array = accept(l, ']') while !empty_array v = @try parse_value(l) @@ -738,7 +751,7 @@ isvalid_binary(c::Char) = '0' <= c <= '1' const ValidSigs = Union{typeof.([isvalid_hex, isvalid_oct, isvalid_binary, isdigit])...} # This function eats things accepted by `f` but also allows eating `_` in between -# digits. Retruns if it ate at lest one character and if it ate an underscore +# digits. Returns if it ate at lest one character and if it ate an underscore function accept_batch_underscore(l::Parser, f::ValidSigs, fail_if_underscore=true)::Err{Tuple{Bool, Bool}} contains_underscore = false at_least_one = false @@ -808,8 +821,6 @@ function parse_number_or_date_start(l::Parser) ate && return parse_int(l, contains_underscore) elseif accept(l, isdigit) return parse_local_time(l) - elseif peek(l) !== '.' - return ParserError(ErrLeadingZeroNotAllowedInteger) end end @@ -922,21 +933,21 @@ ok_end_value(c::Char) = iswhitespace(c) || c == '#' || c == EOF_CHAR || c == ']' accept_two(l, f::F) where {F} = accept_n(l, 2, f) || return(ParserError(ErrParsingDateTime)) function parse_datetime(l) # Year has already been eaten when we reach here - year = parse_int(l, false)::Int64 + year = @try parse_int(l, false) year in 0:9999 || return ParserError(ErrParsingDateTime) # Month accept(l, '-') || return ParserError(ErrParsingDateTime) set_marker!(l) @try accept_two(l, isdigit) - month = parse_int(l, false) + month = @try parse_int(l, false) month in 1:12 || return ParserError(ErrParsingDateTime) accept(l, '-') || return ParserError(ErrParsingDateTime) # Day set_marker!(l) @try accept_two(l, isdigit) - day = parse_int(l, false) + day = @try parse_int(l, false) # Verify the real range in the constructor below day in 1:31 || return ParserError(ErrParsingDateTime) @@ -973,9 +984,10 @@ function parse_datetime(l) end function try_return_datetime(p, year, month, day, h, m, s, ms) - if p.Dates !== nothing + Dates = p.Dates + if Dates !== nothing try - return p.Dates.DateTime(year, month, day, h, m, s, ms) + return Dates.DateTime(year, month, day, h, m, s, ms) catch return ParserError(ErrParsingDateTime) end @@ -985,9 +997,10 @@ function try_return_datetime(p, year, month, day, h, m, s, ms) end function try_return_date(p, year, month, day) - if p.Dates !== nothing + Dates = p.Dates + if Dates !== nothing try - return p.Dates.Date(year, month, day) + return Dates.Date(year, month, day) catch return ParserError(ErrParsingDateTime) end @@ -997,7 +1010,7 @@ function try_return_date(p, year, month, day) end function parse_local_time(l::Parser) - h = parse_int(l, false) + h = @try parse_int(l, false) h in 0:23 || return ParserError(ErrParsingDateTime) _, m, s, ms = @try _parse_local_time(l, true) # TODO: Could potentially parse greater accuracy for the @@ -1006,9 +1019,10 @@ function parse_local_time(l::Parser) end function try_return_time(p, h, m, s, ms) - if p.Dates !== nothing + Dates = p.Dates + if Dates !== nothing try - return p.Dates.Time(h, m, s, ms) + return Dates.Time(h, m, s, ms) catch return ParserError(ErrParsingDateTime) end @@ -1130,7 +1144,7 @@ function parse_string_continue(l::Parser, multiline::Bool, quoted::Bool)::Err{St if !accept_n(l, n, isvalid_hex) return ParserError(ErrInvalidUnicodeScalar) end - codepoint = parse_int(l, false, 16) + codepoint = parse_int(l, false, 16)::Int64 #= Unicode Scalar Value --------------------- @@ -1151,7 +1165,7 @@ function parse_string_continue(l::Parser, multiline::Bool, quoted::Bool)::Err{St end function take_chunks(l::Parser, unescape::Bool)::String - nbytes = sum(length, l.chunks) + nbytes = sum(length, l.chunks; init=0) str = Base._string_n(nbytes) offset = 1 for chunk in l.chunks diff --git a/base/ttyhascolor.jl b/base/ttyhascolor.jl index c852bd5feb62f8..5984dba6d592ef 100644 --- a/base/ttyhascolor.jl +++ b/base/ttyhascolor.jl @@ -1,3 +1,5 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + if Sys.iswindows() ttyhascolor(term_type = nothing) = true else @@ -22,4 +24,4 @@ end in(key_value::Pair{Symbol,Bool}, ::TTY) = key_value.first === :color && key_value.second === get_have_color() haskey(::TTY, key::Symbol) = key === :color getindex(::TTY, key::Symbol) = key === :color ? get_have_color() : throw(KeyError(key)) -get(::TTY, key::Symbol, default) = key === :color ? get_have_color() : default \ No newline at end of file +get(::TTY, key::Symbol, default) = key === :color ? get_have_color() : default diff --git a/base/tuple.jl b/base/tuple.jl index 691b7fb475d8e7..484a5d24e67df0 100644 --- a/base/tuple.jl +++ b/base/tuple.jl @@ -27,12 +27,15 @@ lastindex(@nospecialize t::Tuple) = length(t) size(@nospecialize(t::Tuple), d::Integer) = (d == 1) ? length(t) : throw(ArgumentError("invalid tuple dimension $d")) axes(@nospecialize t::Tuple) = (OneTo(length(t)),) @eval getindex(@nospecialize(t::Tuple), i::Int) = getfield(t, i, $(Expr(:boundscheck))) -@eval getindex(@nospecialize(t::Tuple), i::Real) = getfield(t, convert(Int, i), $(Expr(:boundscheck))) +@eval getindex(@nospecialize(t::Tuple), i::Integer) = getfield(t, convert(Int, i), $(Expr(:boundscheck))) getindex(t::Tuple, r::AbstractArray{<:Any,1}) = (eltype(t)[t[ri] for ri in r]...,) getindex(t::Tuple, b::AbstractArray{Bool,1}) = length(b) == length(t) ? getindex(t, findall(b)) : throw(BoundsError(t, b)) getindex(t::Tuple, c::Colon) = t -# returns new tuple; N.B.: becomes no-op if i is out-of-bounds +get(t::Tuple, i::Integer, default) = i in 1:length(t) ? getindex(t, i) : default +get(f::Callable, t::Tuple, i::Integer) = i in 1:length(t) ? getindex(t, i) : f() + +# returns new tuple; N.B.: becomes no-op if `i` is out-of-bounds """ setindex(c::Tuple, v, i::Integer) @@ -48,21 +51,20 @@ true """ function setindex(x::Tuple, v, i::Integer) @boundscheck 1 <= i <= length(x) || throw(BoundsError(x, i)) - @_inline_meta + @inline _setindex(v, i, x...) end -function _setindex(v, i::Integer, first, tail...) - @_inline_meta - return (ifelse(i == 1, v, first), _setindex(v, i - 1, tail...)...) +function _setindex(v, i::Integer, args...) + @inline + return ntuple(j -> ifelse(j == i, v, args[j]), length(args)) end -_setindex(v, i::Integer) = () ## iterating ## function iterate(@nospecialize(t::Tuple), i::Int=1) - @_inline_meta + @inline return (1 <= i <= length(t)) ? (@inbounds t[i], i + 1) : nothing end @@ -72,19 +74,19 @@ prevind(@nospecialize(t::Tuple), i::Integer) = Int(i)-1 nextind(@nospecialize(t::Tuple), i::Integer) = Int(i)+1 function keys(t::Tuple, t2::Tuple...) - @_inline_meta + @inline OneTo(_maxlength(t, t2...)) end _maxlength(t::Tuple) = length(t) function _maxlength(t::Tuple, t2::Tuple, t3::Tuple...) - @_inline_meta + @inline max(length(t), _maxlength(t2, t3...)) end # this allows partial evaluation of bounded sequences of next() calls on tuples, # while reducing to plain next() for arbitrary iterables. -indexed_iterate(t::Tuple, i::Int, state=1) = (@_inline_meta; (getfield(t, i), i+1)) -indexed_iterate(a::Array, i::Int, state=1) = (@_inline_meta; (a[i], i+1)) +indexed_iterate(t::Tuple, i::Int, state=1) = (@inline; (getfield(t, i), i+1)) +indexed_iterate(a::Array, i::Int, state=1) = (@inline; (a[i], i+1)) function indexed_iterate(I, i) x = iterate(I) x === nothing && throw(BoundsError(I, i)) @@ -96,6 +98,96 @@ function indexed_iterate(I, i, state) x end +""" + Base.rest(collection[, itr_state]) + +Generic function for taking the tail of `collection`, starting from a specific iteration +state `itr_state`. Return a `Tuple`, if `collection` itself is a `Tuple`, a subtype of +`AbstractVector`, if `collection` is an `AbstractArray`, a subtype of `AbstractString` +if `collection` is an `AbstractString`, and an arbitrary iterator, falling back to +`Iterators.rest(collection[, itr_state])`, otherwise. + +Can be overloaded for user-defined collection types to customize the behavior of [slurping +in assignments](@ref destructuring-assignment) in final position, like `a, b... = collection`. + +!!! compat "Julia 1.6" + `Base.rest` requires at least Julia 1.6. + +See also: [`first`](@ref first), [`Iterators.rest`](@ref), [`Base.split_rest`](@ref). + +# Examples +```jldoctest +julia> a = [1 2; 3 4] +2×2 Matrix{Int64}: + 1 2 + 3 4 + +julia> first, state = iterate(a) +(1, 2) + +julia> first, Base.rest(a, state) +(1, [3, 2, 4]) +``` +""" +function rest end +rest(t::Tuple) = t +rest(t::Tuple, i::Int) = ntuple(x -> getfield(t, x+i-1), length(t)-i+1) +rest(a::Array, i::Int=1) = a[i:end] +rest(a::Core.SimpleVector, i::Int=1) = a[i:end] +rest(itr, state...) = Iterators.rest(itr, state...) + +""" + Base.split_rest(collection, n::Int[, itr_state]) -> (rest_but_n, last_n) + +Generic function for splitting the tail of `collection`, starting from a specific iteration +state `itr_state`. Returns a tuple of two new collections. The first one contains all +elements of the tail but the `n` last ones, which make up the second collection. + +The type of the first collection generally follows that of [`Base.rest`](@ref), except that +the fallback case is not lazy, but is collected eagerly into a vector. + +Can be overloaded for user-defined collection types to customize the behavior of [slurping +in assignments](@ref destructuring-assignment) in non-final position, like `a, b..., c = collection`. + +!!! compat "Julia 1.9" + `Base.split_rest` requires at least Julia 1.9. + +See also: [`Base.rest`](@ref). + +# Examples +```jldoctest +julia> a = [1 2; 3 4] +2×2 Matrix{Int64}: + 1 2 + 3 4 + +julia> first, state = iterate(a) +(1, 2) + +julia> first, Base.split_rest(a, 1, state) +(1, ([3, 2], [4])) +``` +""" +function split_rest end +function split_rest(itr, n::Int, state...) + if IteratorSize(itr) == IsInfinite() + throw(ArgumentError("Cannot split an infinite iterator in the middle.")) + end + return _split_rest(rest(itr, state...), n) +end +_split_rest(itr, n::Int) = _split_rest(collect(itr), n) +function _check_length_split_rest(len, n) + len < n && throw(ArgumentError( + "The iterator only contains $len elements, but at least $n were requested." + )) +end +function _split_rest(a::Union{AbstractArray, Core.SimpleVector}, n::Int) + _check_length_split_rest(length(a), n) + return a[begin:end-n], a[end-n+1:end] +end + +split_rest(t::Tuple, n::Int, i=1) = t[i:end-n], t[end-n+1:end] + # Use dispatch to avoid a branch in first first(::Tuple{}) = throw(ArgumentError("tuple must be non-empty")) first(t::Tuple) = t[1] @@ -114,7 +206,7 @@ function eltype(t::Type{<:Tuple{Vararg{E}}}) where {E} end eltype(t::Type{<:Tuple}) = _compute_eltype(t) function _tuple_unique_fieldtypes(@nospecialize t) - @_pure_meta + @_total_meta types = IdSet() t´ = unwrap_unionall(t) # Given t = Tuple{Vararg{S}} where S<:Real, the various @@ -131,7 +223,7 @@ function _tuple_unique_fieldtypes(@nospecialize t) return Core.svec(types...) end function _compute_eltype(@nospecialize t) - @_pure_meta # TODO: the compiler shouldn't need this + @_total_meta # TODO: the compiler shouldn't need this types = _tuple_unique_fieldtypes(t) return afoldl(types...) do a, b # if we've already reached Any, it can't widen any more @@ -152,6 +244,8 @@ safe_tail(t::Tuple{}) = () Return a `Tuple` consisting of all but the last component of `x`. +See also: [`first`](@ref), [`tail`](@ref Base.tail). + # Examples ```jldoctest julia> Base.front((1,2,3)) @@ -162,13 +256,13 @@ ERROR: ArgumentError: Cannot call front on an empty tuple. ``` """ function front(t::Tuple) - @_inline_meta + @inline _front(t...) end _front() = throw(ArgumentError("Cannot call front on an empty tuple.")) _front(v) = () function _front(v, t...) - @_inline_meta + @inline (v, _front(t...)...) end @@ -176,16 +270,22 @@ end # 1 argument function map(f, t::Tuple{}) = () -map(f, t::Tuple{Any,}) = (f(t[1]),) -map(f, t::Tuple{Any, Any}) = (f(t[1]), f(t[2])) -map(f, t::Tuple{Any, Any, Any}) = (f(t[1]), f(t[2]), f(t[3])) -map(f, t::Tuple) = (@_inline_meta; (f(t[1]), map(f,tail(t))...)) +map(f, t::Tuple{Any,}) = (@inline; (f(t[1]),)) +map(f, t::Tuple{Any, Any}) = (@inline; (f(t[1]), f(t[2]))) +map(f, t::Tuple{Any, Any, Any}) = (@inline; (f(t[1]), f(t[2]), f(t[3]))) +map(f, t::Tuple) = (@inline; (f(t[1]), map(f,tail(t))...)) # stop inlining after some number of arguments to avoid code blowup -const Any16{N} = Tuple{Any,Any,Any,Any,Any,Any,Any,Any, - Any,Any,Any,Any,Any,Any,Any,Any,Vararg{Any,N}} -const All16{T,N} = Tuple{T,T,T,T,T,T,T,T, - T,T,T,T,T,T,T,T,Vararg{T,N}} -function map(f, t::Any16) +const Any32{N} = Tuple{Any,Any,Any,Any,Any,Any,Any,Any, + Any,Any,Any,Any,Any,Any,Any,Any, + Any,Any,Any,Any,Any,Any,Any,Any, + Any,Any,Any,Any,Any,Any,Any,Any, + Vararg{Any,N}} +const All32{T,N} = Tuple{T,T,T,T,T,T,T,T, + T,T,T,T,T,T,T,T, + T,T,T,T,T,T,T,T, + T,T,T,T,T,T,T,T, + Vararg{T,N}} +function map(f, t::Any32) n = length(t) A = Vector{Any}(undef, n) for i=1:n @@ -195,13 +295,13 @@ function map(f, t::Any16) end # 2 argument function map(f, t::Tuple{}, s::Tuple{}) = () -map(f, t::Tuple{Any,}, s::Tuple{Any,}) = (f(t[1],s[1]),) -map(f, t::Tuple{Any,Any}, s::Tuple{Any,Any}) = (f(t[1],s[1]), f(t[2],s[2])) +map(f, t::Tuple{Any,}, s::Tuple{Any,}) = (@inline; (f(t[1],s[1]),)) +map(f, t::Tuple{Any,Any}, s::Tuple{Any,Any}) = (@inline; (f(t[1],s[1]), f(t[2],s[2]))) function map(f, t::Tuple, s::Tuple) - @_inline_meta + @inline (f(t[1],s[1]), map(f, tail(t), tail(s))...) end -function map(f, t::Any16, s::Any16) +function map(f, t::Any32, s::Any32) n = length(t) A = Vector{Any}(undef, n) for i = 1:n @@ -214,10 +314,10 @@ heads(ts::Tuple...) = map(t -> t[1], ts) tails(ts::Tuple...) = map(tail, ts) map(f, ::Tuple{}...) = () function map(f, t1::Tuple, t2::Tuple, ts::Tuple...) - @_inline_meta + @inline (f(heads(t1, t2, ts...)...), map(f, tails(t1, t2, ts...)...)...) end -function map(f, t1::Any16, t2::Any16, ts::Any16...) +function map(f, t1::Any32, t2::Any32, ts::Any32...) n = length(t1) A = Vector{Any}(undef, n) for i = 1:n @@ -234,7 +334,7 @@ fill_to_length(t::Tuple{}, val, ::Val{1}) = (val,) fill_to_length(t::Tuple{Any}, val, ::Val{2}) = (t..., val) fill_to_length(t::Tuple{}, val, ::Val{2}) = (val, val) #function fill_to_length(t::Tuple, val, ::Val{N}) where {N} -# @_inline_meta +# @inline # return (t..., ntuple(i -> val, N - length(t))...) #end @@ -245,7 +345,7 @@ fill_to_length(t::Tuple{}, val, ::Val{2}) = (val, val) if nameof(@__MODULE__) === :Base function tuple_type_tail(T::Type) - @_pure_meta # TODO: this method is wrong (and not @pure) + @_total_may_throw_meta # TODO: this method is wrong (and not :total_may_throw) if isa(T, UnionAll) return UnionAll(T.var, tuple_type_tail(T.body)) elseif isa(T, Union) @@ -253,9 +353,9 @@ function tuple_type_tail(T::Type) else T.name === Tuple.name || throw(MethodError(tuple_type_tail, (T,))) if isvatuple(T) && length(T.parameters) == 1 - va = T.parameters[1] - (isa(va, DataType) && isa(va.parameters[2], Int)) || return T - return Tuple{Vararg{va.parameters[1], va.parameters[2]-1}} + va = unwrap_unionall(T.parameters[1])::Core.TypeofVararg + (isdefined(va, :N) && isa(va.N, Int)) || return T + return Tuple{Vararg{va.T, va.N-1}} end return Tuple{argtail(T.parameters...)...} end @@ -271,20 +371,24 @@ Tuple(x::Array{T,0}) where {T} = tuple(getindex(x)) _totuple(::Type{Tuple{}}, itr, s...) = () function _totuple_err(@nospecialize T) - @_noinline_meta + @noinline throw(ArgumentError("too few elements for tuple type $T")) end -function _totuple(T, itr, s...) - @_inline_meta +function _totuple(::Type{T}, itr, s::Vararg{Any,N}) where {T,N} + @inline y = iterate(itr, s...) y === nothing && _totuple_err(T) - return (convert(fieldtype(T, 1), y[1]), _totuple(tuple_type_tail(T), itr, y[2])...) + t1 = convert(fieldtype(T, 1), y[1]) + # inference may give up in recursive calls, so annotate here to force accurate return type to be propagated + rT = tuple_type_tail(T) + ts = _totuple(rT, itr, y[2])::rT + return (t1, ts...) end # use iterative algorithm for long tuples -function _totuple(T::Type{All16{E,N}}, itr) where {E,N} - len = N+16 +function _totuple(T::Type{All32{E,N}}, itr) where {E,N} + len = N+32 elts = collect(E, Iterators.take(itr,len)) if length(elts) != len _totuple_err(T) @@ -303,20 +407,38 @@ _totuple(::Type{Tuple}, itr::NamedTuple) = (itr...,) end +## find ## + +_findfirst_rec(f, i::Int, ::Tuple{}) = nothing +_findfirst_rec(f, i::Int, t::Tuple) = (@inline; f(first(t)) ? i : _findfirst_rec(f, i+1, tail(t))) +function _findfirst_loop(f::Function, t) + for i in 1:length(t) + f(t[i]) && return i + end + return nothing +end +findfirst(f::Function, t::Tuple) = length(t) < 32 ? _findfirst_rec(f, 1, t) : _findfirst_loop(f, t) + +function findlast(f::Function, x::Tuple) + r = findfirst(f, reverse(x)) + return isnothing(r) ? r : length(x) - r + 1 +end + ## filter ## -filter(f, xs::Tuple) = afoldl((ys, x) -> f(x) ? (ys..., x) : ys, (), xs...) +filter_rec(f, xs::Tuple) = afoldl((ys, x) -> f(x) ? (ys..., x) : ys, (), xs...) # use Array for long tuples -filter(f, t::Any16) = Tuple(filter(f, collect(t))) +filter(f, t::Tuple) = length(t) < 32 ? filter_rec(f, t) : Tuple(filter(f, collect(t))) ## comparison ## -isequal(t1::Tuple, t2::Tuple) = (length(t1) == length(t2)) && _isequal(t1, t2) -_isequal(t1::Tuple{}, t2::Tuple{}) = true -_isequal(t1::Tuple{Any}, t2::Tuple{Any}) = isequal(t1[1], t2[1]) -_isequal(t1::Tuple, t2::Tuple) = isequal(t1[1], t2[1]) && _isequal(tail(t1), tail(t2)) -function _isequal(t1::Any16, t2::Any16) +isequal(t1::Tuple, t2::Tuple) = length(t1) == length(t2) && _isequal(t1, t2) +_isequal(::Tuple{}, ::Tuple{}) = true +function _isequal(t1::Tuple{Any,Vararg{Any}}, t2::Tuple{Any,Vararg{Any}}) + return isequal(t1[1], t2[1]) && _isequal(tail(t1), tail(t2)) +end +function _isequal(t1::Any32, t2::Any32) for i = 1:length(t1) if !isequal(t1[i], t2[i]) return false @@ -346,7 +468,7 @@ function _eq_missing(t1::Tuple, t2::Tuple) return _eq_missing(tail(t1), tail(t2)) end end -function _eq(t1::Any16, t2::Any16) +function _eq(t1::Any32, t2::Any32) anymissing = false for i = 1:length(t1) eq = (t1[i] == t2[i]) @@ -362,7 +484,7 @@ end const tuplehash_seed = UInt === UInt64 ? 0x77cfa1eef01bca90 : 0xf01bca90 hash(::Tuple{}, h::UInt) = h + tuplehash_seed hash(t::Tuple, h::UInt) = hash(t[1], hash(tail(t), h)) -function hash(t::Any16, h::UInt) +function hash(t::Any32, h::UInt) out = h + tuplehash_seed for i = length(t):-1:1 out = hash(t[i], out) @@ -383,7 +505,7 @@ function <(t1::Tuple, t2::Tuple) end return tail(t1) < tail(t2) end -function <(t1::Any16, t2::Any16) +function <(t1::Any32, t2::Any32) n1, n2 = length(t1), length(t2) for i = 1:min(n1, n2) a, b = t1[i], t2[i] @@ -410,7 +532,7 @@ function isless(t1::Tuple, t2::Tuple) a, b = t1[1], t2[1] isless(a, b) || (isequal(a, b) && isless(tail(t1), tail(t2))) end -function isless(t1::Any16, t2::Any16) +function isless(t1::Any32, t2::Any32) n1, n2 = length(t1), length(t2) for i = 1:min(n1, n2) a, b = t1[i], t2[i] @@ -433,17 +555,12 @@ reverse(t::Tuple) = revargs(t...) ## specialized reduction ## -# TODO: these definitions cannot yet be combined, since +(x...) -# where x might be any tuple matches too many methods. -# TODO: this is inconsistent with the regular sum in cases where the arguments -# require size promotion to system size. -sum(x::Tuple{Any, Vararg{Any}}) = +(x...) - -# NOTE: should remove, but often used on array sizes -# TODO: this is inconsistent with the regular prod in cases where the arguments -# require size promotion to system size. prod(x::Tuple{}) = 1 -prod(x::Tuple{Any, Vararg{Any}}) = *(x...) +# This is consistent with the regular prod because there is no need for size promotion +# if all elements in the tuple are of system size. +# It is defined here separately in order to support bootstrap, because it's needed earlier +# than the general prod definition is available. +prod(x::Tuple{Int, Vararg{Int}}) = *(x...) all(x::Tuple{}) = true all(x::Tuple{Bool}) = x[1] @@ -459,25 +576,21 @@ any(x::Tuple{Bool, Bool, Bool}) = x[1]|x[2]|x[3] # equivalent to any(f, t), to be used only in bootstrap _tuple_any(f::Function, t::Tuple) = _tuple_any(f, false, t...) function _tuple_any(f::Function, tf::Bool, a, b...) - @_inline_meta + @inline _tuple_any(f, tf | f(a), b...) end _tuple_any(f::Function, tf::Bool) = tf # a version of `in` esp. for NamedTuple, to make it pure, and not compiled for each tuple length -function sym_in(x::Symbol, itr::Tuple{Vararg{Symbol}}) - @nospecialize itr - @_pure_meta +function sym_in(x::Symbol, @nospecialize itr::Tuple{Vararg{Symbol}}) + @_total_meta for y in itr y === x && return true end return false end -function in(x::Symbol, itr::Tuple{Vararg{Symbol}}) - @nospecialize itr - return sym_in(x, itr) -end +in(x::Symbol, @nospecialize itr::Tuple{Vararg{Symbol}}) = sym_in(x, itr) """ @@ -486,3 +599,6 @@ end Returns an empty tuple, `()`. """ empty(@nospecialize x::Tuple) = () + +foreach(f, itr::Tuple) = foldl((_, x) -> (f(x); nothing), itr, init=nothing) +foreach(f, itrs::Tuple...) = foldl((_, xs) -> (f(xs...); nothing), zip(itrs...), init=nothing) diff --git a/base/twiceprecision.jl b/base/twiceprecision.jl index 1490a0624c7d60..860f2d23185cc6 100644 --- a/base/twiceprecision.jl +++ b/base/twiceprecision.jl @@ -63,7 +63,7 @@ representation, even though it is exact from the standpoint of binary representation. Example: -```julia +```julia-repl julia> 1.0 + 1.0001e-15 1.000000000000001 @@ -94,7 +94,7 @@ numbers. Mathematically, `zhi + zlo = x * y`, where `zhi` contains the most significant bits and `zlo` the least significant. Example: -```julia +```julia-repl julia> x = Float32(π) 3.1415927f0 @@ -126,7 +126,7 @@ numbers. Mathematically, `zhi + zlo ≈ x / y`, where `zhi` contains the most significant bits and `zlo` the least significant. Example: -```julia +```julia-repl julia> x, y = Float32(π), 3.1f0 (3.1415927f0, 3.1f0) @@ -162,7 +162,18 @@ div12(x, y) = div12(promote(x, y)...) TwicePrecision{T}((num, denom)) A number with twice the precision of `T`, e.g., quad-precision if `T = -Float64`. `hi` represents the high bits (most significant bits) and +Float64`. + +!!! warn + `TwicePrecision` is an internal type used to increase the + precision of floating-point ranges, and not intended for external use. + If you encounter them in real code, the most likely explanation is + that you are directly accessing the fields of a range. Use + the function interface instead, `step(r)` rather than `r.step` + +# Extended help + +`hi` represents the high bits (most significant bits) and `lo` the low bits (least significant bits). Rational values `num//denom` can be approximated conveniently using the syntax `TwicePrecision{T}((num, denom))`. @@ -194,6 +205,10 @@ function TwicePrecision{T}(x) where {T} TwicePrecision{T}(xT, T(Δx)) end +function TwicePrecision{T}(x::TwicePrecision) where {T} + TwicePrecision{T}(x.hi, x.lo) +end + TwicePrecision{T}(i::Integer) where {T<:AbstractFloat} = TwicePrecision{T}(canonicalize2(splitprec(T, i)...)...) @@ -207,13 +222,21 @@ end function TwicePrecision{T}(nd::Tuple{Any,Any}) where {T} n, d = nd - TwicePrecision{T}(n) / d + TwicePrecision{T}(TwicePrecision{T}(n) / d) end function TwicePrecision{T}(nd::Tuple{I,I}, nb::Integer) where {T,I} twiceprecision(TwicePrecision{T}(nd), nb) end +# Fix #39798 +# See steprangelen_hp(::Type{Float64}, ref::Tuple{Integer,Integer}, +# step::Tuple{Integer,Integer}, nb::Integer, +# len::Integer, offset::Integer) +function TwicePrecision{T}(nd::Tuple{Integer,Integer}, nb::Integer) where T + twiceprecision(TwicePrecision{T}(nd), nb) +end + # Truncating constructors. Useful for generating values that can be # exactly multiplied by small integers. function twiceprecision(val::T, nb::Integer) where {T<:IEEEFloat} @@ -321,13 +344,13 @@ function steprangelen_hp(::Type{Float64}, ref::Tuple{Integer,Integer}, step::Tuple{Integer,Integer}, nb::Integer, len::Integer, offset::Integer) StepRangeLen(TwicePrecision{Float64}(ref), - TwicePrecision{Float64}(step, nb), Int(len), offset) + TwicePrecision{Float64}(step, nb), len, offset) end function steprangelen_hp(::Type{T}, ref::Tuple{Integer,Integer}, step::Tuple{Integer,Integer}, nb::Integer, len::Integer, offset::Integer) where {T<:IEEEFloat} - StepRangeLen{T}(ref[1]/ref[2], step[1]/step[2], Int(len), offset) + StepRangeLen{T}(ref[1]/ref[2], step[1]/step[2], len, offset) end # AbstractFloat constructors (can supply a single number or a 2-tuple @@ -339,14 +362,13 @@ function steprangelen_hp(::Type{Float64}, ref::F_or_FF, step::F_or_FF, nb::Integer, len::Integer, offset::Integer) StepRangeLen(TwicePrecision{Float64}(ref...), - twiceprecision(TwicePrecision{Float64}(step...), nb), Int(len), offset) + twiceprecision(TwicePrecision{Float64}(step...), nb), len, offset) end function steprangelen_hp(::Type{T}, ref::F_or_FF, step::F_or_FF, nb::Integer, len::Integer, offset::Integer) where {T<:IEEEFloat} - StepRangeLen{T}(asF64(ref), - asF64(step), Int(len), offset) + StepRangeLen{T}(asF64(ref), asF64(step), len, offset) end @@ -357,33 +379,36 @@ StepRangeLen(ref::TwicePrecision{T}, step::TwicePrecision{T}, # Construct range for rational start=start_n/den, step=step_n/den function floatrange(::Type{T}, start_n::Integer, step_n::Integer, len::Integer, den::Integer) where T + len = len + 0 # promote with Int if len < 2 || step_n == 0 - return steprangelen_hp(T, (start_n, den), (step_n, den), 0, Int(len), 1) + return steprangelen_hp(T, (start_n, den), (step_n, den), 0, len, oneunit(len)) end # index of smallest-magnitude value - imin = clamp(round(Int, -start_n/step_n+1), 1, Int(len)) + L = typeof(len) + imin = clamp(round(typeof(len), -start_n/step_n+1), oneunit(L), len) # Compute smallest-magnitude element to 2x precision ref_n = start_n+(imin-1)*step_n # this shouldn't overflow, so don't check nb = nbitslen(T, len, imin) - steprangelen_hp(T, (ref_n, den), (step_n, den), nb, Int(len), imin) + steprangelen_hp(T, (ref_n, den), (step_n, den), nb, len, imin) end function floatrange(a::AbstractFloat, st::AbstractFloat, len::Real, divisor::AbstractFloat) + len = len + 0 # promote with Int T = promote_type(typeof(a), typeof(st), typeof(divisor)) m = maxintfloat(T, Int) if abs(a) <= m && abs(st) <= m && abs(divisor) <= m ia, ist, idivisor = round(Int, a), round(Int, st), round(Int, divisor) if ia == a && ist == st && idivisor == divisor # We can return the high-precision range - return floatrange(T, ia, ist, Int(len), idivisor) + return floatrange(T, ia, ist, len, idivisor) end end # Fallback (misses the opportunity to set offset different from 1, # but otherwise this is still high-precision) - steprangelen_hp(T, (a,divisor), (st,divisor), nbitslen(T, len, 1), Int(len), 1) + steprangelen_hp(T, (a,divisor), (st,divisor), nbitslen(T, len, 1), len, oneunit(len)) end -function (:)(start::T, step::T, stop::T) where T<:Union{Float16,Float32,Float64} +function (:)(start::T, step::T, stop::T) where T<:IEEEFloat step == 0 && throw(ArgumentError("range step cannot be zero")) # see if the inputs have exact rational approximations (and if so, # perform all computations in terms of the rationals) @@ -399,7 +424,7 @@ function (:)(start::T, step::T, stop::T) where T<:Union{Float16,Float32,Float64} rem(den, start_d) == 0 && rem(den, step_d) == 0 # check lcm overflow start_n = round(Int, start*den) step_n = round(Int, step*den) - len = max(0, div(den*stop_n - stop_d*start_n + step_n*stop_d, step_n*stop_d)) + len = max(0, Int(div(den*stop_n - stop_d*start_n + step_n*stop_d, step_n*stop_d))) # Integer ops could overflow, so check that this makes sense if isbetween(start, start + (len-1)*step, stop + step/2) && !isbetween(start, start + len*step, stop) @@ -410,6 +435,7 @@ function (:)(start::T, step::T, stop::T) where T<:Union{Float16,Float32,Float64} end end # Fallback, taking start and step literally + # n.b. we use Int as the default length type for IEEEFloats lf = (stop-start)/step if lf < 0 len = 0 @@ -427,7 +453,17 @@ end step(r::StepRangeLen{T,TwicePrecision{T},TwicePrecision{T}}) where {T<:AbstractFloat} = T(r.step) step(r::StepRangeLen{T,TwicePrecision{T},TwicePrecision{T}}) where {T} = T(r.step) -function _range(a::T, st::T, ::Nothing, len::Integer) where T<:Union{Float16,Float32,Float64} +range_start_step_length(a::Real, st::IEEEFloat, len::Integer) = + range_start_step_length(promote(a, st)..., len) + +range_start_step_length(a::IEEEFloat, st::Real, len::Integer) = + range_start_step_length(promote(a, st)..., len) + +range_start_step_length(a::IEEEFloat, st::IEEEFloat, len::Integer) = + range_start_step_length(promote(a, st)..., len) + +function range_start_step_length(a::T, st::T, len::Integer) where T<:IEEEFloat + len = len + 0 # promote with Int start_n, start_d = rat(a) step_n, step_d = rat(st) if start_d != 0 && step_d != 0 && @@ -444,10 +480,22 @@ function _range(a::T, st::T, ::Nothing, len::Integer) where T<:Union{Float16,Flo steprangelen_hp(T, a, st, 0, len, 1) end +range_step_stop_length(step::Real, stop::IEEEFloat, len::Integer) = + range_step_stop_length(promote(step, stop)..., len) + +range_step_stop_length(step::IEEEFloat, stop::Real, len::Integer) = + range_step_stop_length(promote(step, stop)..., len) + +function range_step_stop_length(step::IEEEFloat, stop::IEEEFloat, len::Integer) + r = range_start_step_length(stop, negate(step), len) + reverse(r) +end + # This assumes that r.step has already been split so that (0:len-1)*r.step.hi is exact function unsafe_getindex(r::StepRangeLen{T,<:TwicePrecision,<:TwicePrecision}, i::Integer) where T # Very similar to _getindex_hiprec, but optimized to avoid a 2nd call to add12 - @_inline_meta + @inline + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) u = i - r.offset shift_hi, shift_lo = u*r.step.hi, u*r.step.lo x_hi, x_lo = add12(r.ref.hi, shift_hi) @@ -455,6 +503,7 @@ function unsafe_getindex(r::StepRangeLen{T,<:TwicePrecision,<:TwicePrecision}, i end function _getindex_hiprec(r::StepRangeLen{<:Any,<:TwicePrecision,<:TwicePrecision}, i::Integer) + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) u = i - r.offset shift_hi, shift_lo = u*r.step.hi, u*r.step.lo x_hi, x_lo = add12(r.ref.hi, shift_hi) @@ -462,20 +511,41 @@ function _getindex_hiprec(r::StepRangeLen{<:Any,<:TwicePrecision,<:TwicePrecisio TwicePrecision(x_hi, x_lo) end -function getindex(r::StepRangeLen{T,<:TwicePrecision,<:TwicePrecision}, s::OrdinalRange{<:Integer}) where T +function getindex(r::StepRangeLen{T,<:TwicePrecision,<:TwicePrecision}, s::OrdinalRange{S}) where {T, S<:Integer} @boundscheck checkbounds(r, s) - soffset = 1 + round(Int, (r.offset - first(s))/step(s)) - soffset = clamp(soffset, 1, length(s)) - ioffset = first(s) + (soffset-1)*step(s) - if step(s) == 1 || length(s) < 2 - newstep = r.step - else - newstep = twiceprecision(r.step*step(s), nbitslen(T, length(s), soffset)) - end - if ioffset == r.offset - StepRangeLen(r.ref, newstep, length(s), max(1,soffset)) + len = length(s) + L = typeof(len) + sstep = step_hp(s) + rstep = step_hp(r) + if S === Bool + #rstep *= one(sstep) + if len == 0 + return StepRangeLen{T}(first(r), rstep, zero(L), oneunit(L)) + elseif len == 1 + if first(s) + return StepRangeLen{T}(first(r), rstep, oneunit(L), oneunit(L)) + else + return StepRangeLen{T}(first(r), rstep, zero(L), oneunit(L)) + end + else # len == 2 + return StepRangeLen{T}(last(r), step(r), oneunit(L), oneunit(L)) + end else - StepRangeLen(r.ref + (ioffset-r.offset)*r.step, newstep, length(s), max(1,soffset)) + soffset = round(L, (r.offset - first(s))/sstep + 1) + soffset = clamp(soffset, oneunit(L), len) + ioffset = L(first(s) + (soffset - oneunit(L)) * sstep) + if sstep == 1 || len < 2 + newstep = rstep #* one(sstep) + else + newstep = rstep * sstep + newstep = twiceprecision(newstep, nbitslen(T, len, soffset)) + end + soffset = max(oneunit(L), soffset) + if ioffset == r.offset + return StepRangeLen{T}(r.ref, newstep, len, soffset) + else + return StepRangeLen{T}(r.ref + (ioffset-r.offset)*rstep, newstep, len, soffset) + end end end @@ -485,30 +555,30 @@ end /(r::StepRangeLen{<:Real,<:TwicePrecision}, x::Real) = StepRangeLen(r.ref/x, twiceprecision(r.step/x, nbitslen(r)), length(r), r.offset) -StepRangeLen{T,R,S}(r::StepRangeLen{T,R,S}) where {T<:AbstractFloat,R<:TwicePrecision,S<:TwicePrecision} = r +StepRangeLen{T,R,S,L}(r::StepRangeLen{T,R,S,L}) where {T<:AbstractFloat,R<:TwicePrecision,S<:TwicePrecision,L} = r -StepRangeLen{T,R,S}(r::StepRangeLen) where {T<:AbstractFloat,R<:TwicePrecision,S<:TwicePrecision} = - _convertSRL(StepRangeLen{T,R,S}, r) +StepRangeLen{T,R,S,L}(r::StepRangeLen) where {T<:AbstractFloat,R<:TwicePrecision,S<:TwicePrecision,L} = + _convertSRL(StepRangeLen{T,R,S,L}, r) StepRangeLen{Float64}(r::StepRangeLen) = - _convertSRL(StepRangeLen{Float64,TwicePrecision{Float64},TwicePrecision{Float64}}, r) + _convertSRL(StepRangeLen{Float64,TwicePrecision{Float64},TwicePrecision{Float64},Int}, r) StepRangeLen{T}(r::StepRangeLen) where {T<:IEEEFloat} = - _convertSRL(StepRangeLen{T,Float64,Float64}, r) + _convertSRL(StepRangeLen{T,Float64,Float64,Int}, r) StepRangeLen{Float64}(r::AbstractRange) = - _convertSRL(StepRangeLen{Float64,TwicePrecision{Float64},TwicePrecision{Float64}}, r) + _convertSRL(StepRangeLen{Float64,TwicePrecision{Float64},TwicePrecision{Float64},Int}, r) StepRangeLen{T}(r::AbstractRange) where {T<:IEEEFloat} = - _convertSRL(StepRangeLen{T,Float64,Float64}, r) + _convertSRL(StepRangeLen{T,Float64,Float64,Int}, r) -function _convertSRL(::Type{StepRangeLen{T,R,S}}, r::StepRangeLen{<:Integer}) where {T,R,S} - StepRangeLen{T,R,S}(R(r.ref), S(r.step), length(r), r.offset) +function _convertSRL(::Type{StepRangeLen{T,R,S,L}}, r::StepRangeLen{<:Integer}) where {T,R,S,L} + StepRangeLen{T,R,S,L}(R(r.ref), S(r.step), L(length(r)), L(r.offset)) end -function _convertSRL(::Type{StepRangeLen{T,R,S}}, r::AbstractRange{<:Integer}) where {T,R,S} - StepRangeLen{T,R,S}(R(first(r)), S(step(r)), length(r)) +function _convertSRL(::Type{StepRangeLen{T,R,S,L}}, r::AbstractRange{<:Integer}) where {T,R,S,L} + StepRangeLen{T,R,S,L}(R(first(r)), S(step(r)), L(length(r))) end -function _convertSRL(::Type{StepRangeLen{T,R,S}}, r::AbstractRange{U}) where {T,R,S,U} +function _convertSRL(::Type{StepRangeLen{T,R,S,L}}, r::AbstractRange{U}) where {T,R,S,L,U} # if start and step have a rational approximation in the old type, # then we transfer that rational approximation to the new type f, s = first(r), step(r) @@ -522,17 +592,17 @@ function _convertSRL(::Type{StepRangeLen{T,R,S}}, r::AbstractRange{U}) where {T, rem(den, start_d) == 0 && rem(den, step_d) == 0 start_n = round(Int, f*den) step_n = round(Int, s*den) - return floatrange(T, start_n, step_n, length(r), den) + return floatrange(T, start_n, step_n, L(length(r)), den) end end - __convertSRL(StepRangeLen{T,R,S}, r) + return __convertSRL(StepRangeLen{T,R,S,L}, r) end -function __convertSRL(::Type{StepRangeLen{T,R,S}}, r::StepRangeLen{U}) where {T,R,S,U} - StepRangeLen{T,R,S}(R(r.ref), S(r.step), length(r), r.offset) +function __convertSRL(::Type{StepRangeLen{T,R,S,L}}, r::StepRangeLen{U}) where {T,R,S,L,U} + StepRangeLen{T,R,S,L}(R(r.ref), S(r.step), L(length(r)), L(r.offset)) end -function __convertSRL(::Type{StepRangeLen{T,R,S}}, r::AbstractRange{U}) where {T,R,S,U} - StepRangeLen{T,R,S}(R(first(r)), S(step(r)), length(r)) +function __convertSRL(::Type{StepRangeLen{T,R,S,L}}, r::AbstractRange{U}) where {T,R,S,L,U} + StepRangeLen{T,R,S,L}(R(first(r)), S(step(r)), L(length(r))) end function sum(r::StepRangeLen) @@ -543,7 +613,7 @@ function sum(r::StepRangeLen) np, nn = l - r.offset, r.offset - 1 # positive, negative # To prevent overflow in sum(1:n), multiply its factors by the step sp, sn = sumpair(np), sumpair(nn) - W = widen(Int) + W = widen(typeof(l)) Δn = W(sp[1]) * W(sp[2]) - W(sn[1]) * W(sn[2]) s = r.step * Δn # Add in contributions of ref @@ -579,19 +649,20 @@ function +(r1::StepRangeLen{T,R}, r2::StepRangeLen{T,R}) where T where R<:TwiceP imid = r1.offset ref = r1.ref + r2.ref else - imid = round(Int, (r1.offset+r2.offset)/2) + imid = round(typeof(len), (r1.offset+r2.offset)/2) ref1mid = _getindex_hiprec(r1, imid) ref2mid = _getindex_hiprec(r2, imid) ref = ref1mid + ref2mid end step = twiceprecision(r1.step + r2.step, nbitslen(T, len, imid)) - StepRangeLen{T,typeof(ref),typeof(step)}(ref, step, len, imid) + StepRangeLen{T,typeof(ref),typeof(step),typeof(len)}(ref, step, len, imid) end ## LinRange # For Float16, Float32, and Float64, this returns a StepRangeLen -function _range(start::T, ::Nothing, stop::T, len::Integer) where {T<:IEEEFloat} +function range_start_stop_length(start::T, stop::T, len::Integer) where {T<:IEEEFloat} + len = len + 0 # promote with Int len < 2 && return _linspace1(T, start, stop, len) if start == stop return steprangelen_hp(T, start, zero(T), 0, len, 1) @@ -614,32 +685,35 @@ function _range(start::T, ::Nothing, stop::T, len::Integer) where {T<:IEEEFloat} end function _linspace(start::T, stop::T, len::Integer) where {T<:IEEEFloat} + len = len + 0 # promote with Int (isfinite(start) && isfinite(stop)) || throw(ArgumentError("start and stop must be finite, got $start and $stop")) # Find the index that returns the smallest-magnitude element Δ, Δfac = stop-start, 1 if !isfinite(Δ) # handle overflow for large endpoints - Δ, Δfac = stop/len - start/len, Int(len) + Δ, Δfac = stop/len - start/len, len end tmin = -(start/Δ)/Δfac # t such that (1-t)*start + t*stop == 0 - imin = round(Int, tmin*(len-1)+1) # index approximately corresponding to t + L = typeof(len) + lenn1 = len - oneunit(L) + imin = round(L, tmin*lenn1 + 1) # index approximately corresponding to t if 1 < imin < len # The smallest-magnitude element is in the interior - t = (imin-1)/(len-1) + t = (imin - 1)/lenn1 ref = T((1-t)*start + t*stop) step = imin-1 < len-imin ? (ref-start)/(imin-1) : (stop-ref)/(len-imin) elseif imin <= 1 - imin = 1 + imin = oneunit(L) ref = start - step = (Δ/(len-1))*Δfac + step = (Δ/(lenn1))*Δfac else - imin = Int(len) + imin = len ref = stop - step = (Δ/(len-1))*Δfac + step = (Δ/(lenn1))*Δfac end if len == 2 && !isfinite(step) # For very large endpoints where step overflows, exploit the # split-representation to handle the overflow - return steprangelen_hp(T, start, (-start, stop), 0, 2, 1) + return steprangelen_hp(T, start, (-start, stop), 0, len, oneunit(L)) end # 2x calculations to get high precision endpoint matching while also # preventing overflow in ref_hi+(i-offset)*step_hi @@ -652,23 +726,28 @@ function _linspace(start::T, stop::T, len::Integer) where {T<:IEEEFloat} a, b = (start - x1_hi) - x1_lo, (stop - x2_hi) - x2_lo step_lo = (b - a)/(len - 1) ref_lo = a - (1 - imin)*step_lo - steprangelen_hp(T, (ref, ref_lo), (step_hi, step_lo), 0, Int(len), imin) + steprangelen_hp(T, (ref, ref_lo), (step_hi, step_lo), 0, len, imin) end # range for rational numbers, start = start_n/den, stop = stop_n/den # Note this returns a StepRangeLen -_linspace(::Type{T}, start::Integer, stop::Integer, len::Integer) where {T<:IEEEFloat} = _linspace(T, start, stop, len, 1) +_linspace(::Type{T}, start::Integer, stop::Integer, len::Integer) where {T<:IEEEFloat} = _linspace(T, start, stop, len, one(start)) function _linspace(::Type{T}, start_n::Integer, stop_n::Integer, len::Integer, den::Integer) where T<:IEEEFloat + len = len + 0 # promote with Int len < 2 && return _linspace1(T, start_n/den, stop_n/den, len) - start_n == stop_n && return steprangelen_hp(T, (start_n, den), (zero(start_n), den), 0, len, 1) + L = typeof(len) + start_n == stop_n && return steprangelen_hp(T, (start_n, den), (zero(start_n), den), 0, len, oneunit(L)) tmin = -start_n/(Float64(stop_n) - Float64(start_n)) - imin = round(Int, tmin*(len-1)+1) - imin = clamp(imin, 1, Int(len)) - ref_num = Int128(len-imin) * start_n + Int128(imin-1) * stop_n - ref_denom = Int128(len-1) * den + imin = round(typeof(len), tmin*(len-1)+1) + imin = clamp(imin, oneunit(L), len) + W = widen(L) + start_n = W(start_n) + stop_n = W(stop_n) + ref_num = W(len-imin) * start_n + W(imin-1) * stop_n + ref_denom = W(len-1) * den ref = (ref_num, ref_denom) - step_full = (Int128(stop_n) - Int128(start_n), ref_denom) - steprangelen_hp(T, ref, step_full, nbitslen(T, len, imin), Int(len), imin) + step_full = (stop_n - start_n, ref_denom) + steprangelen_hp(T, ref, step_full, nbitslen(T, len, imin), len, imin) end # For len < 2 @@ -680,7 +759,7 @@ function _linspace1(::Type{T}, start, stop, len::Integer) where T<:IEEEFloat # The output type must be consistent with steprangelen_hp if T<:Union{Float32,Float16} return StepRangeLen{T}(Float64(start), Float64(start) - Float64(stop), len, 1) - else + else # T == Float64 return StepRangeLen(TwicePrecision(start, zero(T)), TwicePrecision(start, -stop), len, 1) end end @@ -689,8 +768,8 @@ end ### Numeric utilities -# Approximate x with a rational representation. Guaranteed to return, -# but not guaranteed to return a precise answer. +# Approximate x with a rational representation as a pair of Int values. +# Guaranteed to return, but not guaranteed to return a precise answer. # https://en.wikipedia.org/wiki/Continued_fraction#Best_rational_approximations function rat(x) y = x @@ -698,7 +777,7 @@ function rat(x) b = c = 0 m = maxintfloat(narrow(typeof(x)), Int) while abs(y) <= m - f = trunc(Int,y) + f = trunc(Int, y) y -= f a, c = f*a + c, a b, d = f*b + d, b @@ -718,7 +797,7 @@ narrow(::Type{Float32}) = Float16 narrow(::Type{Float16}) = Float16 function _tp_prod(t::TwicePrecision, x, y...) - @_inline_meta + @inline _tp_prod(t * x, y...) end _tp_prod(t::TwicePrecision) = t diff --git a/base/util.jl b/base/util.jl index e9db6af3150b72..df9e29790deb66 100644 --- a/base/util.jl +++ b/base/util.jl @@ -18,6 +18,7 @@ const text_colors = Dict{Union{Symbol,Int},String}( :light_blue => "\033[94m", :light_magenta => "\033[95m", :light_cyan => "\033[96m", + :light_white => "\033[97m", :normal => "\033[0m", :default => "\033[39m", :bold => "\033[1m", @@ -67,7 +68,9 @@ Printing with the color `:nothing` will print the string without modifications. """ text_colors -function with_output_color(@nospecialize(f::Function), color::Union{Int, Symbol}, io::IO, args...; bold::Bool = false) +function with_output_color(@nospecialize(f::Function), color::Union{Int, Symbol}, io::IO, args...; + bold::Bool = false, underline::Bool = false, blink::Bool = false, + reverse::Bool = false, hidden::Bool = false) buf = IOBuffer() iscolor = get(io, :color, false)::Bool try f(IOContext(buf, io), args...) @@ -77,12 +80,25 @@ function with_output_color(@nospecialize(f::Function), color::Union{Int, Symbol} print(io, str) else bold && color === :bold && (color = :nothing) + underline && color === :underline && (color = :nothing) + blink && color === :blink && (color = :nothing) + reverse && color === :reverse && (color = :nothing) + hidden && color === :hidden && (color = :nothing) enable_ansi = get(text_colors, color, text_colors[:default]) * - (bold ? text_colors[:bold] : "") - disable_ansi = (bold ? disable_text_style[:bold] : "") * + (bold ? text_colors[:bold] : "") * + (underline ? text_colors[:underline] : "") * + (blink ? text_colors[:blink] : "") * + (reverse ? text_colors[:reverse] : "") * + (hidden ? text_colors[:hidden] : "") + + disable_ansi = (hidden ? disable_text_style[:hidden] : "") * + (reverse ? disable_text_style[:reverse] : "") * + (blink ? disable_text_style[:blink] : "") * + (underline ? disable_text_style[:underline] : "") * + (bold ? disable_text_style[:bold] : "") * get(disable_text_style, color, text_colors[:default]) first = true - for line in split(str, '\n') + for line in eachsplit(str, '\n') first || print(buf, '\n') first = false isempty(line) && continue @@ -94,21 +110,30 @@ function with_output_color(@nospecialize(f::Function), color::Union{Int, Symbol} end """ - printstyled([io], xs...; bold::Bool=false, color::Union{Symbol,Int}=:normal) + printstyled([io], xs...; bold::Bool=false, underline::Bool=false, blink::Bool=false, reverse::Bool=false, hidden::Bool=false, color::Union{Symbol,Int}=:normal) Print `xs` in a color specified as a symbol or integer, optionally in bold. -`color` may take any of the values $(Base.available_text_colors_docstring) +Keyword `color` may take any of the values $(Base.available_text_colors_docstring) or an integer between 0 and 255 inclusive. Note that not all terminals support 256 colors. -If the keyword `bold` is given as `true`, the result will be printed in bold. + +Keywords `bold=true`, `underline=true`, `blink=true` are self-explanatory. +Keyword `reverse=true` prints with foreground and background colors exchanged, +and `hidden=true` should be invisibe in the terminal but can still be copied. +These properties can be used in any combination. + +See also [`print`](@ref), [`println`](@ref), [`show`](@ref). + +!!! compat "Julia 1.7" + Keywords except `color` and `bold` were added in Julia 1.7. """ -printstyled(io::IO, msg...; bold::Bool=false, color::Union{Int,Symbol}=:normal) = - with_output_color(print, color, io, msg...; bold=bold) -printstyled(msg...; bold::Bool=false, color::Union{Int,Symbol}=:normal) = - printstyled(stdout, msg...; bold=bold, color=color) +@constprop :none printstyled(io::IO, msg...; bold::Bool=false, underline::Bool=false, blink::Bool=false, reverse::Bool=false, hidden::Bool=false, color::Union{Int,Symbol}=:normal) = + with_output_color(print, color, io, msg...; bold=bold, underline=underline, blink=blink, reverse=reverse, hidden=hidden) +@constprop :none printstyled(msg...; bold::Bool=false, underline::Bool=false, blink::Bool=false, reverse::Bool=false, hidden::Bool=false, color::Union{Int,Symbol}=:normal) = + printstyled(stdout, msg...; bold=bold, underline=underline, blink=blink, reverse=reverse, hidden=hidden, color=color) """ - Base.julia_cmd(juliapath=joinpath(Sys.BINDIR::String, julia_exename())) + Base.julia_cmd(juliapath=joinpath(Sys.BINDIR, julia_exename())) Return a julia command similar to the one of the running process. Propagates any of the `--cpu-target`, `--sysimage`, `--compile`, `--sysimage-native-code`, @@ -124,7 +149,7 @@ Among others, `--math-mode`, `--warn-overwrite`, and `--trace-compile` are notab !!! compat "Julia 1.5" The flags `--color` and `--startup-file` were added in Julia 1.5. """ -function julia_cmd(julia=joinpath(Sys.BINDIR::String, julia_exename())) +function julia_cmd(julia=joinpath(Sys.BINDIR, julia_exename())) opts = JLOptions() cpu_target = unsafe_string(opts.cpu_target) image_file = unsafe_string(opts.image_file) @@ -154,13 +179,14 @@ function julia_cmd(julia=joinpath(Sys.BINDIR::String, julia_exename())) elseif opts.check_bounds == 2 "no" # off else - "" # "default" + "" # default = "auto" end isempty(check_bounds) || push!(addflags, "--check-bounds=$check_bounds") end opts.can_inline == 0 && push!(addflags, "--inline=no") opts.use_compiled_modules == 0 && push!(addflags, "--compiled-modules=no") opts.opt_level == 2 || push!(addflags, "-O$(opts.opt_level)") + opts.opt_level_min == 0 || push!(addflags, "--min-optlevel=$(opts.opt_level_min)") push!(addflags, "-g$(opts.debug_level)") if opts.code_coverage != 0 # Forward the code-coverage flag only if applicable (if the filename is pid-dependent) @@ -170,6 +196,8 @@ function julia_cmd(julia=joinpath(Sys.BINDIR::String, julia_exename())) push!(addflags, "--code-coverage=user") elseif opts.code_coverage == 2 push!(addflags, "--code-coverage=all") + elseif opts.code_coverage == 3 + push!(addflags, "--code-coverage=@$(unsafe_string(opts.tracked_path))") end isempty(coverage_file) || push!(addflags, "--code-coverage=$coverage_file") end @@ -178,6 +206,8 @@ function julia_cmd(julia=joinpath(Sys.BINDIR::String, julia_exename())) push!(addflags, "--track-allocation=user") elseif opts.malloc_log == 2 push!(addflags, "--track-allocation=all") + elseif opts.malloc_log == 3 + push!(addflags, "--track-allocation=@$(unsafe_string(opts.tracked_path))") end if opts.color == 1 push!(addflags, "--color=yes") @@ -187,6 +217,9 @@ function julia_cmd(julia=joinpath(Sys.BINDIR::String, julia_exename())) if opts.startupfile == 2 push!(addflags, "--startup-file=no") end + if opts.use_sysimage_native_code == 0 + push!(addflags, "--sysimage-native-code=no") + end return `$julia -C$cpu_target -J$image_file $addflags` end @@ -265,6 +298,16 @@ is encountered or EOF (^D) character is entered on a blank line. If a `default` then the user can enter just a newline character to select the `default`. See also `Base.getpass` and `Base.winprompt` for secure entry of passwords. + +# Example + +```julia-repl +julia> your_name = Base.prompt("Enter your name"); +Enter your name: Logan + +julia> your_name +"Logan" +``` """ function prompt(input::IO, output::IO, message::AbstractString; default::AbstractString="") msg = !isempty(default) ? "$message [$default]: " : "$message: " @@ -306,7 +349,13 @@ if Sys.iswindows() succeeded = ccall((:CredPackAuthenticationBufferW, "credui.dll"), stdcall, Bool, (UInt32, Cwstring, Cwstring, Ptr{UInt8}, Ptr{UInt32}), CRED_PACK_GENERIC_CREDENTIALS, default_username, "", credbuf, credbufsize) - @assert succeeded + if !succeeded + credbuf = resize!(credbuf, credbufsize[]) + succeeded = ccall((:CredPackAuthenticationBufferW, "credui.dll"), stdcall, Bool, + (UInt32, Cwstring, Cwstring, Ptr{UInt8}, Ptr{UInt32}), + CRED_PACK_GENERIC_CREDENTIALS, default_username, "", credbuf, credbufsize) + @assert succeeded + end # Step 2: Create the actual dialog # 2.1: Set up the window @@ -364,6 +413,8 @@ end unsafe_crc32c(a, n, crc) = ccall(:jl_crc32c, UInt32, (UInt32, Ptr{UInt8}, Csize_t), crc, a, n) +_crc32c(a::NTuple{<:Any, UInt8}, crc::UInt32=0x00000000) = + unsafe_crc32c(Ref(a), length(a) % Csize_t, crc) _crc32c(a::Union{Array{UInt8},FastContiguousSubArray{UInt8,N,<:Array{UInt8}} where N}, crc::UInt32=0x00000000) = unsafe_crc32c(a, length(a) % Csize_t, crc) @@ -385,6 +436,8 @@ _crc32c(io::IO, crc::UInt32=0x00000000) = _crc32c(io, typemax(Int64), crc) _crc32c(io::IOStream, crc::UInt32=0x00000000) = _crc32c(io, filesize(io)-position(io), crc) _crc32c(uuid::UUID, crc::UInt32=0x00000000) = ccall(:jl_crc32c, UInt32, (UInt32, Ref{UInt128}, Csize_t), crc, uuid.value, 16) +_crc32c(x::UInt64, crc::UInt32=0x00000000) = + ccall(:jl_crc32c, UInt32, (UInt32, Ref{UInt64}, Csize_t), crc, x, 8) """ @kwdef typedef @@ -520,7 +573,7 @@ to the standard libraries before running the tests. If a seed is provided via the keyword argument, it is used to seed the global RNG in the context where the tests are run; otherwise the seed is chosen randomly. """ -function runtests(tests = ["all"]; ncores::Int = ceil(Int, Sys.CPU_THREADS::Int / 2), +function runtests(tests = ["all"]; ncores::Int = ceil(Int, Sys.CPU_THREADS / 2), exit_on_error::Bool=false, revise::Bool=false, seed::Union{BitInteger,Nothing}=nothing) @@ -532,13 +585,18 @@ function runtests(tests = ["all"]; ncores::Int = ceil(Int, Sys.CPU_THREADS::Int seed !== nothing && push!(tests, "--seed=0x$(string(seed % UInt128, base=16))") # cast to UInt128 to avoid a minus sign ENV2 = copy(ENV) ENV2["JULIA_CPU_THREADS"] = "$ncores" + ENV2["JULIA_DEPOT_PATH"] = mktempdir(; cleanup = true) + delete!(ENV2, "JULIA_LOAD_PATH") + delete!(ENV2, "JULIA_PROJECT") try - run(setenv(`$(julia_cmd()) $(joinpath(Sys.BINDIR::String, + run(setenv(`$(julia_cmd()) $(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia", "test", "runtests.jl")) $tests`, ENV2)) nothing catch buf = PipeBuffer() + original_load_path = copy(Base.LOAD_PATH); empty!(Base.LOAD_PATH); pushfirst!(Base.LOAD_PATH, "@stdlib") Base.require(Base, :InteractiveUtils).versioninfo(buf) + empty!(Base.LOAD_PATH); append!(Base.LOAD_PATH, original_load_path) error("A test has failed. Please submit a bug report (https://github.com/JuliaLang/julia/issues)\n" * "including error messages above and the output of versioninfo():\n$(read(buf, String))") end diff --git a/base/uuid.jl b/base/uuid.jl index ce46a047fe7b93..ff4df68ddb7c8c 100644 --- a/base/uuid.jl +++ b/base/uuid.jl @@ -1,9 +1,9 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license """ - Represents a Universally Unique Identifier (UUID). - Can be built from one `UInt128` (all byte values), two `UInt64`, or four `UInt32`. - Conversion from a string will check the UUID validity. +Represents a Universally Unique Identifier (UUID). +Can be built from one `UInt128` (all byte values), two `UInt64`, or four `UInt32`. +Conversion from a string will check the UUID validity. """ struct UUID value::UInt128 @@ -32,56 +32,60 @@ end UInt128(u::UUID) = u.value let -@noinline throw_malformed_uuid(s) = throw(ArgumentError("Malformed UUID string: $(repr(s))")) + uuid_hash_seed = UInt === UInt64 ? 0xd06fa04f86f11b53 : 0x96a1f36d + Base.hash(uuid::UUID, h::UInt) = hash(uuid_hash_seed, hash(convert(NTuple{2, UInt64}, uuid), h)) +end + +let @inline function uuid_kernel(s, i, u) _c = UInt32(@inbounds codeunit(s, i)) d = __convert_digit(_c, UInt32(16)) - d >= 16 && throw_malformed_uuid(s) + d >= 16 && return nothing u <<= 4 - u | d + return u | d end -global UUID -function UUID(s::AbstractString) +function Base.tryparse(::Type{UUID}, s::AbstractString) u = UInt128(0) - ncodeunits(s) != 36 && throw_malformed_uuid(s) + ncodeunits(s) != 36 && return nothing for i in 1:8 u = uuid_kernel(s, i, u) + u === nothing && return nothing end - @inbounds codeunit(s, 9) == UInt8('-') || @goto error + @inbounds codeunit(s, 9) == UInt8('-') || return nothing for i in 10:13 u = uuid_kernel(s, i, u) + u === nothing && return nothing end - @inbounds codeunit(s, 14) == UInt8('-') || @goto error + @inbounds codeunit(s, 14) == UInt8('-') || return nothing for i in 15:18 u = uuid_kernel(s, i, u) + u === nothing && return nothing end - @inbounds codeunit(s, 19) == UInt8('-') || @goto error + @inbounds codeunit(s, 19) == UInt8('-') || return nothing for i in 20:23 u = uuid_kernel(s, i, u) + u === nothing && return nothing end - @inbounds codeunit(s, 24) == UInt8('-') || @goto error + @inbounds codeunit(s, 24) == UInt8('-') || return nothing for i in 25:36 u = uuid_kernel(s, i, u) + u === nothing && return nothing end return Base.UUID(u) - @label error - throw_malformed_uuid(s) end end -parse(::Type{UUID}, s::AbstractString) = UUID(s) -function tryparse(::Type{UUID}, s::AbstractString) - try - return parse(UUID, s) - catch e - if isa(e, ArgumentError) - return nothing - end - rethrow(e) +let + @noinline throw_malformed_uuid(s) = throw(ArgumentError("Malformed UUID string: $(repr(s))")) + function Base.parse(::Type{UUID}, s::AbstractString) + uuid = tryparse(UUID, s) + return uuid === nothing ? throw_malformed_uuid(s) : uuid end end +UUID(s::AbstractString) = parse(UUID, s) + let groupings = [36:-1:25; 23:-1:20; 18:-1:15; 13:-1:10; 8:-1:1] global string function string(u::UUID) diff --git a/base/version.jl b/base/version.jl index 3a57d40fda752f..978abbba1a8aab 100644 --- a/base/version.jl +++ b/base/version.jl @@ -8,18 +8,32 @@ const VInt = UInt32 """ VersionNumber -Version number type which follow the specifications of -[semantic versioning](https://semver.org/), composed of major, minor +Version number type which follows the specifications of +[semantic versioning (semver)](https://semver.org/), composed of major, minor and patch numeric values, followed by pre-release and build -alpha-numeric annotations. See also [`@v_str`](@ref). +alpha-numeric annotations. + +`VersionNumber` objects can be compared with all of the standard comparison +operators (`==`, `<`, `<=`, etc.), with the result following semver rules. + +See also [`@v_str`](@ref) to efficiently construct `VersionNumber` objects +from semver-format literal strings, [`VERSION`](@ref) for the `VersionNumber` +of Julia itself, and [Version Number Literals](@ref man-version-number-literals) +in the manual. # Examples ```jldoctest -julia> VersionNumber("1.2.3") +julia> a = VersionNumber(1, 2, 3) v"1.2.3" -julia> VersionNumber("2.0.1-rc1") +julia> a >= v"1.2" +true + +julia> b = VersionNumber("2.0.1-rc1") v"2.0.1-rc1" + +julia> b >= v"2.0.1" +false ``` """ struct VersionNumber @@ -66,6 +80,7 @@ VersionNumber(major::Integer, minor::Integer = 0, patch::Integer = 0, map(x->x isa Integer ? UInt64(x) : String(x), bld)) VersionNumber(v::Tuple) = VersionNumber(v...) +VersionNumber(v::VersionNumber) = v function print(io::IO, v::VersionNumber) v == typemax(VersionNumber) && return print(io, "∞") @@ -100,17 +115,17 @@ const VERSION_REGEX = r"^ $"ix function split_idents(s::AbstractString) - idents = split(s, '.') + idents = eachsplit(s, '.') pidents = Union{UInt64,String}[occursin(r"^\d+$", ident) ? parse(UInt64, ident) : String(ident) for ident in idents] return tuple(pidents...)::VerTuple end -function VersionNumber(v::AbstractString) +function tryparse(::Type{VersionNumber}, v::AbstractString) v == "∞" && return typemax(VersionNumber) - m = match(VERSION_REGEX, v) - m === nothing && throw(ArgumentError("invalid version string: $v")) + m = match(VERSION_REGEX, String(v)::String) + m === nothing && return nothing major, minor, patch, minus, prerl, plus, build = m.captures - major = parse(VInt, major) + major = parse(VInt, major::AbstractString) minor = minor !== nothing ? parse(VInt, minor) : VInt(0) patch = patch !== nothing ? parse(VInt, patch) : VInt(0) if prerl !== nothing && !isempty(prerl) && prerl[1] == '-' @@ -121,18 +136,14 @@ function VersionNumber(v::AbstractString) return VersionNumber(major, minor, patch, prerl::VerTuple, build::VerTuple) end -parse(::Type{VersionNumber}, v::AbstractString) = VersionNumber(v) -function tryparse(::Type{VersionNumber}, v::AbstractString) - try - return VersionNumber(v) - catch e - if isa(e, InterruptException) - rethrow(e) - end - return nothing - end +function parse(::Type{VersionNumber}, v::AbstractString) + ver = tryparse(VersionNumber, v) + ver === nothing && throw(ArgumentError("invalid version string: $v")) + return ver end +VersionNumber(v::AbstractString) = parse(VersionNumber, v) + """ @v_str @@ -225,7 +236,7 @@ nextmajor(v::VersionNumber) = v < thismajor(v) ? thismajor(v) : VersionNumber(v. """ VERSION -A `VersionNumber` object describing which version of Julia is in use. For details see +A [`VersionNumber`](@ref) object describing which version of Julia is in use. See also [Version Number Literals](@ref man-version-number-literals). """ const VERSION = try @@ -254,6 +265,8 @@ else VersionNumber(libllvm_version_string) end +libllvm_path() = ccall(:jl_get_libllvm, Any, ()) + function banner(io::IO = stdout) if GIT_VERSION_INFO.tagged_commit commit_string = TAGGED_RELEASE_BANNER diff --git a/base/version_git.sh b/base/version_git.sh index d2ac9cb6058a70..2a3352d1066efd 100644 --- a/base/version_git.sh +++ b/base/version_git.sh @@ -5,14 +5,16 @@ echo "# This file was autogenerated in base/version_git.sh" echo "struct GitVersionInfo" -echo " commit::AbstractString" -echo " commit_short::AbstractString" -echo " branch::AbstractString" +echo " commit::String" +echo " commit_short::String" +echo " branch::String" echo " build_number::Int" -echo " date_string::AbstractString" +echo " date_string::String" echo " tagged_commit::Bool" echo " fork_master_distance::Int" echo " fork_master_timestamp::Float64" +echo " build_system_commit::String" +echo " build_system_commit_short::String" echo "end" echo "" @@ -22,7 +24,7 @@ cd $1 if [ "$#" = "2" -a "$2" = "NO_GIT" ]; then # this comment is used in base/Makefile to distinguish boilerplate echo "# Default output if git is not available." - echo "const GIT_VERSION_INFO = GitVersionInfo(\"\" ,\"\" ,\"\" ,0 ,\"\" ,true ,0 ,0.)" + echo 'const GIT_VERSION_INFO = GitVersionInfo("", "", "", 0, "", true, 0, 0.0, "", "")' exit 0 fi # Collect temporary variables @@ -82,6 +84,15 @@ if [ -z "$fork_master_timestamp" ]; then fork_master_timestamp="0" fi +build_system_directory="../.buildkite" +if [ -d "${build_system_directory}/.git" ]; then + build_system_commit=$(git -C "${build_system_directory}" rev-parse HEAD) + build_system_commit_short=$(git -C "${build_system_directory}" rev-parse --short HEAD) +else + build_system_commit="" + build_system_commit_short="" +fi + echo "const GIT_VERSION_INFO = GitVersionInfo(" echo " \"$commit\"," echo " \"$commit_short\"," @@ -90,5 +101,7 @@ echo " $build_number," echo " \"$date_string\"," echo " $tagged_commit," echo " $fork_master_distance," -echo " $fork_master_timestamp." +echo " $fork_master_timestamp.0," +echo " \"$build_system_commit\"," +echo " \"$build_system_commit_short\"," echo ")" diff --git a/base/views.jl b/base/views.jl index ccf24d4cdea3a7..8553695868d6c5 100644 --- a/base/views.jl +++ b/base/views.jl @@ -42,7 +42,7 @@ function replace_ref_begin_end_!(ex, withex) n = 1 J = lastindex(ex.args) for j = 2:J - exj, used = replace_ref_begin_end_!(ex.args[j], (:($firstindex($S)),:($lastindex($S,$n)))) + exj, used = replace_ref_begin_end_!(ex.args[j], (:($firstindex($S,$n)),:($lastindex($S,$n)))) used_S |= used ex.args[j] = exj if isa(exj,Expr) && exj.head === :... @@ -77,10 +77,23 @@ end """ @view A[inds...] -Creates a `SubArray` from an indexing expression. This can only be applied directly to a -reference expression (e.g. `@view A[1,2:end]`), and should *not* be used as the target of -an assignment (e.g. `@view(A[1,2:end]) = ...`). See also [`@views`](@ref) -to switch an entire block of code to use views for slicing. +Transform the indexing expression `A[inds...]` into the equivalent [`view`](@ref) call. + +This can only be applied directly to a single indexing expression and is particularly +helpful for expressions that include the special `begin` or `end` indexing syntaxes +like `A[begin, 2:end-1]` (as those are not supported by the normal [`view`](@ref) +function). + +Note that `@view` cannot be used as the target of a regular assignment (e.g., +`@view(A[1, 2:end]) = ...`), nor would the un-decorated +[indexed assignment](@ref man-indexed-assignment) (`A[1, 2:end] = ...`) +or broadcasted indexed assignment (`A[1, 2:end] .= ...`) make a copy. It can be useful, +however, for _updating_ broadcasted assignments like `@view(A[1, 2:end]) .+= 1` +because this is a simple syntax for `@view(A[1, 2:end]) .= @view(A[1, 2:end]) + 1`, +and the indexing expression on the right-hand side would otherwise make a +copy without the `@view`. + +See also [`@views`](@ref) to switch an entire block of code to use views for non-scalar indexing. !!! compat "Julia 1.5" Using `begin` in an indexing expression to refer to the first index requires at least @@ -201,6 +214,8 @@ to return a view. Scalar indices, non-array types, and explicit [`getindex`](@ref) calls (as opposed to `array[...]`) are unaffected. +Similarly, `@views` converts string slices into [`SubString`](@ref) views. + !!! note The `@views` macro only affects `array[...]` expressions that appear explicitly in the given `expression`, not array slicing that diff --git a/base/weakkeydict.jl b/base/weakkeydict.jl index f21097ddfb3ef0..0a9987671ea9b0 100644 --- a/base/weakkeydict.jl +++ b/base/weakkeydict.jl @@ -10,24 +10,19 @@ references to objects which may be garbage collected even when referenced in a hash table. See [`Dict`](@ref) for further help. Note, unlike [`Dict`](@ref), -`WeakKeyDict` does not convert keys on insertion. +`WeakKeyDict` does not convert keys on insertion, as this would imply the key +object was unreferenced anywhere before insertion. """ mutable struct WeakKeyDict{K,V} <: AbstractDict{K,V} ht::Dict{WeakRef,V} lock::ReentrantLock finalizer::Function + dirty::Bool # Constructors mirror Dict's function WeakKeyDict{K,V}() where V where K - t = new(Dict{Any,V}(), ReentrantLock(), identity) - t.finalizer = function (k) - # when a weak key is finalized, remove from dictionary if it is still there - if islocked(t) - finalizer(t.finalizer, k) - return nothing - end - delete!(t, k) - end + t = new(Dict{Any,V}(), ReentrantLock(), identity, 0) + t.finalizer = k -> t.dirty = true return t end end @@ -69,56 +64,151 @@ function WeakKeyDict(kv) end end +function _cleanup_locked(h::WeakKeyDict) + if h.dirty + h.dirty = false + idx = skip_deleted_floor!(h.ht) + while idx != 0 + if h.ht.keys[idx].value === nothing + _delete!(h.ht, idx) + end + idx = skip_deleted(h.ht, idx + 1) + end + end + return h +end + sizehint!(d::WeakKeyDict, newsz) = sizehint!(d.ht, newsz) empty(d::WeakKeyDict, ::Type{K}, ::Type{V}) where {K, V} = WeakKeyDict{K, V}() +IteratorSize(::Type{<:WeakKeyDict}) = SizeUnknown() + islocked(wkh::WeakKeyDict) = islocked(wkh.lock) +lock(wkh::WeakKeyDict) = lock(wkh.lock) +unlock(wkh::WeakKeyDict) = unlock(wkh.lock) lock(f, wkh::WeakKeyDict) = lock(f, wkh.lock) trylock(f, wkh::WeakKeyDict) = trylock(f, wkh.lock) function setindex!(wkh::WeakKeyDict{K}, v, key) where K !isa(key, K) && throw(ArgumentError("$(limitrepr(key)) is not a valid key for type $K")) - finalizer(wkh.finalizer, key) + # 'nothing' is not valid both because 'finalizer' will reject it, + # and because we therefore use it as a sentinel value + key === nothing && throw(ArgumentError("`nothing` is not a valid WeakKeyDict key")) lock(wkh) do - wkh.ht[WeakRef(key)] = v + _cleanup_locked(wkh) + k = getkey(wkh.ht, key, nothing) + if k === nothing + finalizer(wkh.finalizer, key) + k = WeakRef(key) + else + k.value = key + end + wkh.ht[k] = v end return wkh end +function get!(wkh::WeakKeyDict{K}, key, default) where {K} + v = lock(wkh) do + if key !== nothing && haskey(wkh.ht, key) + wkh.ht[key] + else + wkh[key] = default + end + end + return v +end +function get!(default::Callable, wkh::WeakKeyDict{K}, key) where {K} + v = lock(wkh) do + if key !== nothing && haskey(wkh.ht, key) + wkh.ht[key] + else + wkh[key] = default() + end + end + return v +end function getkey(wkh::WeakKeyDict{K}, kk, default) where K - return lock(wkh) do - k = getkey(wkh.ht, kk, secret_table_token) - k === secret_table_token && return default - return k.value::K + k = lock(wkh) do + local k = getkey(wkh.ht, kk, nothing) + k === nothing && return nothing + return k.value end + return k === nothing ? default : k::K end -map!(f,iter::ValueIterator{<:WeakKeyDict})= map!(f, values(iter.dict.ht)) -get(wkh::WeakKeyDict{K}, key, default) where {K} = lock(() -> get(wkh.ht, key, default), wkh) -get(default::Callable, wkh::WeakKeyDict{K}, key) where {K} = lock(() -> get(default, wkh.ht, key), wkh) -function get!(wkh::WeakKeyDict{K}, key, default) where {K} - !isa(key, K) && throw(ArgumentError("$(limitrepr(key)) is not a valid key for type $K")) - lock(() -> get!(wkh.ht, WeakRef(key), default), wkh) +map!(f, iter::ValueIterator{<:WeakKeyDict})= map!(f, values(iter.dict.ht)) + +function get(wkh::WeakKeyDict{K}, key, default) where {K} + key === nothing && throw(KeyError(nothing)) + lock(wkh) do + return get(wkh.ht, key, default) + end end -function get!(default::Callable, wkh::WeakKeyDict{K}, key) where {K} - !isa(key, K) && throw(ArgumentError("$(limitrepr(key)) is not a valid key for type $K")) - lock(() -> get!(default, wkh.ht, WeakRef(key)), wkh) +function get(default::Callable, wkh::WeakKeyDict{K}, key) where {K} + key === nothing && throw(KeyError(nothing)) + lock(wkh) do + return get(default, wkh.ht, key) + end +end +function pop!(wkh::WeakKeyDict{K}, key) where {K} + key === nothing && throw(KeyError(nothing)) + lock(wkh) do + return pop!(wkh.ht, key) + end +end +function pop!(wkh::WeakKeyDict{K}, key, default) where {K} + key === nothing && return default + lock(wkh) do + return pop!(wkh.ht, key, default) + end +end +function delete!(wkh::WeakKeyDict, key) + key === nothing && return wkh + lock(wkh) do + delete!(wkh.ht, key) + end + return wkh +end +function empty!(wkh::WeakKeyDict) + lock(wkh) do + empty!(wkh.ht) + end + return wkh +end +function haskey(wkh::WeakKeyDict{K}, key) where {K} + key === nothing && return false + lock(wkh) do + return haskey(wkh.ht, key) + end +end +function getindex(wkh::WeakKeyDict{K}, key) where {K} + key === nothing && throw(KeyError(nothing)) + lock(wkh) do + return getindex(wkh.ht, key) + end +end +isempty(wkh::WeakKeyDict) = length(wkh) == 0 +function length(t::WeakKeyDict) + lock(t) do + _cleanup_locked(t) + return length(t.ht) + end end -pop!(wkh::WeakKeyDict{K}, key) where {K} = lock(() -> pop!(wkh.ht, key), wkh) -pop!(wkh::WeakKeyDict{K}, key, default) where {K} = lock(() -> pop!(wkh.ht, key, default), wkh) -delete!(wkh::WeakKeyDict, key) = (lock(() -> delete!(wkh.ht, key), wkh); wkh) -empty!(wkh::WeakKeyDict) = (lock(() -> empty!(wkh.ht), wkh); wkh) -haskey(wkh::WeakKeyDict{K}, key) where {K} = lock(() -> haskey(wkh.ht, key), wkh) -getindex(wkh::WeakKeyDict{K}, key) where {K} = lock(() -> getindex(wkh.ht, key), wkh) -isempty(wkh::WeakKeyDict) = isempty(wkh.ht) -length(t::WeakKeyDict) = length(t.ht) function iterate(t::WeakKeyDict{K,V}, state...) where {K, V} - y = lock(() -> iterate(t.ht, state...), t) - y === nothing && return nothing - wkv, newstate = y - kv = Pair{K,V}(wkv[1].value::K, wkv[2]) - return (kv, newstate) + return lock(t) do + while true + y = iterate(t.ht, state...) + y === nothing && return nothing + wkv, state = y + k = wkv[1].value + GC.safepoint() # ensure `k` is now gc-rooted + k === nothing && continue # indicates `k` is scheduled for deletion + kv = Pair{K,V}(k::K, wkv[2]) + return (kv, state) + end + end end filter!(f, d::WeakKeyDict) = filter_in_one_pass!(f, d) diff --git a/ui/.gitignore b/cli/.gitignore similarity index 100% rename from ui/.gitignore rename to cli/.gitignore diff --git a/cli/Makefile b/cli/Makefile new file mode 100644 index 00000000000000..11855ee6244dc0 --- /dev/null +++ b/cli/Makefile @@ -0,0 +1,141 @@ +SRCDIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) +JULIAHOME := $(abspath $(SRCDIR)/..) +BUILDDIR ?= . +include $(JULIAHOME)/deps/Versions.make +include $(JULIAHOME)/Make.inc +include $(JULIAHOME)/deps/llvm-ver.make + + +HEADERS := $(addprefix $(SRCDIR)/,jl_exports.h loader.h) $(addprefix $(JULIAHOME)/src/,julia_fasttls.h support/platform.h support/dirpath.h jl_exported_data.inc jl_exported_funcs.inc) + +LOADER_CFLAGS = $(JCFLAGS) -I$(BUILDROOT)/src -I$(JULIAHOME)/src -I$(JULIAHOME)/src/support -I$(build_includedir) -ffreestanding +LOADER_LDFLAGS = $(JLDFLAGS) -ffreestanding -L$(build_shlibdir) -L$(build_libdir) + +ifeq ($(OS),WINNT) +LOADER_CFLAGS += -municode -mconsole -nostdlib -fno-stack-check -fno-stack-protector -mno-stack-arg-probe +endif + +ifeq ($(OS),WINNT) +LOADER_LDFLAGS += -municode -mconsole -nostdlib --disable-auto-import \ + --disable-runtime-pseudo-reloc -lntdll -lkernel32 -lpsapi +else ifeq ($(OS),Linux) +LOADER_LDFLAGS += -Wl,--no-as-needed -ldl -lpthread -rdynamic -lc -Wl,--as-needed +else ifeq ($(OS),FreeBSD) +LOADER_LDFLAGS += -Wl,--no-as-needed -ldl -lpthread -rdynamic -lc -Wl,--as-needed +else ifeq ($(OS),Darwin) +LOADER_LDFLAGS += -lSystem +endif + +# Build list of dependent libraries that must be opened +SHIPFLAGS += -DDEP_LIBS="\"$(LOADER_BUILD_DEP_LIBS)\"" +DEBUGFLAGS += -DDEP_LIBS="\"$(LOADER_DEBUG_BUILD_DEP_LIBS)\"" + +EXE_OBJS := $(BUILDDIR)/loader_exe.o +EXE_DOBJS := $(BUILDDIR)/loader_exe.dbg.obj +LIB_OBJS := $(BUILDDIR)/loader_lib.o +LIB_DOBJS := $(BUILDDIR)/loader_lib.dbg.obj + +# If this is an architecture that supports dynamic linking, link in a trampoline definition +ifneq (,$(wildcard $(SRCDIR)/trampolines/trampolines_$(ARCH).S)) +LIB_OBJS += $(BUILDDIR)/loader_trampolines.o +LIB_DOBJS += $(BUILDDIR)/loader_trampolines.o +endif + +default: release +all: release debug +release debug : % : julia-% libjulia-% + +$(BUILDDIR)/loader_lib.o : $(SRCDIR)/loader_lib.c $(HEADERS) $(JULIAHOME)/VERSION + @$(call PRINT_CC, $(CC) -DLIBRARY_EXPORTS $(SHIPFLAGS) $(LOADER_CFLAGS) -c $< -o $@) +$(BUILDDIR)/loader_lib.dbg.obj : $(SRCDIR)/loader_lib.c $(HEADERS) $(JULIAHOME)/VERSION + @$(call PRINT_CC, $(CC) -DLIBRARY_EXPORTS $(DEBUGFLAGS) $(LOADER_CFLAGS) -c $< -o $@) +$(BUILDDIR)/loader_exe.o : $(SRCDIR)/loader_exe.c $(HEADERS) $(JULIAHOME)/VERSION + @$(call PRINT_CC, $(CC) $(SHIPFLAGS) $(LOADER_CFLAGS) -c $< -o $@) +$(BUILDDIR)/loader_exe.dbg.obj : $(SRCDIR)/loader_exe.c $(HEADERS) $(JULIAHOME)/VERSION + @$(call PRINT_CC, $(CC) $(DEBUGFLAGS) $(LOADER_CFLAGS) -c $< -o $@) +$(BUILDDIR)/loader_trampolines.o : $(SRCDIR)/trampolines/trampolines_$(ARCH).S $(HEADERS) $(SRCDIR)/trampolines/common.h + @$(call PRINT_CC, $(CC) $(SHIPFLAGS) $(LOADER_CFLAGS) $< -c -o $@) + +# Debugging target to help us see what kind of code is being generated for our trampolines +dump-trampolines: $(SRCDIR)/trampolines/trampolines_$(ARCH).S + $(CC) $(SHIPFLAGS) $(LOADER_CFLAGS) $< -S | sed -E 's/ ((%%)|;) /\n/g' | sed -E 's/.global/\n.global/g' + +DIRS = $(build_bindir) $(build_libdir) +$(DIRS): + @mkdir -p $@ + +ifeq ($(OS),WINNT) +$(BUILDDIR)/julia_res.o: $(JULIAHOME)/contrib/windows/julia.rc $(JULIAHOME)/VERSION + JLVER=`cat $(JULIAHOME)/VERSION` && \ + JLVERi=`echo $$JLVER | perl -nle \ + '/^(\d+)\.?(\d*)\.?(\d*)/ && \ + print int $$1,",",int $$2,",",int $$3,",0"'` && \ + $(CROSS_COMPILE)windres $< -O coff -o $@ -DJLVER=$$JLVERi -DJLVER_STR=\\\"$$JLVER\\\" +EXE_OBJS += $(BUILDDIR)/julia_res.o +EXE_DOBJS += $(BUILDDIR)/julia_res.o +endif + +# Embed an Info.plist in the julia executable +# Create an intermediate target Info.plist for Darwin code signing. +ifeq ($(DARWIN_FRAMEWORK),1) +$(BUILDDIR)/Info.plist: $(JULIAHOME)/VERSION + /usr/libexec/PlistBuddy -x -c "Clear dict" $@ + /usr/libexec/PlistBuddy -x -c "Add :CFBundleName string julia" $@ + /usr/libexec/PlistBuddy -x -c "Add :CFBundleIdentifier string $(darwin_codesign_id_julia_ui)" $@ + /usr/libexec/PlistBuddy -x -c "Add :CFBundleInfoDictionaryVersion string 6.0" $@ + /usr/libexec/PlistBuddy -x -c "Add :CFBundleVersion string $(JULIA_COMMIT)" $@ + /usr/libexec/PlistBuddy -x -c "Add :CFBundleShortVersionString string $(JULIA_MAJOR_VERSION).$(JULIA_MINOR_VERSION).$(JULIA_PATCH_VERSION)" $@ +.INTERMEDIATE: $(BUILDDIR)/Info.plist # cleanup this file after we are done using it +JLDFLAGS += -Wl,-sectcreate,__TEXT,__info_plist,Info.plist +$(build_bindir)/julia$(EXE): $(BUILDDIR)/Info.plist +$(build_bindir)/julia-debug$(EXE): $(BUILDDIR)/Info.plist +endif + +julia-release: $(build_bindir)/julia$(EXE) +julia-debug: $(build_bindir)/julia-debug$(EXE) +libjulia-release: $(build_shlibdir)/libjulia.$(SHLIB_EXT) +libjulia-debug: $(build_shlibdir)/libjulia-debug.$(SHLIB_EXT) + +ifeq ($(OS),WINNT) +# On Windows we need to strip out exported functions from the generated import library. +STRIP_EXPORTED_FUNCS := $(shell $(CPP_STDOUT) -I$(JULIAHOME)/src $(SRCDIR)/list_strip_symbols.h) +endif + +$(build_shlibdir)/libjulia.$(JL_MAJOR_MINOR_SHLIB_EXT): $(LIB_OBJS) $(SRCDIR)/list_strip_symbols.h | $(build_shlibdir) $(build_libdir) + @$(call PRINT_LINK, $(CC) $(call IMPLIB_FLAGS,$@.tmp) $(LOADER_CFLAGS) -DLIBRARY_EXPORTS -shared $(SHIPFLAGS) $(LIB_OBJS) -o $@ \ + $(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(RPATH_LIB) $(call SONAME_FLAGS,libjulia.$(JL_MAJOR_SHLIB_EXT))) + @$(INSTALL_NAME_CMD)libjulia.$(SHLIB_EXT) $@ +ifeq ($(OS), WINNT) + @# Note that if the objcopy command starts getting too long, we can use `@file` to read + @# command-line options from `file` instead. + @$(call PRINT_ANALYZE, $(OBJCOPY) $(build_libdir)/$(notdir $@).tmp.a $(STRIP_EXPORTED_FUNCS) $(build_libdir)/$(notdir $@).a && rm $(build_libdir)/$(notdir $@).tmp.a) +endif + +$(build_shlibdir)/libjulia-debug.$(JL_MAJOR_MINOR_SHLIB_EXT): $(LIB_DOBJS) $(SRCDIR)/list_strip_symbols.h | $(build_shlibdir) $(build_libdir) + @$(call PRINT_LINK, $(CC) $(call IMPLIB_FLAGS,$@.tmp) $(LOADER_CFLAGS) -DLIBRARY_EXPORTS -shared $(DEBUGFLAGS) $(LIB_DOBJS) -o $@ \ + $(JLIBLDFLAGS) $(LOADER_LDFLAGS) $(RPATH_LIB) $(call SONAME_FLAGS,libjulia-debug.$(JL_MAJOR_SHLIB_EXT))) + @$(INSTALL_NAME_CMD)libjulia-debug.$(SHLIB_EXT) $@ +ifeq ($(OS), WINNT) + @$(call PRINT_ANALYZE, $(OBJCOPY) $(build_libdir)/$(notdir $@).tmp.a $(STRIP_EXPORTED_FUNCS) $(build_libdir)/$(notdir $@).a && rm $(build_libdir)/$(notdir $@).tmp.a) +endif + +ifneq ($(OS), WINNT) +$(build_shlibdir)/libjulia.$(JL_MAJOR_SHLIB_EXT) $(build_shlibdir)/libjulia-debug.$(JL_MAJOR_SHLIB_EXT): $(build_shlibdir)/libjulia%.$(JL_MAJOR_SHLIB_EXT): \ + $(build_shlibdir)/libjulia%.$(JL_MAJOR_MINOR_SHLIB_EXT) + @$(call PRINT_LINK, ln -sf $(notdir $<) $@) +$(build_shlibdir)/libjulia.$(SHLIB_EXT) $(build_shlibdir)/libjulia-debug.$(SHLIB_EXT): $(build_shlibdir)/libjulia%.$(SHLIB_EXT): \ + $(build_shlibdir)/libjulia%.$(JL_MAJOR_MINOR_SHLIB_EXT) $(build_shlibdir)/libjulia%.$(JL_MAJOR_SHLIB_EXT) + @$(call PRINT_LINK, ln -sf $(notdir $<) $@) +endif + +$(build_bindir)/julia$(EXE): $(EXE_OBJS) $(build_shlibdir)/libjulia.$(SHLIB_EXT) | $(build_bindir) + @$(call PRINT_LINK, $(CC) $(LOADER_CFLAGS) $(SHIPFLAGS) $(EXE_OBJS) -o $@ $(LOADER_LDFLAGS) $(RPATH) -ljulia) + +$(build_bindir)/julia-debug$(EXE): $(EXE_DOBJS) $(build_shlibdir)/libjulia-debug.$(SHLIB_EXT) | $(build_bindir) + @$(call PRINT_LINK, $(CC) $(LOADER_CFLAGS) $(DEBUGFLAGS) $(EXE_DOBJS) -o $@ $(LOADER_LDFLAGS) $(RPATH) -ljulia-debug) + +clean: | $(CLEAN_TARGETS) + rm -f $(BUILDDIR)/*.o $(BUILDDIR)/*.dbg.obj + rm -f $(build_bindir)/julia* + +.PHONY: clean release debug julia-release julia-debug diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000000000..4021aceb7d8398 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,31 @@ +# cli and loader + +This directory contains the code used by the Julia loader, implementing the pieces necessary to isolate ourselves from the native dynamic loader enough to reimplement useful features such as RPATH across all platforms. +This loader comprises the `julia` executable and the `libjulia` library, which are responsible for setting things up such that `libjulia-internal` and any other internal dependencies can be reliably loaded. +The code is organized in three pieces: + +* `loader_exe.c` gets built into the main `julia` executable. It immediately loads `libjulia`. +* `loader_lib.c` gets built into the main `libjulia` shared library. This is the main entrypoint for the Julia runtime loading process, which occurs within `jl_load_repl()`. +* `trampolines/*.S`, which contains assembly definitions for symbol forwarding trampolines. These are used to allow `libjulia` to re-export symbols such that a C linker can use `libjulia` directly for embedding usecases. + +The main requirements of the loader are as follows: + +- **Isolation**: We need to be able to load our own copy of `libgcc_s.so`, etc... + On Linux/macOS, proper application of `RPATH` can influence the linker's decisions, however errant `LD_LIBRARY_PATH` entries or system libraries inserted into the build process can still interfere, not to mention Windows' lack of `RPATH`-like capabilities. + To address this, the loader is built as a stand-alone binary that does not depend on the large set of dependencies that `libjulia-internal` itself does, and manually `dlopen()`'s a list of dependencies using logic similar to that of an `RPATH`. +- **Compatibility**: We need to support embedding usecases without forcing embedders to care about all of these things. + For linking against the Julia runtime by simply providing `-ljulia` on the link line, we must ensure that all public interfaces, whether function symbols or data symbols, must be exported from `libjulia`. + This motivates our usage of function trampolines to re-export functions from `libjulia-internal`, and the reason why all public data symbols are defined within `libjulia`, then imported into `libjulia-internal` for initialization. +- **Flexibility**: We need to be able to make use of system libraries when requested to do so by the user at build time. + Currently, we embed the list of libraries to be `dlopen()`'ed within `libjulia` as a string (See the definition of `DEP_LIBS` in `Make.inc` and its usage in `loader_lib.c`). + This is flexible enough as we do not support changing this configuration at runtime, however in the future, we may need to add some simple parsing logic in `loader_lib.c` to inspect a `LocalPreferences.toml` and construct the list of libraries to load from that. +- **Speed**: This whole process should be fast, especially function trampolines. + To this end, we write everything in low-overhead assembly, borrowing inspiration from the PLT trampolines that the linker already generates when using dynamic libraries. + +## Public interface definition + +The public interface exported by `libjulia` is contained within `.inc` files stored in `src`; one for exported data symbols, [`src/jl_exported_data.inc`](../src/jl_exported_data.inc) and one for exported functions, [`src/jl_exported_funcs.inc`](../src/jl_exported_funcs.inc). +Adding entries to the data list will cause `libjulia` to generate a placeholder variable declaration. +Most symbols are declared to be of type `void *`, however for symbols that are of a different size, they are declared along with their type. +Adding entries to the function list will cause `libjulia` to generate a trampoline definition (using a trampoline according to the architecture of the target processor) and then at runtime, when `libjulia` has successfully loaded `libjulia-internal`, it will `dlsym()` that symbol from within `libjulia-internal` and set it as the target of the trampoline. +All initialization will occur automatically upon successful load of `libjulia`, so there is no need for user code to call an initialization before invoking typical `libjulia-internal` functions (although initialization of the runtime itself is still necessary, e.g. calling `jl_init()`). diff --git a/cli/jl_exports.h b/cli/jl_exports.h new file mode 100644 index 00000000000000..e9be7c6f2f819b --- /dev/null +++ b/cli/jl_exports.h @@ -0,0 +1,75 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +// Bring in the curated lists of exported data and function symbols, then +// perform C preprocessor magic upon them to generate lists of declarations and +// functions to re-export our function symbols from libjulia-internal to libjulia. +#include "../src/jl_exported_data.inc" +#include "../src/jl_exported_funcs.inc" + +// Define pointer data as `const void * $(name);` +#define XX(name) JL_DLLEXPORT const void * name; +JL_EXPORTED_DATA_POINTERS(XX) +#undef XX + +// Define symbol data as `$(type) $(name);` +#define XX(name, type) JL_DLLEXPORT type name; +JL_EXPORTED_DATA_SYMBOLS(XX) +#undef XX + +// Declare list of exported functions (sans type) +#define XX(name) JL_DLLEXPORT void name(void); +typedef void (anonfunc)(void); +JL_RUNTIME_EXPORTED_FUNCS(XX) +#ifdef _OS_WINDOWS_ +JL_RUNTIME_EXPORTED_FUNCS_WIN(XX) +#endif +JL_CODEGEN_EXPORTED_FUNCS(XX) +#undef XX + +// Define holder locations for function addresses as `const void * $(name)_addr = NULL; +#define XX(name) JL_HIDDEN anonfunc * name##_addr = NULL; +JL_RUNTIME_EXPORTED_FUNCS(XX) +#ifdef _OS_WINDOWS_ +JL_RUNTIME_EXPORTED_FUNCS_WIN(XX) +#endif +JL_CODEGEN_EXPORTED_FUNCS(XX) +#undef XX + +// Generate lists of function names and addresses +#define XX(name) "i" #name, +static const char *const jl_runtime_exported_func_names[] = { + JL_RUNTIME_EXPORTED_FUNCS(XX) +#ifdef _OS_WINDOWS_ + JL_RUNTIME_EXPORTED_FUNCS_WIN(XX) +#endif + NULL +}; +#undef XX + +#define XX(name) #name"_impl", +static const char *const jl_codegen_exported_func_names[] = { + JL_CODEGEN_EXPORTED_FUNCS(XX) + NULL +}; +#undef XX + +#define XX(name) #name"_fallback", +static const char *const jl_codegen_fallback_func_names[] = { + JL_CODEGEN_EXPORTED_FUNCS(XX) + NULL +}; +#undef XX + +#define XX(name) &name##_addr, +static anonfunc **const jl_runtime_exported_func_addrs[] = { + JL_RUNTIME_EXPORTED_FUNCS(XX) +#ifdef _OS_WINDOWS_ + JL_RUNTIME_EXPORTED_FUNCS_WIN(XX) +#endif + NULL +}; +static anonfunc **const jl_codegen_exported_func_addrs[] = { + JL_CODEGEN_EXPORTED_FUNCS(XX) + NULL +}; +#undef XX diff --git a/cli/list_strip_symbols.h b/cli/list_strip_symbols.h new file mode 100644 index 00000000000000..5d534616e132be --- /dev/null +++ b/cli/list_strip_symbols.h @@ -0,0 +1,10 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +#include "jl_exported_funcs.inc" +#include "trampolines/common.h" +#define XX(x) --strip-symbol=CNAME(x) +JL_RUNTIME_EXPORTED_FUNCS(XX) +#ifdef _OS_WINDOWS_ +JL_RUNTIME_EXPORTED_FUNCS_WIN(XX) +#endif +#undef XX diff --git a/cli/loader.h b/cli/loader.h new file mode 100644 index 00000000000000..2d0b977f7142f8 --- /dev/null +++ b/cli/loader.h @@ -0,0 +1,100 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +/* Bring in definitions for `_OS_X_`, `JL_PATH_MAX` and `PATHSEPSTRING`, `jl_ptls_t`, etc... */ +#include "../src/support/platform.h" +#include "../src/support/dirpath.h" +#include "../src/julia_fasttls.h" + +#ifdef _OS_WINDOWS_ +/* We need to reimplement a bunch of standard library stuff on windows, + * but we want to make sure that it doesn't conflict with the actual implementations + * once those get linked into this process. */ +#define fwrite loader_fwrite +#define fputs loader_fputs +#define exit loader_exit +#define strlen loader_strlen +#define wcslen loader_wcslen +#define strncat loader_strncat +#define memcpy loader_memcpy +#define dirname loader_dirname +#define strchr loader_strchr +#define malloc loader_malloc +#define realloc loader_realloc +#endif + +#include + +#ifdef _OS_WINDOWS_ + +#define WIN32_LEAN_AND_MEAN +#include + +#else + +#ifdef _OS_DARWIN_ +#include +#endif +#ifdef _OS_FREEBSD_ +#include +#include +#endif +#define _GNU_SOURCE // Need this for `dladdr()` +#include +#include +#include +#include +#include +#include +#include + +#endif + +// Borrow definition from `support/dtypes.h` +#ifdef _OS_WINDOWS_ +# ifdef LIBRARY_EXPORTS +# define JL_DLLEXPORT __declspec(dllexport) +# else +# define JL_DLLEXPORT __declspec(dllimport) +# endif +#define JL_HIDDEN +#else +# if defined(LIBRARY_EXPORTS) && defined(_OS_LINUX_) +# define JL_DLLEXPORT __attribute__ ((visibility("protected"))) +# else +# define JL_DLLEXPORT __attribute__ ((visibility("default"))) +# endif +#define JL_HIDDEN __attribute__ ((visibility("hidden"))) +#endif +/* + * DEP_LIBS is our list of dependent libraries that must be loaded before `libjulia`. + * Note that order matters, as each entry will be opened in-order. We define here a + * dummy value just so this file compiles on its own, and also so that developers can + * see what this value should look like. Note that the last entry must always be + * `libjulia`, and that all paths should be relative to this loader library path. + */ +#if !defined(DEP_LIBS) +#define DEP_LIBS "../lib/example.so:../lib/libjulia.so" +#endif + +// We need to dlopen() ourselves in order to introspect the libdir. +#if defined(JL_DEBUG_BUILD) +#define LIBJULIA_NAME "libjulia-debug" +#else +#define LIBJULIA_NAME "libjulia" +#endif + + +// Declarations from `loader_lib.c` and `loader_win_utils.c` +JL_DLLEXPORT extern int jl_load_repl(int, char **); +JL_DLLEXPORT void jl_loader_print_stderr(const char * msg); +void jl_loader_print_stderr3(const char * msg1, const char * msg2, const char * msg3); +static void * lookup_symbol(const void * lib_handle, const char * symbol_name); + +#ifdef _OS_WINDOWS_ +LPWSTR *CommandLineToArgv(LPWSTR lpCmdLine, int *pNumArgs); +int wchar_to_utf8(const wchar_t * wstr, char *str, size_t maxlen); +int utf8_to_wchar(const char * str, wchar_t *wstr, size_t maxlen); +void setup_stdio(void); +#endif + +#include "../src/jloptions.h" diff --git a/cli/loader_exe.c b/cli/loader_exe.c new file mode 100644 index 00000000000000..07a0bddcd4b87f --- /dev/null +++ b/cli/loader_exe.c @@ -0,0 +1,77 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +// This defines a bare-bones loader that opens `libjulia` and immediately invokes its `load_repl()` function. +#include "loader.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Bring in helper functions for windows without libgcc. */ +#ifdef _OS_WINDOWS_ +#include "loader_win_utils.c" +#endif + +JULIA_DEFINE_FAST_TLS + +#ifdef _COMPILER_ASAN_ENABLED_ +JL_DLLEXPORT const char* __asan_default_options() +{ + return "allow_user_segv_handler=1:detect_leaks=0"; + // FIXME: enable LSAN after fixing leaks & defining __lsan_default_suppressions(), + // or defining __lsan_default_options = exitcode=0 once publicly available + // (here and in flisp/flmain.c) +} +#endif + +#ifdef _OS_WINDOWS_ +int mainCRTStartup(void) +{ + int argc; + LPWSTR * wargv = CommandLineToArgv(GetCommandLine(), &argc); + char ** argv = (char **)malloc(sizeof(char*) * (argc + 1)); + setup_stdio(); +#else +int main(int argc, char * argv[]) +{ +#endif + +#if defined(_COMPILER_ASAN_ENABLED_) || defined(_COMPILER_TSAN_ENABLED_) + // ASAN/TSAN do not support RTLD_DEEPBIND + // https://github.com/google/sanitizers/issues/611 + putenv("LBT_USE_RTLD_DEEPBIND=0"); +#endif + + // Convert Windows wchar_t values to UTF8 +#ifdef _OS_WINDOWS_ + for (int i = 0; i < argc; i++) { + size_t max_arg_len = 4*wcslen(wargv[i]); + argv[i] = (char *)malloc(max_arg_len); + if (!wchar_to_utf8(wargv[i], argv[i], max_arg_len)) { + jl_loader_print_stderr("Unable to convert all arguments to UTF-8!\n"); + return 1; + } + } + argv[argc] = NULL; +#endif + + // Call load_repl with our initialization arguments: + int ret = jl_load_repl(argc, argv); + + // On Windows we're running without the CRT that would do this for us + exit(ret); + return ret; +} + +#if defined(__GLIBC__) && (defined(_COMPILER_ASAN_ENABLED_) || defined(_COMPILER_TSAN_ENABLED_)) +// fork is generally bad news, but it is better if we prevent applications from +// making it worse as openblas threadpools cause it to hang +int __register_atfork232(void (*prepare)(void), void (*parent)(void), void (*child)(void), void *dso_handle) { + return 0; +} +__asm__ (".symver __register_atfork232, __register_atfork@@GLIBC_2.3.2"); +#endif + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/cli/loader_lib.c b/cli/loader_lib.c new file mode 100644 index 00000000000000..74241510ffd257 --- /dev/null +++ b/cli/loader_lib.c @@ -0,0 +1,292 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +// This file defines an RPATH-style relative path loader for all platforms +#include "loader.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Bring in definitions of symbols exported from libjulia. */ +#include "jl_exports.h" + +/* Bring in helper functions for windows without libgcc. */ +#ifdef _OS_WINDOWS_ +#include "loader_win_utils.c" +#endif + +// Save DEP_LIBS to a variable that is explicitly sized for expansion +static char dep_libs[1024] = DEP_LIBS; + +JL_DLLEXPORT void jl_loader_print_stderr(const char * msg) +{ + fputs(msg, stderr); +} +// I use three arguments a lot. +void jl_loader_print_stderr3(const char * msg1, const char * msg2, const char * msg3) +{ + jl_loader_print_stderr(msg1); + jl_loader_print_stderr(msg2); + jl_loader_print_stderr(msg3); +} + +/* Wrapper around dlopen(), with extra relative pathing thrown in*/ +static void * load_library(const char * rel_path, const char * src_dir, int err) { + void * handle = NULL; + + // See if a handle is already open to the basename + const char *basename = rel_path + strlen(rel_path); + while (basename-- > rel_path) + if (*basename == PATHSEPSTRING[0] || *basename == '/') + break; + basename++; +#if defined(_OS_WINDOWS_) + if ((handle = GetModuleHandleA(basename))) + return handle; +#else + // if err == 0 the library is optional, so don't allow global lookups to see it + if ((handle = dlopen(basename, RTLD_NOLOAD | RTLD_NOW | (err ? RTLD_GLOBAL : RTLD_LOCAL)))) + return handle; +#endif + + char path[2*JL_PATH_MAX + 1] = {0}; + strncat(path, src_dir, sizeof(path) - 1); + strncat(path, PATHSEPSTRING, sizeof(path) - 1); + strncat(path, rel_path, sizeof(path) - 1); + +#if defined(_OS_WINDOWS_) + wchar_t wpath[2*JL_PATH_MAX + 1] = {0}; + if (!utf8_to_wchar(path, wpath, 2*JL_PATH_MAX)) { + jl_loader_print_stderr3("ERROR: Unable to convert path ", path, " to wide string!\n"); + exit(1); + } + handle = (void *)LoadLibraryExW(wpath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); +#else + handle = dlopen(path, RTLD_NOW | (err ? RTLD_GLOBAL : RTLD_LOCAL)); +#endif + + if (handle == NULL) { + if (!err) + return NULL; + jl_loader_print_stderr3("ERROR: Unable to load dependent library ", path, "\n"); +#if defined(_OS_WINDOWS_) + LPWSTR wmsg = TEXT(""); + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS | + FORMAT_MESSAGE_MAX_WIDTH_MASK, + NULL, GetLastError(), + MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), + (LPWSTR)&wmsg, 0, NULL); + char err[256] = {0}; + wchar_to_utf8(wmsg, err, 255); + jl_loader_print_stderr3("Message:", err, "\n"); +#else + char *dlerr = dlerror(); + if (dlerr != NULL) { + jl_loader_print_stderr3("Message:", dlerr, "\n"); + } +#endif + exit(1); + } + return handle; +} + +static void * lookup_symbol(const void * lib_handle, const char * symbol_name) { +#ifdef _OS_WINDOWS_ + return GetProcAddress((HMODULE) lib_handle, symbol_name); +#else + return dlsym((void *)lib_handle, symbol_name); +#endif +} + +// Find the location of libjulia. +char lib_dir[JL_PATH_MAX]; +JL_DLLEXPORT const char * jl_get_libdir() +{ + // Reuse the path if this is not the first call. + if (lib_dir[0] != 0) { + return lib_dir; + } +#if defined(_OS_WINDOWS_) + // On Windows, we use GetModuleFileNameW + wchar_t libjulia_path[JL_PATH_MAX]; + HMODULE libjulia = NULL; + + // Get a handle to libjulia. + if (!utf8_to_wchar(LIBJULIA_NAME, libjulia_path, JL_PATH_MAX)) { + jl_loader_print_stderr3("ERROR: Unable to convert path ", LIBJULIA_NAME, " to wide string!\n"); + exit(1); + } + libjulia = LoadLibraryW(libjulia_path); + if (libjulia == NULL) { + jl_loader_print_stderr3("ERROR: Unable to load ", LIBJULIA_NAME, "!\n"); + exit(1); + } + if (!GetModuleFileNameW(libjulia, libjulia_path, JL_PATH_MAX)) { + jl_loader_print_stderr("ERROR: GetModuleFileName() failed\n"); + exit(1); + } + if (!wchar_to_utf8(libjulia_path, lib_dir, JL_PATH_MAX)) { + jl_loader_print_stderr("ERROR: Unable to convert julia path to UTF-8\n"); + exit(1); + } +#else + // On all other platforms, use dladdr() + Dl_info info; + if (!dladdr(&jl_get_libdir, &info)) { + jl_loader_print_stderr("ERROR: Unable to dladdr(&jl_get_libdir)!\n"); + char *dlerr = dlerror(); + if (dlerr != NULL) { + jl_loader_print_stderr3("Message:", dlerr, "\n"); + } + exit(1); + } + strcpy(lib_dir, info.dli_fname); +#endif + // Finally, convert to dirname + const char * new_dir = dirname(lib_dir); + if (new_dir != lib_dir) { + // On some platforms, dirname() mutates. On others, it does not. + memcpy(lib_dir, new_dir, strlen(new_dir)+1); + } + return lib_dir; +} + +void * libjulia_internal = NULL; +__attribute__((constructor)) void jl_load_libjulia_internal(void) { + // Only initialize this once + if (libjulia_internal != NULL) { + return; + } + + // Introspect to find our own path + const char * lib_dir = jl_get_libdir(); + + // Pre-load libraries that libjulia-internal needs. + int deps_len = strlen(dep_libs); + char * curr_dep = &dep_libs[0]; + + // We keep track of "special" libraries names (ones whose name is prefixed with `@`) + // which are libraries that we want to load in some special, custom way, such as + // `libjulia-internal` or `libjulia-codegen`. + int special_idx = 0; + char * special_library_names[2] = {NULL}; + while (1) { + // try to find next colon character; if we can't, break out + char * colon = strchr(curr_dep, ':'); + if (colon == NULL) + break; + + // Chop the string at the colon so it's a valid-ending-string + *colon = '\0'; + + // If this library name starts with `@`, don't open it here (but mark it as special) + if (curr_dep[0] == '@') { + if (special_idx > sizeof(special_library_names)/sizeof(char *)) { + jl_loader_print_stderr("ERROR: Too many special library names specified, check LOADER_BUILD_DEP_LIBS and friends!\n"); + exit(1); + } + special_library_names[special_idx] = curr_dep + 1; + special_idx += 1; + } else { + load_library(curr_dep, lib_dir, 1); + } + + // Skip ahead to next dependency + curr_dep = colon + 1; + } + + if (special_idx != sizeof(special_library_names)/sizeof(char *)) { + jl_loader_print_stderr("ERROR: Too few special library names specified, check LOADER_BUILD_DEP_LIBS and friends!\n"); + exit(1); + } + + // Unpack our special library names. This is why ordering of library names matters. + libjulia_internal = load_library(special_library_names[0], lib_dir, 1); + void *libjulia_codegen = load_library(special_library_names[1], lib_dir, 0); + const char * const * codegen_func_names; + const char *codegen_liberr; + if (libjulia_codegen == NULL) { + // if codegen is not available, use fallback implementation in libjulia-internal + libjulia_codegen = libjulia_internal; + codegen_func_names = jl_codegen_fallback_func_names; + codegen_liberr = " from libjulia-internal\n"; + } + else { + codegen_func_names = jl_codegen_exported_func_names; + codegen_liberr = " from libjulia-codegen\n"; + } + + // Once we have libjulia-internal loaded, re-export its symbols: + for (unsigned int symbol_idx=0; jl_runtime_exported_func_names[symbol_idx] != NULL; ++symbol_idx) { + void *addr = lookup_symbol(libjulia_internal, jl_runtime_exported_func_names[symbol_idx]); + if (addr == NULL) { + jl_loader_print_stderr3("ERROR: Unable to load ", jl_runtime_exported_func_names[symbol_idx], " from libjulia-internal\n"); + exit(1); + } + (*jl_runtime_exported_func_addrs[symbol_idx]) = addr; + } + // jl_options must be initialized very early, in case an embedder sets some + // values there before calling jl_init + ((void (*)(void))jl_init_options_addr)(); + + for (unsigned int symbol_idx=0; codegen_func_names[symbol_idx] != NULL; ++symbol_idx) { + void *addr = lookup_symbol(libjulia_codegen, codegen_func_names[symbol_idx]); + if (addr == NULL) { + jl_loader_print_stderr3("ERROR: Unable to load ", codegen_func_names[symbol_idx], codegen_liberr); + exit(1); + } + (*jl_codegen_exported_func_addrs[symbol_idx]) = addr; + } + // Next, if we're on Linux/FreeBSD, set up fast TLS. +#if !defined(_OS_WINDOWS_) && !defined(_OS_DARWIN_) + void (*jl_pgcstack_setkey)(void*, void*(*)(void)) = lookup_symbol(libjulia_internal, "jl_pgcstack_setkey"); + if (jl_pgcstack_setkey == NULL) { + jl_loader_print_stderr("ERROR: Cannot find jl_pgcstack_setkey() function within libjulia-internal!\n"); + exit(1); + } + void *fptr = lookup_symbol(RTLD_DEFAULT, "jl_get_pgcstack_static"); + void *(*key)(void) = lookup_symbol(RTLD_DEFAULT, "jl_pgcstack_addr_static"); + if (fptr != NULL && key != NULL) + jl_pgcstack_setkey(fptr, key); +#endif + + // jl_options must be initialized very early, in case an embedder sets some + // values there before calling jl_init + ((void (*)(void))jl_init_options_addr)(); +} + +// Load libjulia and run the REPL with the given arguments (in UTF-8 format) +JL_DLLEXPORT int jl_load_repl(int argc, char * argv[]) { + // Some compilers/platforms are known to have `__attribute__((constructor))` issues, + // so we have a fallback call of `jl_load_libjulia_internal()` here. + if (libjulia_internal == NULL) { + jl_load_libjulia_internal(); + if (libjulia_internal == NULL) { + jl_loader_print_stderr("ERROR: libjulia-internal could not be loaded!\n"); + exit(1); + } + } + // Load the repl entrypoint symbol and jump into it! + int (*entrypoint)(int, char **) = (int (*)(int, char **))lookup_symbol(libjulia_internal, "jl_repl_entrypoint"); + if (entrypoint == NULL) { + jl_loader_print_stderr("ERROR: Unable to find `jl_repl_entrypoint()` within libjulia-internal!\n"); + exit(1); + } + return entrypoint(argc, (char **)argv); +} + +#ifdef _OS_WINDOWS_ +int __stdcall DllMainCRTStartup(void* instance, unsigned reason, void* reserved) { + setup_stdio(); + + // Because we override DllMainCRTStartup, we have to manually call our constructor methods + jl_load_libjulia_internal(); + return 1; +} +#endif + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/cli/loader_win_utils.c b/cli/loader_win_utils.c new file mode 100644 index 00000000000000..621834a030c52d --- /dev/null +++ b/cli/loader_win_utils.c @@ -0,0 +1,200 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +// Workarounds for compiling via mingw without using libgcc_s +typedef struct { + HANDLE fd; + BOOL isconsole; +} FILE; + +static FILE _stdout = { INVALID_HANDLE_VALUE }; +static FILE _stderr = { INVALID_HANDLE_VALUE }; + +FILE *stdout = &_stdout; +FILE *stderr = &_stderr; + +int loader_fwrite(const WCHAR *str, size_t nchars, FILE *out) { + DWORD written; + if (out->isconsole) { + if (WriteConsole(out->fd, str, nchars, &written, NULL)) + return written; + } else { + if (WriteFile(out->fd, str, sizeof(WCHAR) * nchars, &written, NULL)) + return written; + } + return -1; +} + +int loader_fputs(const char *str, FILE *out) { + wchar_t wstr[1024]; + utf8_to_wchar(str, wstr, 1024); + return fwrite(wstr, wcslen(wstr), out); +} + +void * loader_malloc(const size_t size) { + return HeapAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, size); +} + +void * loader_realloc(void * mem, const size_t size) { + return HeapReAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, mem, size); +} + +LPWSTR *CommandLineToArgv(LPWSTR lpCmdLine, int *pNumArgs) { + LPWSTR out = lpCmdLine; + LPWSTR cmd = out; + unsigned MaxEntries = 4; + unsigned backslashes = 0; + int in_quotes = 0; + int empty = 1; + LPWSTR *cmds; + *pNumArgs = 0; + cmds = (LPWSTR*)malloc(sizeof(LPWSTR) * MaxEntries); + while (1) { + WCHAR c = *lpCmdLine++; + switch (c) { + case 0: + if (!empty) { + *out++ = '\0'; + cmds[(*pNumArgs)++] = cmd; + } + cmds[*pNumArgs] = NULL; + return cmds; + default: + *out++ = c; + empty = 0; + break; + case '"': + out -= backslashes / 2; // remove half of the backslashes + if (backslashes % 2) + *(out - 1) = '"'; // replace \ with " + else + in_quotes = !in_quotes; // treat as quote delimater + empty = 0; + break; + case '\t': + case ' ': + if (in_quotes) { + *out++ = c; + } else if (!empty) { + *out++ = '\0'; + cmds[(*pNumArgs)++] = cmd; + cmd = out; + empty = 1; + if (*pNumArgs >= MaxEntries - 1) { + MaxEntries *= 2; + cmds = (LPWSTR*)realloc(cmds, sizeof(LPWSTR) * MaxEntries); + } + } + } + if (c == '\\') + backslashes++; + else + backslashes = 0; + } +} + +void setup_stdio() { + DWORD mode = 0; + _stdout.fd = GetStdHandle(STD_OUTPUT_HANDLE); + _stdout.isconsole = GetConsoleMode(_stdout.fd, &mode); + _stderr.fd = GetStdHandle(STD_ERROR_HANDLE); + _stderr.isconsole = GetConsoleMode(_stderr.fd, &mode); +} + +void loader_exit(int code) { + ExitProcess(code); +} + + +/* Utilities to convert from Windows' wchar_t stuff to UTF-8 */ +int wchar_to_utf8(const wchar_t * wstr, char *str, size_t maxlen) { + /* Fast-path empty strings, as WideCharToMultiByte() returns zero for them. */ + if (wstr[0] == L'\0') { + str[0] = '\0'; + return 1; + } + size_t len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL); + if (!len) + return 0; + if (len > maxlen) + return 0; + if (!WideCharToMultiByte(CP_UTF8, 0, wstr, -1, str, len, NULL, NULL)) + return 0; + return 1; +} + +int utf8_to_wchar(const char * str, wchar_t * wstr, size_t maxlen) { + /* Fast-path empty strings, as WideCharToMultiByte() returns zero for them. */ + if (str[0] == '\0') { + wstr[0] = L'\0'; + return 1; + } + size_t len = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if (!len) + return 0; + if (len > maxlen) + return 0; + if (!MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, len)) + return 0; + return 1; +} + +size_t loader_strlen(const char * x) { + int idx = 0; + while (x[idx] != 0) + idx++; + return idx; +} + +size_t loader_wcslen(const wchar_t * x) { + int idx = 0; + while (x[idx] != 0) + idx++; + return idx; +} + +char * loader_strncat(char * base, const char * tail, size_t maxlen) { + int base_len = strlen(base); + int tail_len = strlen(tail); + for (int idx=base_len; idx 0 && x[idx] != PATHSEPSTRING[0]) { + idx -= 1; + } + if (x[idx] == PATHSEPSTRING[0]) { + // Special-case x == "/" + if (idx == 0) { + x[1] = '\0'; + return x; + } else { + x[idx] = '\0'; + return x; + } + } + x[0] = '.'; + x[1] = '\0'; + return x; +} + +char * loader_strchr(const char * haystack, int needle) { + int idx=0; + while (haystack[idx] != needle) { + if (haystack[idx] == 0) { + return NULL; + } + idx++; + } + return (char *)haystack + idx; +} diff --git a/cli/trampolines/common.h b/cli/trampolines/common.h new file mode 100644 index 00000000000000..00d703c341515c --- /dev/null +++ b/cli/trampolines/common.h @@ -0,0 +1,77 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +#include "../../src/support/platform.h" + +// Preprocessor annoyances +#define CONCAT_(x,y) x##y +#define CONCAT(x,y) CONCAT_(x, y) +#define CNAMEADDR(name) CONCAT(CNAME(name),_addr) +#define STR_(x) #x +#define STR(x) STR_(x) +#define I(x) x + +// On macOS and 32-bit windows, we need to prepend underscores on symbols to match the C ABI +#if defined(__APPLE__) || (defined(_WIN32) && !defined(_WIN64)) +#define UNDERSCORE(x) _##x +#else +#define UNDERSCORE(x) x +#endif + +// Windows requires some help with the linker when it comes to debuginfo/exporting +#if defined(_WIN32) || defined(_WIN64) +#define DEBUGINFO(name) .def name; \ + .scl 2; \ + .type 32; \ + .endef +#define EXPORT(name) .section .drectve,"r"; \ + .ascii STR(-export:##I(name)); \ + .ascii " "; \ + .section .text +#elif defined(__ELF__) +#define DEBUGINFO(name) .type CNAME(name),@function +#define EXPORT(name) .size CNAME(name), . - CNAME(name) +#else +#define DEBUGINFO(name) +#define EXPORT(name) +#endif + +// Windows 64-bit uses SEH +#if defined(_WIN64) +#define SEH_START1(name) .seh_proc CNAME(name) +#define SEH_START2() .seh_endprologue +#define SEH_END() .seh_endproc +#else +#define SEH_START1(name) +#define SEH_START2() +#define SEH_END() +#endif + +// If we're compiling with control-flow branch protection, mark the trampoline entry +// points with `endbr{32,64}`, as appropriate on this arch +#if defined(__CET__) && __CET__ & 1 != 0 +#if defined(__x86_64__) +#define CET_START() endbr64 +#else +#define CET_START() endbr32 +#endif +#else +#define CET_START() +#endif + +// aarch64 on mac requires some special assembler syntax for both calculating memory +// offsets and even just the assembler statement separator token +#if defined(__aarch64__) +#if defined(__APPLE__) +#define PAGE(x) x##@PAGE +#define PAGEOFF(x) x##@PAGEOFF +#define SEP %% +#else +#define PAGE(x) x +#define PAGEOFF(x) :lo12:##x +#define SEP ; +#endif +#endif + +// If someday we need to mangle everything, we do so by defining this `CNAME()` +// to do something more complex than just `UNDERSCORE(x)`. +#define CNAME(x) UNDERSCORE(x) diff --git a/cli/trampolines/trampolines_aarch64.S b/cli/trampolines/trampolines_aarch64.S new file mode 100644 index 00000000000000..2d87ae6dcdb1cf --- /dev/null +++ b/cli/trampolines/trampolines_aarch64.S @@ -0,0 +1,21 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +#include "common.h" +#include "../../src/jl_exported_funcs.inc" + +#define XX(name) \ +.global CNAME(name) SEP \ +.cfi_startproc SEP \ +.p2align 2 SEP \ +CNAME(name)##: SEP \ + adrp x16, PAGE(CNAME(name##_addr)) SEP \ + ldr x16, [x16, PAGEOFF(CNAME(name##_addr))] SEP \ + br x16 SEP \ +.cfi_endproc SEP \ + +JL_RUNTIME_EXPORTED_FUNCS(XX) +#ifdef _OS_WINDOWS_ +JL_RUNTIME_EXPORTED_FUNCS_WIN(XX) +#endif +JL_CODEGEN_EXPORTED_FUNCS(XX) +#undef XX diff --git a/cli/trampolines/trampolines_arm.S b/cli/trampolines/trampolines_arm.S new file mode 100644 index 00000000000000..5ce6617f3f04e6 --- /dev/null +++ b/cli/trampolines/trampolines_arm.S @@ -0,0 +1,24 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +#include "common.h" +#include "../../src/jl_exported_funcs.inc" + +#define XX(name) \ +.global CNAME(name); \ +.cfi_startproc; \ +CNAME(name)##:; \ + ldr ip, CONCAT(.L,CNAMEADDR(name)); \ +CONCAT(.L,CNAME(name)): ;\ + add ip, pc, ip; \ + ldr pc, [ip]; \ + .align 2; \ +CONCAT(.L,CNAMEADDR(name))##: ; \ + .word CNAMEADDR(name)##-(CONCAT(.L,CNAME(name)) + 8); \ +.cfi_endproc; \ + +JL_RUNTIME_EXPORTED_FUNCS(XX) +#ifdef _OS_WINDOWS_ +JL_RUNTIME_EXPORTED_FUNCS_WIN(XX) +#endif +JL_CODEGEN_EXPORTED_FUNCS(XX) +#undef XX diff --git a/cli/trampolines/trampolines_i686.S b/cli/trampolines/trampolines_i686.S new file mode 100644 index 00000000000000..3d9cacf0ce652c --- /dev/null +++ b/cli/trampolines/trampolines_i686.S @@ -0,0 +1,22 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +#include "common.h" +#include "../../src/jl_exported_funcs.inc" + +#define XX(name) \ +DEBUGINFO(CNAME(name)); \ +.global CNAME(name); \ +.cfi_startproc; \ +CNAME(name)##:; \ + CET_START(); \ + jmpl *(CNAMEADDR(name)); \ + ud2; \ +.cfi_endproc; \ +EXPORT(name); \ + +JL_RUNTIME_EXPORTED_FUNCS(XX) +#ifdef _OS_WINDOWS_ +JL_RUNTIME_EXPORTED_FUNCS_WIN(XX) +#endif +JL_CODEGEN_EXPORTED_FUNCS(XX) +#undef XX diff --git a/cli/trampolines/trampolines_powerpc64le.S b/cli/trampolines/trampolines_powerpc64le.S new file mode 100644 index 00000000000000..8b32ef91d2464f --- /dev/null +++ b/cli/trampolines/trampolines_powerpc64le.S @@ -0,0 +1,29 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +#include "common.h" +#include "../../src/jl_exported_funcs.inc" + +// Notes: +// bctr: branch to CTR without LR update (tail-call) +// localentry: On PPC functions have a localentry that assumes r2 contains +// the TOC pointer, and a global entry point that sets r2. +// See 64-Bit ELF V2 ABI Specification: Power Architecture v1.4 + +#define XX(name) \ +.global CNAME(name); \ +.type CNAME(name)##, @function; \ +.cfi_startproc; \ +CNAME(name)##: ; \ + addis 2, 12, .TOC.-CNAME(name)##@ha; \ + addi 2, 2, .TOC.-CNAME(name)##@l; \ + .localentry CNAME(name)##,.-CNAME(name)##; \ + addis 12,2,CNAMEADDR(name)##@toc@ha; \ + ld 12,CNAMEADDR(name)##@toc@l(12); \ + mtctr 12; \ + bctr; \ +.cfi_endproc; \ +.size CNAME(name)##,.-CNAME(name)##; \ + +JL_RUNTIME_EXPORTED_FUNCS(XX) +JL_CODEGEN_EXPORTED_FUNCS(XX) +#undef XX diff --git a/cli/trampolines/trampolines_x86_64.S b/cli/trampolines/trampolines_x86_64.S new file mode 100644 index 00000000000000..3b800da56eee17 --- /dev/null +++ b/cli/trampolines/trampolines_x86_64.S @@ -0,0 +1,26 @@ +// This file is a part of Julia. License is MIT: https://julialang.org/license + +#include "common.h" +#include "../../src/jl_exported_funcs.inc" + +#define XX(name) \ +DEBUGINFO(name); \ +.global CNAME(name); \ +.cfi_startproc; \ +SEH_START1(name); \ +CNAME(name)##:; \ +SEH_START2(); \ + CET_START(); \ + mov CNAMEADDR(name)(%rip),%r11; \ + jmpq *%r11; \ + ud2; \ +SEH_END(); \ +.cfi_endproc; \ +EXPORT(name); \ + +JL_RUNTIME_EXPORTED_FUNCS(XX) +#ifdef _OS_WINDOWS_ +JL_RUNTIME_EXPORTED_FUNCS_WIN(XX) +#endif +JL_CODEGEN_EXPORTED_FUNCS(XX) +#undef XX diff --git a/contrib/README.md b/contrib/README.md index d1b2485dabe558..f75dc4488fb0bd 100644 --- a/contrib/README.md +++ b/contrib/README.md @@ -6,9 +6,8 @@ Installation |[ mac/ ](https://github.com/JuliaLang/julia/blob/master/contrib/mac/) | Mac install files | |[ windows/ ](https://github.com/JuliaLang/julia/blob/master/contrib/windows/) | Windows install files | |[ add_license_to_files.jl ](https://github.com/JuliaLang/julia/blob/master/contrib/add_license_to_files.jl ) | Add the Julia license to files in the Julia Project | -|[ check-whitespace.sh ](https://github.com/JuliaLang/julia/blob/master/contrib/check-whitespace.sh) | Check for trailing white space | +|[ check-whitespace.jl ](https://github.com/JuliaLang/julia/blob/master/contrib/check-whitespace.jl) | Check for white space issues | |[ commit-name.sh ](https://github.com/JuliaLang/julia/blob/master/contrib/commit-name.sh) | Computes a version name for a commit | -|[ filterArgs.sh ](https://github.com/JuliaLang/julia/blob/master/contrib/filterArgs.sh) | Update library search code to use only tokens that start with -L | |[ fixup-libgfortran.sh ](https://github.com/JuliaLang/julia/blob/master/contrib/fixup-libgfortran.sh) | Include libgfortran and libquadmath for installations | |[ fixup-libstdc++.sh ](https://github.com/JuliaLang/julia/blob/master/contrib/fixup-libstdc++.sh) | Include libstdc++ for installations | |[ install.sh ](https://github.com/JuliaLang/julia/blob/master/contrib/install.sh) | Installation script with different permissions | @@ -17,7 +16,6 @@ Installation |[ julia.desktop ](https://github.com/JuliaLang/julia/blob/master/contrib/julia.desktop) | GNOME desktop config file | |[ relative_path.py ](https://github.com/JuliaLang/julia/blob/master/contrib/relative_path.py) | Convert absolute paths into relative paths | |[ stringreplace.c ](https://github.com/JuliaLang/julia/blob/master/contrib/stringreplace.c) | Replace strings to hardcoded paths in binaries during `make install` | -|[ travis_fastfail.sh ](https://github.com/JuliaLang/julia/blob/master/contrib/travis_fastfail.sh ) | Checks for queued build tests in Travis | Debugging ========= @@ -25,4 +23,4 @@ Debugging | Name | Description | | ------------------------------ | ----------------------------------------------------------- | |[ debug_bootstrap.gdb ](https://github.com/JuliaLang/julia/blob/master/contrib/debug_bootstrap.gdb) | Bootstrap process using the debug build | -|[ valgrind-julia.supp ](https://github.com/JuliaLang/julia/blob/master/contrib/valgrind-julia.supp) | Suppressions for Valgrind debugging tool | +|[ valgrind-julia.supp ](https://github.com/JuliaLang/julia/blob/master/contrib/valgrind-julia.supp) | Suppressions for Valgrind debugging tool | diff --git a/contrib/add_license_to_files.jl b/contrib/add_license_to_files.jl index ce52881e2a031f..1d301a54553948 100644 --- a/contrib/add_license_to_files.jl +++ b/contrib/add_license_to_files.jl @@ -15,22 +15,24 @@ const print_result = true # prints files which where not processed. const rootdirs = [ "../base", + "../cli", "../contrib", "../src", "../stdlib", - "../test", ] -# to exculde whole sub directories +# to exclude whole sub directories const excludedirs = [ # see: https://github.com/JuliaLang/julia/pull/11073#issuecomment-98090053 - "../base/grisu", "../base/ryu", "../src/flisp", + "../stdlib/TOML/test/testfiles", + "../test/testhelpers/allocation_file.jl", ] const skipfiles = [ "../contrib/add_license_to_files.jl", + "../contrib/asan/check.jl", # files to check - already copyright # see: https://github.com/JuliaLang/julia/pull/11073#issuecomment-98099389 "../base/special/trig.jl", @@ -44,11 +46,10 @@ const skipfiles = [ "../src/abi_x86.cpp", "../src/abi_x86_64.cpp", "../src/disasm.cpp", - "../src/getopt.c", - "../src/getopt.h", "../src/support/END.h", "../src/support/ENTRY.amd64.h", "../src/support/ENTRY.i387.h", + "../src/support/_setjmp.win32.S", "../src/support/MurmurHash3.c", "../src/support/MurmurHash3.h", "../src/support/asprintf.c", @@ -58,6 +59,7 @@ const skipfiles = [ "../src/support/tzfile.h", "../src/support/utf8.c", "../src/crc32c.c", + "../src/mach_excUser.c", ] const ext_prefix = Dict([ @@ -66,6 +68,7 @@ const ext_prefix = Dict([ (".h", "// "), (".c", "// "), (".cpp", "// "), + (".S", "// "), ]) const new_license = "This file is a part of Julia. License is MIT: https://julialang.org/license" @@ -104,6 +107,7 @@ function getfilespaths!(filepaths::Vector, rootdir::AbstractString) abs_rootdir = abspath(rootdir) for name in readdir(abs_rootdir) path = joinpath(abs_rootdir, name) + islink(path) && continue if isdir(path) getfilespaths!(filepaths, path) else @@ -118,6 +122,7 @@ function add_license_line!(unprocessed::Vector, src::AbstractString, new_license for name in readdir(src) path = normpath(joinpath(src, name)) + islink(path) && continue if isdir(path) if path in abs_excludedirs getfilespaths!(unprocessed, path) diff --git a/contrib/asan/Make.user.asan b/contrib/asan/Make.user.asan new file mode 100644 index 00000000000000..96ed13b54e0f97 --- /dev/null +++ b/contrib/asan/Make.user.asan @@ -0,0 +1,27 @@ +TOOLCHAIN=$(BUILDROOT)/../toolchain +BINDIR=$(TOOLCHAIN)/usr/bin +TOOLDIR=$(TOOLCHAIN)/usr/tools + +# use our new toolchain +USECLANG=1 +override CC=$(TOOLDIR)/clang +override CXX=$(TOOLDIR)/clang++ +export ASAN_SYMBOLIZER_PATH=$(TOOLDIR)/llvm-symbolizer + +USE_BINARYBUILDER_LLVM=1 + +override SANITIZE=1 +override SANITIZE_ADDRESS=1 + +# make the GC use regular malloc/frees, which are hooked by ASAN +override WITH_GC_DEBUG_ENV=1 + +# default to a debug build for better line number reporting +override JULIA_BUILD_MODE=debug + +# Enable Julia assertions and LLVM assertions +FORCE_ASSERTIONS=1 +LLVM_ASSERTIONS=1 + +# Build a minimal system image +JULIA_PRECOMPILE=0 diff --git a/contrib/asan/Make.user.tools b/contrib/asan/Make.user.tools new file mode 100644 index 00000000000000..1bd6f97e39111d --- /dev/null +++ b/contrib/asan/Make.user.tools @@ -0,0 +1,2 @@ +USE_BINARYBUILDER_LLVM=1 +BUILD_LLVM_CLANG=1 diff --git a/contrib/asan/build.sh b/contrib/asan/build.sh new file mode 100755 index 00000000000000..77f3078b35c42e --- /dev/null +++ b/contrib/asan/build.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# This file is a part of Julia. License is MIT: https://julialang.org/license + +# +# Usage: +# contrib/asan/build.sh [...] +# +# Build ASAN-enabled julia. Given a workspace directory , build +# ASAN-enabled julia in /asan. Required toolss are install under +# /toolchain. This scripts also takes optional arguments +# which are passed to `make`. The default make target is `debug`. + +set -ue + +# `$WORKSPACE` is a directory in which we create `toolchain` and `asan` +# sub-directories. +WORKSPACE="$1" +shift +if [ "$WORKSPACE" = "" ]; then + echo "Workspace directory must be specified as the first argument" >&2 + exit 2 +fi + +mkdir -pv "$WORKSPACE" +WORKSPACE="$(cd "$WORKSPACE" && pwd)" +if [ "$WORKSPACE" = "" ]; then + echo "Failed to create the workspace directory." >&2 + exit 2 +fi + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +JULIA_HOME="$HERE/../../" + +echo +echo "Installing toolchain..." + +TOOLCHAIN="$WORKSPACE/toolchain" +if [ ! -d "$TOOLCHAIN" ]; then + make -C "$JULIA_HOME" configure O=$TOOLCHAIN + cp "$HERE/Make.user.tools" "$TOOLCHAIN/Make.user" +fi + +make -C "$TOOLCHAIN/deps" install-clang install-llvm-tools + +echo +echo "Building Julia..." + +BUILD="$WORKSPACE/asan" +if [ ! -d "$BUILD" ]; then + make -C "$JULIA_HOME" configure O="$BUILD" + cp "$HERE/Make.user.asan" "$BUILD/Make.user" +fi + +make -C "$BUILD" "$@" diff --git a/contrib/asan/check.jl b/contrib/asan/check.jl new file mode 100755 index 00000000000000..2933aaf3fb4e31 --- /dev/null +++ b/contrib/asan/check.jl @@ -0,0 +1,87 @@ +#!/bin/bash +# -*- mode: julia -*- +# This file is a part of Julia. License is MIT: https://julialang.org/license +# +# Usage: +# contrib/asan/check.jl +# +# Check that is built with ASAN. +# +#= +JULIA="${JULIA:-julia}" +exec "$JULIA" --startup-file=no --compile=min "${BASH_SOURCE[0]}" "$@" +=# + +function main(args = ARGS)::Int + if length(args) != 1 + @error "Expect a single argument" args + return 2 + end + julia, = args + + # It looks like double-free is easy to robustly trigger. + code = """ + @info "Testing a pattern that would trigger ASAN" + write(ARGS[1], "started") + + ptr = ccall(:malloc, Ptr{UInt}, (Csize_t,), 256) + ccall(:free, Cvoid, (Ptr{UInt},), ptr) + ccall(:free, Cvoid, (Ptr{UInt},), ptr) + + @error "Failed to trigger ASAN" + """ + + local proc + timeout = Threads.Atomic{Bool}(false) + isstarted = false + mktemp() do tmppath, tmpio + cmd = `$julia -e $code $tmppath` + # Note: Ideally, we set ASAN_SYMBOLIZER_PATH here. But there is no easy + # way to find out the path from just a Julia binary. + + @debug "Starting a process" cmd + proc = run(pipeline(cmd; stdout, stderr); wait = false) + timer = Timer(10) + @sync try + @async begin + try + wait(timer) + true + catch err + err isa EOFError || rethrow() + false + end && begin + timeout[] = true + kill(proc) + end + end + wait(proc) + finally + close(timer) + end + + # At the very beginning of the process, the `julia` subprocess put a + # marker that it is successfully started. This is to avoid mixing + # non-functional `julia` binary (or even non-`julia` command) and + # correctly working `julia` with ASAN: + isstarted = read(tmpio, String) == "started" + end + + if timeout[] + @error "Timeout waiting for the subprocess" + return 1 + elseif success(proc) + @error "ASAN was not triggered" + return 1 + elseif !isstarted + @error "Failed to start the process" + return 1 + else + @info "ASAN is functional in the Julia binary `$julia`" + return 0 + end +end + +if abspath(PROGRAM_FILE) == @__FILE__ + exit(main()) +end diff --git a/contrib/bpftrace/gc_all.bt b/contrib/bpftrace/gc_all.bt new file mode 100755 index 00000000000000..f78e8f3aa607d8 --- /dev/null +++ b/contrib/bpftrace/gc_all.bt @@ -0,0 +1,44 @@ +#!/usr/bin/env bpftrace + +BEGIN +{ + printf("Tracing Julia GC Times... Hit Ctrl-C to end.\n"); +} + +usdt:usr/lib/libjulia-internal.so:julia:gc__begin +{ + $now = nsecs; + @time[pid] = $now; + @start[pid] = $now; +} + +usdt:usr/lib/libjulia-internal.so:julia:gc__stop_the_world +/@start[pid]/ +{ + $now = nsecs; + @stop_the_world_usecs[pid] = hist(($now - @time[pid]) / 1000); + @time[pid] = $now; +} + +usdt:usr/lib/libjulia-internal.so:julia:gc__end +/@start[pid]/ +{ + $now = nsecs; + @gc_total_usecs[pid] = hist(($now - @start[pid]) / 1000); + @gc_phase_usecs[pid] = hist(($now - @time[pid]) / 1000); + @time[pid] = $now; + delete(@start[pid]); +} + +usdt:usr/lib/libjulia-internal.so:julia:gc__finalizer +/@time[pid]/ +{ + @finalizer[pid] = hist((nsecs - @time[pid]) / 1000); + delete(@time[pid]); +} + +END +{ + clear(@start); + clear(@time); +} diff --git a/contrib/bpftrace/gc_simple.bt b/contrib/bpftrace/gc_simple.bt new file mode 100755 index 00000000000000..559f41c41cf72c --- /dev/null +++ b/contrib/bpftrace/gc_simple.bt @@ -0,0 +1,23 @@ +#!/usr/bin/env bpftrace + +BEGIN +{ + printf("Tracing Julia GC Times... Hit Ctrl-C to end.\n"); +} + +usdt:usr/lib/libjulia-internal.so:julia:gc__begin +{ + @start[pid] = nsecs; +} + +usdt:usr/lib/libjulia-internal.so:julia:gc__end +/@start[pid]/ +{ + @usecs[pid] = hist((nsecs - @start[pid]) / 1000); + delete(@start[pid]); +} + +END +{ + clear(@start); +} diff --git a/contrib/bpftrace/gc_stop_the_world_latency.bt b/contrib/bpftrace/gc_stop_the_world_latency.bt new file mode 100755 index 00000000000000..8e541bcb421e2d --- /dev/null +++ b/contrib/bpftrace/gc_stop_the_world_latency.bt @@ -0,0 +1,23 @@ +#!/usr/bin/env bpftrace + +BEGIN +{ + printf("Tracing Julia GC Stop-The-World Latency... Hit Ctrl-C to end.\n"); +} + +usdt:usr/lib/libjulia-internal.so:julia:gc__begin +{ + @start[pid] = nsecs; +} + +usdt:usr/lib/libjulia-internal.so:julia:gc__stop_the_world +/@start[pid]/ +{ + @usecs[pid] = hist((nsecs - @start[pid]) / 1000); + delete(@start[pid]); +} + +END +{ + clear(@start); +} diff --git a/contrib/bpftrace/rt_all.bt b/contrib/bpftrace/rt_all.bt new file mode 100755 index 00000000000000..d4de28e354a508 --- /dev/null +++ b/contrib/bpftrace/rt_all.bt @@ -0,0 +1,81 @@ +#!/usr/bin/env bpftrace + +BEGIN +{ + printf("Tracing Julia Task events... Hit Ctrl-C to end.\n"); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__run__task +{ + printf("Task running: %x\n", arg0); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__pause__task +{ + printf("Task pausing: %x\n", arg0); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__new__task +{ + printf("Task created: %x (Parent %x)\n", arg1, arg0); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__start__task +{ + printf("Task started: %x\n", arg0); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__finish__task +{ + printf("Task finished: %x\n", arg0); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__start__process__events +{ + printf("Task processing libuv events: %x\n", arg0); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__finish__process__events +{ + printf("Task processed libuv events: %x\n", arg0); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__taskq__insert +{ + printf("Thread %x inserting task to multiq: %x\n", arg0, arg1); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__taskq__get +{ + printf("Thread %x popped task from multiq: %x\n", arg0, arg1); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__sleep__check__wake +{ + printf("Thread waking: %x (was sleeping?: %d)\n", arg0, arg1); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__sleep__check__wakeup +{ + printf("Thread wakeup: %x\n", arg0); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__sleep__check__sleep +{ + printf("Thread trying to sleep: %x\n", arg0); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__sleep__check__taskq__wake +{ + printf("Thread waking due to non-empty task queue: %x\n", arg0); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__sleep__check__task__wake +{ + printf("Thread waking due to popped task: %x\n", arg0); +} + +usdt:usr/lib/libjulia-internal.so:julia:rt__sleep__check__uv__wake +{ + printf("Thread waking due to libuv: %x\n", arg0); +} diff --git a/contrib/check-whitespace.jl b/contrib/check-whitespace.jl new file mode 100755 index 00000000000000..4d078d400daea4 --- /dev/null +++ b/contrib/check-whitespace.jl @@ -0,0 +1,55 @@ +#!/usr/bin/env julia + +const patterns = split(""" + *.1 + *.c + *.cpp + *.h + *.inc + *.jl + *.lsp + *.make + *.md + *.mk + *.rst + *.scm + *.sh + *.yml + *Makefile +""") + +const errors = Set{Tuple{String,Int,String}}() + +for path in eachline(`git ls-files -- $patterns`) + lineno = 0 + non_blank = 0 + + file_err(msg) = push!(errors, (path, 0, msg)) + line_err(msg) = push!(errors, (path, lineno, msg)) + + for line in eachline(path, keep=true) + lineno += 1 + contains(line, '\r') && file_err("non-UNIX line endings") + contains(line, '\ua0') && line_err("non-breaking space") + endswith(line, '\n') || line_err("no trailing newline") + line = chomp(line) + endswith(line, r"\s") && line_err("trailing whitespace") + contains(line, r"\S") && (non_blank = lineno) + end + non_blank < lineno && line_err("trailing blank lines") +end + +if isempty(errors) + println(stderr, "Whitespace check found no issues.") + exit(0) +else + println(stderr, "Whitespace check found $(length(errors)) issues:") + for (path, lineno, msg) in sort!(collect(errors)) + if lineno == 0 + println(stderr, "$path -- $msg") + else + println(stderr, "$path:$lineno -- $msg") + end + end + exit(1) +end diff --git a/contrib/check-whitespace.sh b/contrib/check-whitespace.sh deleted file mode 100755 index c380d7bdd29691..00000000000000 --- a/contrib/check-whitespace.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/sh -# This file is a part of Julia. License is MIT: https://julialang.org/license - -# Check for trailing white space in source files; -# report an error if so - -# Files to check: -set -f # disable glob expansion in this script -file_patterns=' -*.1 -*.c -*.cpp -*.h -*.jl -*.lsp -*.scm -*.inc -*.make -*.mk -*.md -*.rst -*.sh -*.yml -*Makefile -' - -# TODO: Look also for trailing empty lines, and missing '\n' after the last line -if git --no-pager grep --color -n --full-name -e ' $' -- $file_patterns; then - echo "Error: trailing whitespace found in source file(s)" - echo "" - echo "This can often be fixed with:" - echo " git rebase --whitespace=fix HEAD~1" - echo "or" - echo " git rebase --whitespace=fix master" - echo "and then a forced push of the correct branch" - exit 1 -fi diff --git a/contrib/codesign.sh b/contrib/codesign.sh new file mode 100755 index 00000000000000..03866c4bb1ac1b --- /dev/null +++ b/contrib/codesign.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# This file is a part of Julia. License is MIT: https://julialang.org/license + +# Codesign binary files for macOS. + +usage() { + echo "Usage: ${0} MACOS_CODESIGN_IDENTITY FILE-OR-DIRECTORY" + exit 0 +} + +# Default codesign identity to `-` if not provided +if [ -z "${1}" ]; then + MACOS_CODESIGN_IDENTITY="-" + ENTITLEMENTS="" +else + MACOS_CODESIGN_IDENTITY="${1}" + ENTITLEMENTS="--entitlements $(dirname "${0}")/mac/app/Entitlements.plist" +fi + +if [ "${#}" -eq 2 ]; then + if [ -f "${2}" ]; then + # Codesign only the given file + MACHO_FILES="${2}" + elif [ -d "${2}" ]; then + # Find all files in the given directory + MACHO_FILES=$(find "${2}" -type f -perm -0111 | cut -d: -f1) + else + usage + fi +else + usage +fi + +echo "Codesigning with identity ${MACOS_CODESIGN_IDENTITY}" +for f in ${MACHO_FILES}; do + echo "Codesigning ${f}..." + codesign -s "${MACOS_CODESIGN_IDENTITY}" --option=runtime ${ENTITLEMENTS} -vvv --timestamp --deep --force "${f}" +done diff --git a/contrib/download_cmake.sh b/contrib/download_cmake.sh index d122e1e0f07d45..1deeb08ddded24 100755 --- a/contrib/download_cmake.sh +++ b/contrib/download_cmake.sh @@ -8,31 +8,38 @@ mkdir -p "$(dirname "$0")"/../deps/scratch cd "$(dirname "$0")"/../deps/scratch CMAKE_VERSION_MAJOR=3 -CMAKE_VERSION_MINOR=7 -CMAKE_VERSION_PATCH=1 +CMAKE_VERSION_MINOR=19 +CMAKE_VERSION_PATCH=3 CMAKE_VERSION_MAJMIN=$CMAKE_VERSION_MAJOR.$CMAKE_VERSION_MINOR CMAKE_VERSION=$CMAKE_VERSION_MAJMIN.$CMAKE_VERSION_PATCH # listed at https://cmake.org/files/v$CMAKE_VERSION_MAJMIN/cmake-$CMAKE_VERSION-SHA-256.txt -# for the files cmake-$CMAKE_VERSION-Darwin-x86_64.tar.gz -# and cmake-$CMAKE_VERSION-Linux-x86_64.tar.gz -CMAKE_SHA256_DARWIN=1851d1448964893fdc5a8c05863326119f397a3790e0c84c40b83499c7960267 -CMAKE_SHA256_LINUX=7b4b7a1d9f314f45722899c0521c261e4bfab4a6b532609e37fef391da6bade2 +# for the files cmake-$CMAKE_VERSION-macos-universal.tar.gz +# cmake-$CMAKE_VERSION-Linux-x86_64.tar.gz and cmake-$CMAKE_VERSION-Linux-aarch64.tar.gz +CMAKE_SHA256_DARWIN=a6b79ad05f89241a05797510e650354d74ff72cc988981cdd1eb2b3b2bda66ac +CMAKE_SHA256_LINUX_X86_64=c18b65697e9679e5c88dccede08c323cd3d3730648e59048047bba82097e0ffc +CMAKE_SHA256_LINUX_AARCH64=66e507c97ffb586d7ca6567890808b792c8eb004b645706df6fbf27826a395a2 PLATFORM="$(uname)-$(uname -m)" -FULLNAME=cmake-$CMAKE_VERSION-$PLATFORM case $PLATFORM in - Darwin-x86_64) + Darwin-*) + FULLNAME=cmake-$CMAKE_VERSION-macos-universal ../tools/jldownload https://cmake.org/files/v$CMAKE_VERSION_MAJMIN/$FULLNAME.tar.gz echo "$CMAKE_SHA256_DARWIN $FULLNAME.tar.gz" | shasum -a 256 -c - CMAKE_EXTRACTED_PATH=$FULLNAME/CMake.app/Contents/bin/cmake;; Linux-x86_64) + FULLNAME=cmake-$CMAKE_VERSION-$PLATFORM ../tools/jldownload https://cmake.org/files/v$CMAKE_VERSION_MAJMIN/$FULLNAME.tar.gz - echo "$CMAKE_SHA256_LINUX $FULLNAME.tar.gz" | sha256sum -c - + echo "$CMAKE_SHA256_LINUX_X86_64 $FULLNAME.tar.gz" | sha256sum -c - + CMAKE_EXTRACTED_PATH=$FULLNAME/bin/cmake;; + Linux-aarch64) + FULLNAME=cmake-$CMAKE_VERSION-$PLATFORM + ../tools/jldownload https://cmake.org/files/v$CMAKE_VERSION_MAJMIN/$FULLNAME.tar.gz + echo "$CMAKE_SHA256_LINUX_AARCH64 $FULLNAME.tar.gz" | sha256sum -c - CMAKE_EXTRACTED_PATH=$FULLNAME/bin/cmake;; *) - echo "This script only supports x86_64 Mac and Linux. For other platforms," >&2 - echo "get cmake from your package manager or compile it from source." >&2 + echo "This script only supports Mac and Linux, both for x86_64 and aarch64." >&2 + echo "For other platforms, get cmake from your package manager or compile it from source." >&2 exit 1;; esac diff --git a/contrib/filterArgs.sh b/contrib/filterArgs.sh deleted file mode 100755 index 823745e004e6ea..00000000000000 --- a/contrib/filterArgs.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -# This file is a part of Julia. License is MIT: https://julialang.org/license - -# Loop over all command line arguments -for i in "$@"; do - # If an argument starts with -L, echo it out sans -L! - case $i in - -L*) printf '"%s"\n' "${i#-L}" ;; - esac -done diff --git a/contrib/fixup-libgfortran.sh b/contrib/fixup-libgfortran.sh index 897a1a0119e94d..6121665fb5a869 100755 --- a/contrib/fixup-libgfortran.sh +++ b/contrib/fixup-libgfortran.sh @@ -14,7 +14,7 @@ debug() { :; } fi if [ -z "$1" ]; then - echo "Usage: $0 " + echo "Usage: $0 [--verbose] " exit 1 fi @@ -160,4 +160,3 @@ for lib in libopenblas libcholmod liblapack $SONAMES; do done done done - diff --git a/contrib/fixup-libstdc++.sh b/contrib/fixup-libstdc++.sh index ee84094169b61c..1c19d98a54b1e4 100755 --- a/contrib/fixup-libstdc++.sh +++ b/contrib/fixup-libstdc++.sh @@ -3,7 +3,7 @@ # Run as: fixup-libstdc++.sh -if [ -z "$1" ]; then +if [ "$#" -ne 2 ]; then echo "Usage: $0 " exit 1 fi @@ -11,8 +11,8 @@ fi libdir="$1" private_libdir="$2" -if [ ! -f "$libdir/libjulia.so" ]; then - echo "ERROR: Could not open $libdir/libjulia.so" >&2 +if [ ! -f "$private_libdir/libjulia-internal.so" ]; then + echo "ERROR: Could not open $private_libdir/libjulia-internal.so" >&2 exit 2 fi @@ -24,7 +24,7 @@ find_shlib () } # Discover libstdc++ location and name -LIBSTD=$(find_shlib "$libdir/libjulia.so" "libstdc++.so") +LIBSTD=$(find_shlib "$private_libdir/libjulia-internal.so" "libstdc++.so") LIBSTD_NAME=$(basename $LIBSTD) LIBSTD_DIR=$(dirname $LIBSTD) diff --git a/contrib/generate_precompile.jl b/contrib/generate_precompile.jl index d52e16036801e6..a10d195229cabf 100644 --- a/contrib/generate_precompile.jl +++ b/contrib/generate_precompile.jl @@ -1,6 +1,10 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -if isempty(ARGS) || ARGS[1] !== "0" +if Threads.nthreads() != 1 + @warn "Running this file with multiple Julia threads may lead to a build error" Threads.nthreads() +end + +if Base.isempty(Base.ARGS) || Base.ARGS[1] !== "0" Sys.__init_build() # Prevent this from being put into the Main namespace @eval Module() begin @@ -9,6 +13,7 @@ if !isdefined(Base, :uv_eventloop) end Base.include(@__MODULE__, joinpath(Sys.BINDIR, "..", "share", "julia", "test", "testhelpers", "FakePTYs.jl")) import .FakePTYs: open_fake_pty +using Base.Meta CTRL_C = '\x03' UP_ARROW = "\e[A" @@ -16,24 +21,43 @@ DOWN_ARROW = "\e[B" hardcoded_precompile_statements = """ # used by Revise.jl -@assert precompile(Tuple{typeof(Base.parse_cache_header), String}) -@assert precompile(Tuple{typeof(pushfirst!), Vector{Any}, Function}) +precompile(Tuple{typeof(Base.parse_cache_header), String}) +precompile(Base.read_dependency_src, (String, String)) + # used by Requires.jl -@assert precompile(Tuple{typeof(get!), Type{Vector{Function}}, Dict{Base.PkgId,Vector{Function}}, Base.PkgId}) -@assert precompile(Tuple{typeof(haskey), Dict{Base.PkgId,Vector{Function}}, Base.PkgId}) -@assert precompile(Tuple{typeof(delete!), Dict{Base.PkgId,Vector{Function}}, Base.PkgId}) -@assert precompile(Tuple{typeof(push!), Vector{Function}, Function}) +precompile(Tuple{typeof(get!), Type{Vector{Function}}, Dict{Base.PkgId,Vector{Function}}, Base.PkgId}) +precompile(Tuple{typeof(haskey), Dict{Base.PkgId,Vector{Function}}, Base.PkgId}) +precompile(Tuple{typeof(delete!), Dict{Base.PkgId,Vector{Function}}, Base.PkgId}) +precompile(Tuple{typeof(push!), Vector{Function}, Function}) + # miscellaneous -@assert precompile(Tuple{typeof(Base.require), Base.PkgId}) -@assert precompile(Tuple{typeof(isassigned), Core.SimpleVector, Int}) -@assert precompile(Tuple{typeof(Base.Experimental.register_error_hint), Any, Type}) +precompile(Tuple{typeof(Base.require), Base.PkgId}) +precompile(Tuple{typeof(Base.recursive_prefs_merge), Base.Dict{String, Any}}) +precompile(Tuple{typeof(isassigned), Core.SimpleVector, Int}) +precompile(Tuple{typeof(getindex), Core.SimpleVector, Int}) +precompile(Tuple{typeof(Base.Experimental.register_error_hint), Any, Type}) +precompile(Tuple{typeof(Base.display_error), Base.ExceptionStack}) +precompile(Tuple{Core.kwftype(typeof(Type)), NamedTuple{(:sizehint,), Tuple{Int}}, Type{IOBuffer}}) +precompile(Base.CoreLogging.current_logger_for_env, (Base.CoreLogging.LogLevel, String, Module)) +precompile(Base.CoreLogging.current_logger_for_env, (Base.CoreLogging.LogLevel, Symbol, Module)) +precompile(Base.CoreLogging.env_override_minlevel, (Symbol, Module)) +precompile(Base.StackTraces.lookup, (Ptr{Nothing},)) """ -precompile_script = """ +for T in (Float16, Float32, Float64), IO in (IOBuffer, IOContext{IOBuffer}, Base.TTY, IOContext{Base.TTY}) + global hardcoded_precompile_statements + hardcoded_precompile_statements *= "precompile(Tuple{typeof(show), $IO, $T})\n" +end + +repl_script = """ 2+2 print("") +printstyled("a", "b") +display([1]) +display([1 2; 3 4]) @time 1+1 ; pwd +$CTRL_C ? reinterpret using Ra\t$CTRL_C \\alpha\t$CTRL_C @@ -45,13 +69,41 @@ f(x) = x03 f(1,2) [][1] cd("complet_path\t\t$CTRL_C -# Used by JuliaInterpreter -push!(Set{Module}(), Main) -push!(Set{Method}(), first(methods(collect))) -# Used by Revise -(setindex!(Dict{String,Base.PkgId}(), Base.PkgId(Base), "file.jl"))["file.jl"] -(setindex!(Dict{Base.PkgId,String}(), "file.jl", Base.PkgId(Base)))[Base.PkgId(Base)] -get(Base.pkgorigins, Base.PkgId(Base), nothing) +""" + +precompile_script = """ +# NOTE: these were moved to the end of Base.jl. TODO: move back here. +# # Used by Revise & its dependencies +# while true # force inference +# delete!(push!(Set{Module}(), Base), Main) +# m = first(methods(+)) +# delete!(push!(Set{Method}(), m), m) +# empty!(Set()) +# push!(push!(Set{Union{GlobalRef,Symbol}}(), :two), GlobalRef(Base, :two)) +# (setindex!(Dict{String,Base.PkgId}(), Base.PkgId(Base), "file.jl"))["file.jl"] +# (setindex!(Dict{Symbol,Vector{Int}}(), [1], :two))[:two] +# (setindex!(Dict{Base.PkgId,String}(), "file.jl", Base.PkgId(Base)))[Base.PkgId(Base)] +# (setindex!(Dict{Union{GlobalRef,Symbol}, Vector{Int}}(), [1], :two))[:two] +# (setindex!(IdDict{Type, Union{Missing, Vector{Tuple{LineNumberNode, Expr}}}}(), missing, Int))[Int] +# Dict{Symbol, Union{Nothing, Bool, Symbol}}(:one => false)[:one] +# Dict(Base => [:(1+1)])[Base] +# Dict(:one => [1])[:one] +# Dict("abc" => Set())["abc"] +# pushfirst!([], sum) +# get(Base.pkgorigins, Base.PkgId(Base), nothing) +# sort!([1,2,3]) +# unique!([1,2,3]) +# cumsum([1,2,3]) +# append!(Int[], BitSet()) +# isempty(BitSet()) +# delete!(BitSet([1,2]), 3) +# deleteat!(Int32[1,2,3], [1,3]) +# deleteat!(Any[1,2,3], [1,3]) +# Core.svec(1, 2) == Core.svec(3, 4) +# # copy(Core.Compiler.retrieve_code_info(Core.Compiler.specialize_method(which(+, (Int, Int)), [Int, Int], Core.svec()))) +# any(t->t[1].line > 1, [(LineNumberNode(2,:none),:(1+1))]) +# break # end force inference +# end """ julia_exepath() = joinpath(Sys.BINDIR, Base.julia_exename()) @@ -60,7 +112,7 @@ have_repl = haskey(Base.loaded_modules, Base.PkgId(Base.UUID("3fa0cd96-eef1-5676-8a61-b3b8758bbffb"), "REPL")) if have_repl hardcoded_precompile_statements *= """ - @assert precompile(Tuple{typeof(getproperty), REPL.REPLBackend, Symbol}) + precompile(Tuple{typeof(getproperty), REPL.REPLBackend, Symbol}) """ end @@ -68,25 +120,39 @@ Distributed = get(Base.loaded_modules, Base.PkgId(Base.UUID("8ba89e20-285c-5b6f-9357-94700520ee1b"), "Distributed"), nothing) if Distributed !== nothing + hardcoded_precompile_statements *= """ + precompile(Tuple{typeof(Distributed.remotecall),Function,Int,Module,Vararg{Any, 100}}) + precompile(Tuple{typeof(Distributed.procs)}) + precompile(Tuple{typeof(Distributed.finalize_ref), Distributed.Future}) + """ +# This is disabled because it doesn't give much benefit +# and the code in Distributed is poorly typed causing many invalidations +#= precompile_script *= """ using Distributed addprocs(2) pmap(x->iseven(x) ? 1 : 0, 1:4) @distributed (+) for i = 1:100 Int(rand(Bool)) end """ +=# end + Artifacts = get(Base.loaded_modules, Base.PkgId(Base.UUID("56f22d72-fd6d-98f1-02f0-08ddc0907c33"), "Artifacts"), nothing) if Artifacts !== nothing precompile_script *= """ - using Artifacts, Base.BinaryPlatforms - artifacts_toml = abspath($(repr(joinpath(Sys.STDLIB, "Artifacts", "test", "Artifacts.toml")))) - cd(() -> @artifact_str("c_simple"), dirname(artifacts_toml)) + using Artifacts, Base.BinaryPlatforms, Libdl + artifacts_toml = abspath(joinpath(Sys.STDLIB, "Artifacts", "test", "Artifacts.toml")) + artifact_hash("HelloWorldC", artifacts_toml) + oldpwd = pwd(); cd(dirname(artifacts_toml)) + macroexpand(Main, :(@artifact_str("HelloWorldC"))) + cd(oldpwd) artifacts = Artifacts.load_artifacts_toml(artifacts_toml) - platforms = [Artifacts.unpack_platform(e, "c_simple", artifacts_toml) for e in artifacts["c_simple"]] + platforms = [Artifacts.unpack_platform(e, "HelloWorldC", artifacts_toml) for e in artifacts["HelloWorldC"]] best_platform = select_platform(Dict(p => triplet(p) for p in platforms)) + dlopen("libjulia$(ccall(:jl_is_debugbuild, Cint, ()) != 0 ? "-debug" : "")", RTLD_LAZY | RTLD_DEEPBIND) """ end @@ -96,7 +162,8 @@ Pkg = get(Base.loaded_modules, nothing) if Pkg !== nothing - precompile_script *= Pkg.precompile_script + # TODO: Split Pkg precompile script into REPL and script part + repl_script *= Pkg.precompile_script end FileWatching = get(Base.loaded_modules, @@ -104,8 +171,9 @@ FileWatching = get(Base.loaded_modules, nothing) if FileWatching !== nothing hardcoded_precompile_statements *= """ - @assert precompile(Tuple{typeof(FileWatching.watch_file), String, Float64}) - @assert precompile(Tuple{typeof(FileWatching.watch_file), String, Int}) + precompile(Tuple{typeof(FileWatching.watch_file), String, Float64}) + precompile(Tuple{typeof(FileWatching.watch_file), String, Int}) + precompile(Tuple{typeof(FileWatching._uv_hook_close), FileWatching.FileMonitor}) """ end @@ -118,28 +186,103 @@ if Libdl !== nothing """ end +Test = get(Base.loaded_modules, + Base.PkgId(Base.UUID("8dfed614-e22c-5e08-85e1-65c5234f0b40"), "Test"), + nothing) +if Test !== nothing + hardcoded_precompile_statements *= """ + precompile(Tuple{typeof(Test.do_test), Test.ExecutionResult, Any}) + precompile(Tuple{typeof(Test.testset_beginend_call), Tuple{String, Expr}, Expr, LineNumberNode}) + precompile(Tuple{Type{Test.DefaultTestSet}, String}) + precompile(Tuple{Type{Test.DefaultTestSet}, AbstractString}) + precompile(Tuple{Core.kwftype(Type{Test.DefaultTestSet}), Any, Type{Test.DefaultTestSet}, AbstractString}) + precompile(Tuple{typeof(Test.finish), Test.DefaultTestSet}) + precompile(Tuple{typeof(Test.eval_test), Expr, Expr, LineNumberNode, Bool}) + precompile(Tuple{typeof(Test._inferred), Expr, Module}) + precompile(Tuple{typeof(Test.push_testset), Test.DefaultTestSet}) + precompile(Tuple{typeof(Test.get_alignment), Test.DefaultTestSet, Int}) + precompile(Tuple{typeof(Test.get_test_result), Any, Any}) + precompile(Tuple{typeof(Test.do_test_throws), Test.ExecutionResult, Any, Any}) + precompile(Tuple{typeof(Test.print_counts), Test.DefaultTestSet, Int, Int, Int, Int, Int, Int, Int}) + precompile(Tuple{typeof(Test._check_testset), Type, Expr}) + precompile(Tuple{typeof(Test.test_expr!), Any, Any}) + precompile(Tuple{typeof(Test.test_expr!), Any, Any, Vararg{Any, 100}}) + precompile(Tuple{typeof(Test.pop_testset)}) + precompile(Tuple{typeof(Test.match_logs), Function, Tuple{Symbol, Regex}}) + precompile(Tuple{typeof(Test.match_logs), Function, Tuple{String, Regex}}) + precompile(Tuple{typeof(Base.CoreLogging.shouldlog), Test.TestLogger, Base.CoreLogging.LogLevel, Module, Symbol, Symbol}) + precompile(Tuple{typeof(Base.CoreLogging.handle_message), Test.TestLogger, Base.CoreLogging.LogLevel, String, Module, Symbol, Symbol, String, Int}) + precompile(Tuple{typeof(Core.kwfunc(Base.CoreLogging.handle_message)), typeof((exception=nothing,)), typeof(Base.CoreLogging.handle_message), Test.TestLogger, Base.CoreLogging.LogLevel, String, Module, Symbol, Symbol, String, Int}) + precompile(Tuple{typeof(Test.detect_ambiguities), Any}) + precompile(Tuple{typeof(Test.collect_test_logs), Function}) + precompile(Tuple{typeof(Test.do_broken_test), Test.ExecutionResult, Any}) + precompile(Tuple{typeof(Test.record), Test.DefaultTestSet, Union{Test.Error, Test.Fail}}) + precompile(Tuple{typeof(Test.filter_errors), Test.DefaultTestSet}) + """ +end + +Profile = get(Base.loaded_modules, + Base.PkgId(Base.UUID("9abbd945-dff8-562f-b5e8-e1ebf5ef1b79"), "Profile"), + nothing) +if Profile !== nothing + repl_script *= Profile.precompile_script + hardcoded_precompile_statements *= """ + precompile(Tuple{typeof(Profile.tree!), Profile.StackFrameTree{UInt64}, Vector{UInt64}, Dict{UInt64, Vector{Base.StackTraces.StackFrame}}, Bool, Symbol, Int, UInt}) + precompile(Tuple{typeof(Profile.tree!), Profile.StackFrameTree{UInt64}, Vector{UInt64}, Dict{UInt64, Vector{Base.StackTraces.StackFrame}}, Bool, Symbol, Int, UnitRange{UInt}}) + precompile(Tuple{typeof(Profile.tree!), Profile.StackFrameTree{UInt64}, Vector{UInt64}, Dict{UInt64, Vector{Base.StackTraces.StackFrame}}, Bool, Symbol, UnitRange{Int}, UInt}) + precompile(Tuple{typeof(Profile.tree!), Profile.StackFrameTree{UInt64}, Vector{UInt64}, Dict{UInt64, Vector{Base.StackTraces.StackFrame}}, Bool, Symbol, UnitRange{Int}, UnitRange{UInt}}) + precompile(Tuple{typeof(Profile.tree!), Profile.StackFrameTree{UInt64}, Vector{UInt64}, Dict{UInt64, Vector{Base.StackTraces.StackFrame}}, Bool, Symbol, Vector{Int}, Vector{UInt}}) + """ +end + +const JULIA_PROMPT = "julia> " +const PKG_PROMPT = "pkg> " +const SHELL_PROMPT = "shell> " +const HELP_PROMPT = "help?> " + function generate_precompile_statements() start_time = time_ns() debug_output = devnull # or stdout + sysimg = Base.unsafe_string(Base.JLOptions().image_file) - # Precompile a package + # Extract the precompile statements from the precompile file + statements = Set{String}() + + # From hardcoded statements + for statement in split(hardcoded_precompile_statements::String, '\n') + push!(statements, statement) + end + + # Collect statements from running the script mktempdir() do prec_path - push!(DEPOT_PATH, prec_path) - push!(LOAD_PATH, prec_path) + # Also precompile a package here pkgname = "__PackagePrecompilationStatementModule" mkpath(joinpath(prec_path, pkgname, "src")) - write(joinpath(prec_path, pkgname, "src", "$pkgname.jl"), + path = joinpath(prec_path, pkgname, "src", "$pkgname.jl") + write(path, """ module $pkgname end """) - @eval using __PackagePrecompilationStatementModule - empty!(LOAD_PATH) - empty!(DEPOT_PATH) + tmp_prec = tempname(prec_path) + tmp_proc = tempname(prec_path) + s = """ + pushfirst!(DEPOT_PATH, $(repr(prec_path))); + Base.PRECOMPILE_TRACE_COMPILE[] = $(repr(tmp_prec)); + Base.compilecache(Base.PkgId($(repr(pkgname))), $(repr(path))) + $precompile_script + """ + run(`$(julia_exepath()) -O0 --sysimage $sysimg --trace-compile=$tmp_proc --startup-file=no -Cnative -e $s`) + for f in (tmp_prec, tmp_proc) + for statement in split(read(f, String), '\n') + occursin("Main.", statement) && continue + push!(statements, statement) + end + end end mktemp() do precompile_file, precompile_file_h - # Run a repl process and replay our script + # Collect statements from running a REPL process and replaying our REPL script pts, ptm = open_fake_pty() blackhole = Sys.isunix() ? "/dev/null" : "nul" if have_repl @@ -152,12 +295,10 @@ function generate_precompile_statements() p = withenv("JULIA_HISTORY" => blackhole, "JULIA_PROJECT" => nothing, # remove from environment "JULIA_LOAD_PATH" => Sys.iswindows() ? "@;@stdlib" : "@:@stdlib", + "JULIA_PKG_PRECOMPILE_AUTO" => "0", "TERM" => "") do - sysimg = Base.unsafe_string(Base.JLOptions().image_file) run(```$(julia_exepath()) -O0 --trace-compile=$precompile_file --sysimage $sysimg - --cpu-target=native --startup-file=no --color=yes - -e 'import REPL; REPL.Terminals.is_precompiling[] = true' - -i $cmdargs```, + --cpu-target=native --startup-file=no -i $cmdargs```, pts, pts, pts; wait=false) end Base.close_stdio(pts) @@ -170,27 +311,26 @@ function generate_precompile_statements() Sys.iswindows() && (sleep(0.1); yield(); yield()) # workaround hang - probably a libuv issue? write(output_copy, l) end - close(output_copy) - close(ptm) catch ex - close(output_copy) - close(ptm) if !(ex isa Base.IOError && ex.code == Base.UV_EIO) rethrow() # ignore EIO on ptm after pts dies end + finally + close(output_copy) + close(ptm) end # wait for the definitive prompt before start writing to the TTY - readuntil(output_copy, "julia>") + readuntil(output_copy, JULIA_PROMPT) sleep(0.1) readavailable(output_copy) # Input our script if have_repl - precompile_lines = split(precompile_script, '\n'; keepempty=false) + precompile_lines = split(repl_script::String, '\n'; keepempty=false) curr = 0 for l in precompile_lines sleep(0.1) curr += 1 - print("\rGenerating precompile statements... $curr/$(length(precompile_lines))") + print("\rGenerating REPL precompile statements... $curr/$(length(precompile_lines))") # consume any other output bytesavailable(output_copy) > 0 && readavailable(output_copy) # push our input @@ -198,9 +338,16 @@ function generate_precompile_statements() write(ptm, l, "\n") readuntil(output_copy, "\n") # wait for the next prompt-like to appear - # NOTE: this is rather inaccurate because the Pkg REPL mode is a special flower readuntil(output_copy, "\n") - readuntil(output_copy, "> ") + strbuf = "" + while true + strbuf *= String(readavailable(output_copy)) + occursin(JULIA_PROMPT, strbuf) && break + occursin(PKG_PROMPT, strbuf) && break + occursin(SHELL_PROMPT, strbuf) && break + occursin(HELP_PROMPT, strbuf) && break + sleep(0.1) + end end println() end @@ -210,54 +357,77 @@ function generate_precompile_statements() close(ptm) write(debug_output, "\n#### FINISHED ####\n") - # Extract the precompile statements from the precompile file - statements = Set{String}() - for statement in eachline(precompile_file_h) + for statement in split(read(precompile_file, String), '\n') # Main should be completely clean occursin("Main.", statement) && continue push!(statements, statement) end + end - for statement in split(hardcoded_precompile_statements, '\n') - push!(statements, statement) - end - - # Create a staging area where all the loaded packages are available - PrecompileStagingArea = Module() - for (_pkgid, _mod) in Base.loaded_modules - if !(_pkgid.name in ("Main", "Core", "Base")) - eval(PrecompileStagingArea, :(const $(Symbol(_mod)) = $_mod)) - end + # Create a staging area where all the loaded packages are available + PrecompileStagingArea = Module() + for (_pkgid, _mod) in Base.loaded_modules + if !(_pkgid.name in ("Main", "Core", "Base")) + eval(PrecompileStagingArea, :(const $(Symbol(_mod)) = $_mod)) end + end - # Execute the collected precompile statements - n_succeeded = 0 - include_time = @elapsed for statement in sort(collect(statements)) - # println(statement) - try - Base.include_string(PrecompileStagingArea, statement) - n_succeeded += 1 - print("\rExecuting precompile statements... $n_succeeded/$(length(statements))") - catch - # See #28808 - # @error "Failed to precompile $statement" + # Execute the collected precompile statements + n_succeeded = 0 + include_time = @elapsed for statement in sort!(collect(statements)) + # println(statement) + # XXX: skip some that are broken. these are caused by issue #39902 + occursin("Tuple{Artifacts.var\"#@artifact_str\", LineNumberNode, Module, Any, Any}", statement) && continue + occursin("Tuple{Base.Cartesian.var\"#@ncall\", LineNumberNode, Module, Int64, Any, Vararg{Any}}", statement) && continue + occursin("Tuple{Base.Cartesian.var\"#@ncall\", LineNumberNode, Module, Int32, Any, Vararg{Any}}", statement) && continue + occursin("Tuple{Base.Cartesian.var\"#@nloops\", LineNumberNode, Module, Any, Any, Any, Vararg{Any}}", statement) && continue + occursin("Tuple{Core.var\"#@doc\", LineNumberNode, Module, Vararg{Any}}", statement) && continue + # XXX: this is strange, as this isn't the correct representation of this + occursin("typeof(Core.IntrinsicFunction)", statement) && continue + # XXX: this is strange, as this method should not be getting compiled + occursin(", Core.Compiler.AbstractInterpreter, ", statement) && continue + try + ps = Meta.parse(statement) + isexpr(ps, :call) || continue + popfirst!(ps.args) # precompile(...) + ps.head = :tuple + l = ps.args[end] + if (isexpr(l, :tuple) || isexpr(l, :curly)) && length(l.args) > 0 # Tuple{...} or (...) + # XXX: precompile doesn't currently handle overloaded Vararg arguments very well. + # Replacing N with a large number works around it. + l = l.args[end] + if isexpr(l, :curly) && length(l.args) == 2 && l.args[1] === :Vararg # Vararg{T} + push!(l.args, 100) # form Vararg{T, 100} instead + end end + # println(ps) + ps = Core.eval(PrecompileStagingArea, ps) + # XXX: precompile doesn't currently handle overloaded nospecialize arguments very well. + # Skipping them avoids the warning. + ms = length(ps) == 1 ? Base._methods_by_ftype(ps[1], 1, Base.get_world_counter()) : Base.methods(ps...) + ms isa Vector || continue + precompile(ps...) + n_succeeded += 1 + print("\rExecuting precompile statements... $n_succeeded/$(length(statements))") + catch ex + # See #28808 + @warn "Failed to precompile expression" form=statement exception=ex _module=nothing _file=nothing _line=0 end - println() - if have_repl - # Seems like a reasonable number right now, adjust as needed - # comment out if debugging script - @assert n_succeeded > 1200 - end - - tot_time = time_ns() - start_time - include_time *= 1e9 - gen_time = tot_time - include_time - println("Precompilation complete. Summary:") - print("Total ─────── "); Base.time_print(tot_time); println() - print("Generation ── "); Base.time_print(gen_time); print(" "); show(IOContext(stdout, :compact=>true), gen_time / tot_time * 100); println("%") - print("Execution ─── "); Base.time_print(include_time); print(" "); show(IOContext(stdout, :compact=>true), include_time / tot_time * 100); println("%") end + println() + if have_repl + # Seems like a reasonable number right now, adjust as needed + # comment out if debugging script + n_succeeded > 1200 || @warn "Only $n_succeeded precompile statements" + end + + tot_time = time_ns() - start_time + include_time *= 1e9 + gen_time = tot_time - include_time + println("Precompilation complete. Summary:") + print("Total ─────── "); Base.time_print(tot_time); println() + print("Generation ── "); Base.time_print(gen_time); print(" "); show(IOContext(stdout, :compact=>true), gen_time / tot_time * 100); println("%") + print("Execution ─── "); Base.time_print(include_time); print(" "); show(IOContext(stdout, :compact=>true), include_time / tot_time * 100); println("%") return end @@ -266,6 +436,7 @@ generate_precompile_statements() # As a last step in system image generation, # remove some references to build time environment for a more reproducible build. +Base.Filesystem.temp_cleanup_purge(force=true) @eval Base PROGRAM_FILE = "" @eval Sys begin BINDIR = "" diff --git a/contrib/julia-config.jl b/contrib/julia-config.jl index d69e09aba05810..9c6e39216d8173 100755 --- a/contrib/julia-config.jl +++ b/contrib/julia-config.jl @@ -11,8 +11,6 @@ const options = [ "--framework" ]; -threadingOn() = ccall(:jl_threading_enabled, Cint, ()) != 0 - function shell_escape(str) str = replace(str, "'" => "'\''") return "'$str'" @@ -79,7 +77,7 @@ end function cflags(doframework) flags = IOBuffer() - print(flags, "-std=gnu99") + print(flags, "-std=gnu11") if doframework include = shell_escape(frameworkDir()) print(flags, " -F", include) diff --git a/contrib/mac/app/Entitlements.plist b/contrib/mac/app/Entitlements.plist index b84dccb00f95cb..95c1a02d589585 100644 --- a/contrib/mac/app/Entitlements.plist +++ b/contrib/mac/app/Entitlements.plist @@ -4,7 +4,7 @@ com.apple.security.automation.apple-events - com.apple.security.cs.get-task-allow + com.apple.security.get-task-allow com.apple.security.cs.allow-dyld-environment-variables diff --git a/contrib/mac/app/Makefile b/contrib/mac/app/Makefile index 665b4f1566b6f4..81b7e47cdf2cfa 100644 --- a/contrib/mac/app/Makefile +++ b/contrib/mac/app/Makefile @@ -49,9 +49,13 @@ dmg/$(APP_NAME): startup.applescript julia.icns -mkdir -p $@/Contents/Resources/julia make -C $(JULIAHOME) binary-dist tar zxf $(JULIAHOME)/$(JULIA_BINARYDIST_FILENAME).tar.gz -C $@/Contents/Resources/julia --strip-components 1 + find $@/Contents/Resources/julia -type f -exec chmod -w {} \; + # Even though the tarball may already be signed, we re-sign here to make it easier to add + # unsigned executables (like the app launcher) and whatnot, without needing to maintain lists + # of what is or is not signed. Codesigning is cheap, so might as well do it early and often. if [ -n "$$MACOS_CODESIGN_IDENTITY" ]; then \ echo "Codesigning with identity $$MACOS_CODESIGN_IDENTITY"; \ - MACHO_FILES=$$(find "$@" -type f -perm -755 | cut -d: -f1); \ + MACHO_FILES=$$(find "$@" -type f -perm -0111 | cut -d: -f1); \ for f in $${MACHO_FILES}; do \ echo "Codesigning $${f}..."; \ codesign -s "$$MACOS_CODESIGN_IDENTITY" --option=runtime --entitlements Entitlements.plist -vvv --timestamp --deep --force "$${f}"; \ diff --git a/contrib/mac/app/notarize_check.sh b/contrib/mac/app/notarize_check.sh index ccb46844abec3c..3cf347e8e84fc9 100755 --- a/contrib/mac/app/notarize_check.sh +++ b/contrib/mac/app/notarize_check.sh @@ -1,4 +1,5 @@ #!/bin/bash +# This file is a part of Julia. License is MIT: https://julialang.org/license # Note that you need to have exported `APPLEID` and `APPLEID_PASSWORD` for this to work. diff --git a/contrib/mac/app/renotarize_dmg.sh b/contrib/mac/app/renotarize_dmg.sh index 82d7f7872e6708..f0d6d0a197e5f5 100755 --- a/contrib/mac/app/renotarize_dmg.sh +++ b/contrib/mac/app/renotarize_dmg.sh @@ -1,4 +1,5 @@ #!/bin/bash +# This file is a part of Julia. License is MIT: https://julialang.org/license # We need a URL if [[ -z "$1" ]]; then @@ -12,28 +13,28 @@ if [[ -z "${APPLEID}" ]] || [[ -z "${APPLEID_PASSWORD}" ]]; then exit 1 fi -# Translate from `s3://` URL to `https://` url: +# Use `aws` to download an `s3://` URL, otherwise use `curl` URL="$1" if [[ "$URL" == s3://* ]]; then - # Chop off `s3://` - URL="${URL:5}" - # Split into bucket.s3.aws.com/path - URL="https://${URL%%/*}.s3.amazonaws.com/${URL#*/}" + aws s3 cp "${URL}" . +elif [[ "${URL}" == http* ]]; then + # Download .dmg + curl -L "${URL}" -O +else + echo "Unknown URL format: '${URL}'" >&2 + exit 1 fi -# Download .dmg -curl -L "${URL}" -O - # Unpack dmg into our `dmg` folder rm -rf dmg +DMG_NAME=$(basename "${URL}") # Copy app over to our `dmg` folder for j in /Volumes/Julia-*; do hdiutil detach "${j}"; done -hdiutil mount "$(basename "$1")" +hdiutil mount "${DMG_NAME}" cp -Ra /Volumes/Julia-* dmg -# Override some important Makefile variables -DMG_NAME=$(basename "$1") +# Autodetect APP_NAME and VOL_NAME APP_NAME=$(basename dmg/*.app) VOL_NAME=$(basename /Volumes/Julia-*) @@ -46,3 +47,8 @@ for j in /Volumes/Julia-*; do hdiutil detach "${j}"; done # Run notarization make notarize "DMG_NAME=${DMG_NAME}" "APP_NAME=${APP_NAME}" "VOL_NAME=${VOL_NAME}" + +# If it was an s3 bucket, auto-upload it +if [[ "${URL}" == s3://* ]]; then + aws s3 cp --acl public-read "${DMG_NAME}" "${URL}" +fi diff --git a/contrib/mac/app/startup.applescript b/contrib/mac/app/startup.applescript index f02830a3902dce..9964049f34ed6c 100644 --- a/contrib/mac/app/startup.applescript +++ b/contrib/mac/app/startup.applescript @@ -1,5 +1,4 @@ -set RootPath to POSIX path of (path to me) -tell application id "com.apple.terminal" - do script ("exec '" & RootPath & "Contents/Resources/julia/bin/julia'") - activate -end tell +set RootPath to (path to me) +set JuliaPath to POSIX path of ((RootPath as text) & "Contents:Resources:julia:bin:julia") +set JuliaFile to POSIX file JuliaPath +tell application id "com.apple.finder" to open JuliaFile diff --git a/contrib/mac/framework/Makefile b/contrib/mac/framework/Makefile index 4449436ac6fd2e..1f8a55b26188ad 100644 --- a/contrib/mac/framework/Makefile +++ b/contrib/mac/framework/Makefile @@ -143,7 +143,7 @@ endif # cleanup unnecessary install outputs rm $(DESTDIR)$(datarootdir)/julia/startup.jl rm -rf $(DESTDIR)$(datarootdir)/icons $(DESTDIR)$(datarootdir)/applications $(DESTDIR)$(datarootdir)/appdata - find $(DESTDIR)$(prefix)/$(framework_directory) \( -name '.DS_Store' -o -name '.gitignore' -o -name Makefile -o -name .travis.yml -o -name .codecov.yml \) -delete + find $(DESTDIR)$(prefix)/$(framework_directory) \( -name '.DS_Store' -o -name '.gitignore' -o -name Makefile -o -name .codecov.yml \) -delete # Include Julia's license info $(INSTALL_F) $(JULIAHOME)/LICENSE.md $(DESTDIR)$(prefix)/$(framework_resources) @@ -159,7 +159,7 @@ endif #NB: must be the last lines of the recipe, else signature may be invalidated. # Codesign should look at the embedded Info.plist to get the signing identifier. - # See JLDFLAGS in Make.inc for Darwin platform and Info.plist target in ui/Makefile. + # See JLDFLAGS in Make.inc for Darwin platform and Info.plist target in cli/Makefile. codesign -s "$(DARWIN_CODESIGN_KEYCHAIN_IDENTITY)" -v $(darwin_codesign_options) $(darwin_codesign_julia_options) $(DESTDIR)$(prefix)/$(framework_helpers)/julia ifeq ($(BUNDLE_DEBUG_LIBS),1) codesign -s "$(DARWIN_CODESIGN_KEYCHAIN_IDENTITY)" -v $(darwin_codesign_options) $(darwin_codesign_julia_options) $(DESTDIR)$(prefix)/$(framework_helpers)/julia-debug diff --git a/contrib/mac/frameworkapp/JuliaLauncher/Assets.xcassets/AppIcon.appiconset/Contents.json b/contrib/mac/frameworkapp/JuliaLauncher/Assets.xcassets/AppIcon.appiconset/Contents.json index 2fe2dbc16b987d..5071eb935ab9b2 100644 --- a/contrib/mac/frameworkapp/JuliaLauncher/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/contrib/mac/frameworkapp/JuliaLauncher/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -65,4 +65,4 @@ "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/contrib/mac/frameworkapp/JuliaLauncher/Assets.xcassets/Contents.json b/contrib/mac/frameworkapp/JuliaLauncher/Assets.xcassets/Contents.json index da4a164c918651..2d92bd53fdb222 100644 --- a/contrib/mac/frameworkapp/JuliaLauncher/Assets.xcassets/Contents.json +++ b/contrib/mac/frameworkapp/JuliaLauncher/Assets.xcassets/Contents.json @@ -3,4 +3,4 @@ "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/contrib/mac/frameworkapp/Makefile b/contrib/mac/frameworkapp/Makefile index 93392cd4ec3d02..c94a5be145db9a 100644 --- a/contrib/mac/frameworkapp/Makefile +++ b/contrib/mac/frameworkapp/Makefile @@ -76,7 +76,7 @@ $(BUILDROOT)/framework-component.plist: $(JULIAHOME)/contrib/mac/frameworkapp/fr # important properties. Together, the properties allow one "Julia.framework" # to exist at a location with multiple versions of Julia within. # -# 1. The component's identifer is versioned to match the bundled framework. +# 1. The component's identifier is versioned to match the bundled framework. # This allows multiple versions of the component to be installed with the # Julia.framework/Versions directory. # 2. The component-plist identifies the Versions/x.y directory as an upgradable @@ -116,8 +116,8 @@ signedproductarchive: $(PRODUCTARCHIVE) mv $<.signed $< clean: - -rm -rf $(XCARCHIVE) $(XCDERIVEDDATA) $(XCEXPORT) - -rm -rf $(FRAMEWORK_DESTDIR) + rm -rf $(XCARCHIVE) $(XCDERIVEDDATA) $(XCEXPORT) + rm -rf $(FRAMEWORK_DESTDIR) -rm -f $(PRODUCTARCHIVE) .PHONY: appexport clean productarchive signedproductarchive diff --git a/contrib/mac/frameworkapp/README.md b/contrib/mac/frameworkapp/README.md index 43cee0ee3716a6..94c344d16564fc 100644 --- a/contrib/mac/frameworkapp/README.md +++ b/contrib/mac/frameworkapp/README.md @@ -12,7 +12,7 @@ the top of the `Makefile` to set appropriate code signing parameters. The framework is installed in `/Library/Frameworks` and the app in `/Applications`. Installation may be system-wide (i.e., relative to `/`) or -local to the user's home directory (i.e., `$Home/Appliations/Julia.app`). +local to the user's home directory (i.e., `$Home/Applications/Julia.app`). The `julia` binary is embedded in the framework at `Julia.framework/Helpers/julia`. diff --git a/contrib/mac/frameworkapp/installresources/conclusion.rtf b/contrib/mac/frameworkapp/installresources/conclusion.rtf index 8d794ae31c04b0..1f3e60f5f52773 100644 --- a/contrib/mac/frameworkapp/installresources/conclusion.rtf +++ b/contrib/mac/frameworkapp/installresources/conclusion.rtf @@ -77,4 +77,4 @@ Conclusion\ \f1 \cb1 \ \pard\pardeftab720\partightenfactor0 -\f2 \cf0 \cb2 ln -s INSTALL_LOCATION/Julia.framework/Helpers/julia DIR_IN_PATH/julia} \ No newline at end of file +\f2 \cf0 \cb2 ln -s INSTALL_LOCATION/Julia.framework/Helpers/julia DIR_IN_PATH/julia} diff --git a/contrib/mac/frameworkapp/installresources/readme.rtf b/contrib/mac/frameworkapp/installresources/readme.rtf index d555047dd5c1c2..935d9a5f6a5760 100644 --- a/contrib/mac/frameworkapp/installresources/readme.rtf +++ b/contrib/mac/frameworkapp/installresources/readme.rtf @@ -28,4 +28,4 @@ Readme\ \f2 \cb2 $HOME \f1 \cb1 usually expands to \f2 \cb2 /Users/username -\f1 \cb1 ).} \ No newline at end of file +\f1 \cb1 ).} diff --git a/contrib/new-stdlib.sh b/contrib/new-stdlib.sh new file mode 100755 index 00000000000000..15f82cffb1c46b --- /dev/null +++ b/contrib/new-stdlib.sh @@ -0,0 +1,74 @@ +#!/bin/sh +# This file is a part of Julia. License is MIT: https://julialang.org/license + +set -eu # stop on failure + +printf -- "Julia Stdlib Creator Helper Wizard\n" +printf -- "----------------------------------\n" + +ROOT=$(dirname "$0")/../stdlib +read -p "Name: " NAME +read -p "Github User account (empty for local): " USER + +if [ -z "$USER" ]; then + +UUID=$(uuidgen | tr [A-Z] [a-z]) + +sed -e "/^STDLIBS =/,/^\$/s!^\$!\\ +STDLIBS += $NAME\\ +!" "$ROOT/Makefile" >"$ROOT/Makefile.tmp" +mv "$ROOT/Makefile.tmp" "$ROOT/Makefile" + +mkdir "$ROOT/$NAME" +mkdir "$ROOT/$NAME/src" +mkdir "$ROOT/$NAME/test" + +cat >"$ROOT/$NAME/Project.toml" <"$ROOT/$NAME/src/$NAME.jl" <"$ROOT/$NAME/test/runtests.jl" <"$ROOT/Makefile.tmp" +mv "$ROOT/Makefile.tmp" "$ROOT/Makefile" + +cat >"$ROOT/$NAME.version" <= 3: - libgfortran_version = { - "4": "libgfortran3", - "5": "libgfortran3", - "6": "libgfortran3", - "7": "libgfortran4", - "8": "libgfortran5", - "9": "libgfortran5", - "10": "libgfortran5", - "11": "libgfortran5", - }[list(filter(lambda x: re.match("\d+\.\d+(\.\d+)?", x), sys.argv[2].split()))[-1].split('.')[0]] + # If there was no gfortran/gcc version passed in, default to the latest libgfortran version + if not sys.argv[2]: + libgfortran_version = "libgfortran5" + else: + # Take the last thing that looks like a version number, and extract its major component + version_numbers = list(filter(lambda x: re.match("\d+\.\d+(\.\d+)?", x), sys.argv[2].split())) + major_ver = int(version_numbers[-1].split('.')[0]) + if major_ver <= 6: + libgfortran_version = "libgfortran3" + elif major_ver <= 7: + libgfortran_version = "libgfortran4" + else: + libgfortran_version = "libgfortran5" if cxx_abi == "blank_cxx_abi": if len(sys.argv) == 4: diff --git a/contrib/print_sorted_stdlibs.jl b/contrib/print_sorted_stdlibs.jl new file mode 100644 index 00000000000000..bbf890328cb4ef --- /dev/null +++ b/contrib/print_sorted_stdlibs.jl @@ -0,0 +1,99 @@ +#!/usr/bin/env julia +# This file is a part of Julia. License is MIT: https://julialang.org/license + +using TOML + +function check_flag(flag) + idxs = findall(flag .== ARGS) + for idx in reverse(idxs) + popat!(ARGS, idx) + end + return !isempty(idxs) +end + +if check_flag("--help") || check_flag("-h") + println("Usage: julia print_sorted_stdlibs.jl [stdlib_dir] [--exclude-jlls]") +end + +# Allow users to ask for JLL or no JLLs +exclude_jlls = check_flag("--exclude-jlls") + +# Default to the `stdlib/vX.Y` directory +STDLIB_DIR = get(ARGS, 1, joinpath(@__DIR__, "..", "usr", "share", "julia", "stdlib")) +vXYdirs = readdir(STDLIB_DIR) +if length(vXYdirs) == 1 && match(r"v\d\.\d", vXYdirs[1]) !== nothing + STDLIB_DIR = joinpath(STDLIB_DIR, vXYdirs[1]) +end + +project_deps = Dict{String,Set{String}}() +for project_dir in readdir(STDLIB_DIR, join=true) + files = readdir(project_dir) + if "Project.toml" in files + project = TOML.parsefile(joinpath(project_dir, "Project.toml")) + + if !haskey(project, "name") + continue + end + name = project["name"] + deps = Set(collect(keys(get(project, "deps", Dict{String,String}())))) + project_deps[name] = deps + end +end + +#println("Found $(length(keys(project_deps))) stdlib projects") + +function project_depth(project) + deps = project_deps[project] + if isempty(deps) + return 0 + end + + depth = 1 + while !all(isempty(project_deps[d]) for d in deps) + depth += 1 + + if depth > 100 + error("Failed to converge while finding project depth for $(project)!") + end + + new_deps = Set{String}() + for d in deps + union!(new_deps, project_deps[d]) + end + deps = new_deps + end + return depth +end + +project_depths = Dict(p => project_depth(p) for p in keys(project_deps)) + +function project_isless(p1, p2) + if project_depths[p1] != project_depths[p2] + return isless(project_depths[p1], project_depths[p2]) + end + return isless(p1, p2) +end + +sorted_projects = sort(collect(keys(project_depths)), lt=project_isless) + +if exclude_jlls + filter!(p -> !endswith(p, "_jll"), sorted_projects) +end + +# Print out sorted projects, ready to be pasted into `sysimg.jl` +last_depth = 0 +println(" # Stdlibs sorted in dependency, then alphabetical, order by contrib/print_sorted_stdlibs.jl") +if exclude_jlls + println(" # Run with the `--exclude-jlls` option to filter out all JLL packages") +end +println(" stdlibs = [") +println(" # No dependencies") +for p in sorted_projects + if project_depths[p] != last_depth + global last_depth = project_depths[p] + println() + println(" # $(last_depth)-depth packages") + end + println(" :$(p),") +end +println(" ]") diff --git a/contrib/refresh_bb_tarballs.sh b/contrib/refresh_bb_tarballs.sh deleted file mode 100755 index e3c44b954d0b8b..00000000000000 --- a/contrib/refresh_bb_tarballs.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/sh -# This file is a part of Julia. License is MIT: https://julialang.org/license - -# Invoke this with no arguments to refresh all tarballs, or with a project name to refresh only that project. -# -# Example: -# ./refresh_bb_tarballs.sh gmp - -# Get this list via: -# using BinaryBuilder -# print("TRIPLETS=\"$(join(triplet.(BinaryBuilder.supported_platforms()), " "))\"") -TRIPLETS="i686-linux-gnu x86_64-linux-gnu aarch64-linux-gnu armv7l-linux-gnueabihf powerpc64le-linux-gnu i686-linux-musl x86_64-linux-musl aarch64-linux-musl armv7l-linux-musleabihf x86_64-apple-darwin14 x86_64-unknown-freebsd11.1 i686-w64-mingw32 x86_64-w64-mingw32" - -# These are the projects currently using BinaryBuilder; both GCC-expanded and non-GCC-expanded: -BB_PROJECTS="mbedtls libssh2 nghttp2 mpfr curl libgit2 pcre libuv unwind osxunwind dsfmt objconv p7zip zlib suitesparse openlibm" -BB_GCC_EXPANDED_PROJECTS="openblas" -BB_CXX_EXPANDED_PROJECTS="gmp llvm" - -# If we've been given a project name, filter down to that one: -if [ -n "${1}" ]; then - case "${BB_PROJECTS}" in - *${1}*) BB_PROJECTS="${1}" ;; - *) BB_PROJECTS="" ;; - esac - case "${BB_GCC_EXPANDED_PROJECTS}" in - *${1}*) BB_GCC_EXPANDED_PROJECTS="${1}" ;; - *) BB_GCC_EXPANDED_PROJECTS="" ;; - esac - case "${BB_CXX_EXPANDED_PROJECTS}" in - *${1}*) BB_CXX_EXPANDED_PROJECTS="${1}" ;; - *) BB_CXX_EXPANDED_PROJECTS="" ;; - esac -fi - -# Get "contrib/" directory path -CONTRIB_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) - -# Get the source hash for each project -for proj in ${BB_PROJECTS}; do - PROJ="$(echo ${proj} | tr [a-z] [A-Z])" - make -C "${CONTRIB_DIR}/../deps" USE_BINARYBUILDER_${PROJ}=0 DEPS_GIT=0 extract-${proj} -done - -# For each triplet and each project, download the BB tarball and save its hash: -for triplet in ${TRIPLETS}; do - for proj in ${BB_PROJECTS}; do - PROJ="$(echo ${proj} | tr [a-z] [A-Z])" - make -C "${CONTRIB_DIR}/../deps" USE_BINARYBUILDER_${PROJ}=1 ${PROJ}_BB_TRIPLET=${triplet} distclean-${proj} - make -C "${CONTRIB_DIR}/../deps" USE_BINARYBUILDER_${PROJ}=1 ${PROJ}_BB_TRIPLET=${triplet} install-${proj} - done - - for proj in ${BB_GCC_EXPANDED_PROJECTS}; do - PROJ="$(echo ${proj} | tr [a-z] [A-Z])" - for libgfortran in libgfortran3 libgfortran4 libgfortran5; do - make -C "${CONTRIB_DIR}/../deps" USE_BINARYBUILDER_${PROJ}=1 ${PROJ}_BB_TRIPLET=${triplet}-${libgfortran} BB_TRIPLET_CXXABI=${triplet} distclean-${proj} - make -C "${CONTRIB_DIR}/../deps" USE_BINARYBUILDER_${PROJ}=1 ${PROJ}_BB_TRIPLET=${triplet}-${libgfortran} BB_TRIPLET_CXXABI=${triplet} install-${proj} - done - done - - for proj in ${BB_CXX_EXPANDED_PROJECTS}; do - PROJ="$(echo ${proj} | tr [a-z] [A-Z])" - for cxx in cxx03 cxx11; do - make -C "${CONTRIB_DIR}/../deps" USE_BINARYBUILDER_${PROJ}=1 ${PROJ}_BB_TRIPLET=${triplet}-${cxx} BB_TRIPLET_CXXABI=${triplet} distclean-${proj} - make -C "${CONTRIB_DIR}/../deps" USE_BINARYBUILDER_${PROJ}=1 ${PROJ}_BB_TRIPLET=${triplet}-${cxx} BB_TRIPLET_CXXABI=${triplet} install-${proj} - done - done -done diff --git a/contrib/refresh_checksums.mk b/contrib/refresh_checksums.mk new file mode 100644 index 00000000000000..898bd5841ee82c --- /dev/null +++ b/contrib/refresh_checksums.mk @@ -0,0 +1,142 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +# Invoke this with no arguments to refresh all tarballs, or with a project name to refresh only that project. +# +# Example: +# make -f contrib/refresh_checksums.mk gmp + +SRCDIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) +JULIAHOME := $(abspath $(SRCDIR)/..) + +# force a sane / stable configuration +export LC_ALL=C +export LANG=C +.SUFFIXES: + +# Default target that will have everything else added to it as a dependency +all: checksum pack-checksum + +# Get this list via: +# using BinaryBuilder +# print("TRIPLETS=\"$(join(sort(triplet.(BinaryBuilder.supported_platforms(;experimental=true))), " "))\"") +TRIPLETS=aarch64-apple-darwin aarch64-linux-gnu aarch64-linux-musl armv6l-linux-gnueabihf armv6l-linux-musleabihf armv7l-linux-gnueabihf armv7l-linux-musleabihf i686-linux-gnu i686-linux-musl i686-w64-mingw32 powerpc64le-linux-gnu x86_64-apple-darwin x86_64-linux-gnu x86_64-linux-musl x86_64-unknown-freebsd x86_64-w64-mingw32 +CLANG_TRIPLETS=$(filter %-darwin %-freebsd,$(TRIPLETS)) +NON_CLANG_TRIPLETS=$(filter-out %-darwin %-freebsd,$(TRIPLETS)) + +# These are the projects currently using BinaryBuilder; both GCC-expanded and non-GCC-expanded: +BB_PROJECTS=mbedtls libssh2 nghttp2 mpfr curl libgit2 pcre libuv unwind llvmunwind dsfmt objconv p7zip zlib libsuitesparse openlibm blastrampoline +BB_GCC_EXPANDED_PROJECTS=openblas csl +BB_CXX_EXPANDED_PROJECTS=gmp llvm clang llvm-tools +# These are non-BB source-only deps +NON_BB_PROJECTS=patchelf mozillacert lapack libwhich utf8proc + +ifneq ($(VERBOSE),1) +QUIET_MAKE := -s +else +QUIET_MAKE := +endif + +# Convert `llvm-tools` to `LLVM_TOOLS` +define makevar +$(shell echo $(1) | tr 'a-z-' 'A-Z_') +endef + +# If $(2) == `src`, this will generate a `USE_BINARYBUILDER_FOO=0` make flag +# It will also generate a `FOO_BB_TRIPLET=$(2)` make flag. +define make_flags +USE_BINARYBUILDER=$(if $(filter src,$(2)),0,1) $(if $(filter src,$(2)),FC_VERSION=7.0.0,) $(call makevar,$(1))_BB_TRIPLET=$(if $(filter src,$(2)),,$(2)) LLVM_ASSERTIONS=$(if $(filter assert,$(3)),1,0) DEPS_GIT=0 +endef + +# checksum_bb_dep takes in (name, triplet), and generates a `checksum-$(1)-$(2)` target. +# note that `"src"` is a special triplet value. +# if $(3) is "assert", we set BINARYBUILDER_LLVM_ASSERTS=1 +define checksum_dep +checksum-$(1)-$(2)-$(3): clean-$(1) + -+$(MAKE) $(QUIET_MAKE) -C "$(JULIAHOME)/deps" $(call make_flags,$(1),$(2),$(3)) checksum-$(1) +.PHONY: checksum-$(1)-$(2)-$(3) + +# Add this guy to his project target +checksum-$(1): checksum-$(1)-$(2)-$(3) + +# Add a dependency to the pack target +# TODO: can we make this so it only adds an ordering but not a dependency? +pack-checksum-$(1): | checksum-$(1) + +# Add this guy to the `checksum` and `pack-checksum` default targets (e.g. `make -f contrib/refresh_checksums.mk openblas`) +checksum: checksum-$1 +$1 pack-checksum: pack-checksum-$1 +endef + +# Generate targets for source hashes for all our projects +$(foreach project,$(BB_PROJECTS) $(BB_GCC_EXPANDED_PROJECTS) $(BB_CXX_EXPANDED_PROJECTS) $(NON_BB_PROJECTS),$(eval $(call checksum_dep,$(project),src))) + +# Generate targets for triplet-specific hashes for all our BB projects +$(foreach project,$(BB_PROJECTS),$(foreach triplet,$(TRIPLETS),$(eval $(call checksum_dep,$(project),$(triplet))))) +$(foreach project,$(BB_GCC_EXPANDED_PROJECTS),$(foreach triplet,$(TRIPLETS),$(foreach libgfortran_version,libgfortran3 libgfortran4 libgfortran5,$(eval $(call checksum_dep,$(project),$(triplet)-$(libgfortran_version)))))) + +# Because MacOS and FreeBSD use clang, they don't actually use cxxstring_abi expansion: +$(foreach project,$(BB_CXX_EXPANDED_PROJECTS),$(foreach triplet,$(NON_CLANG_TRIPLETS),$(foreach cxxstring_abi,cxx11 cxx03,$(eval $(call checksum_dep,$(project),$(triplet)-$(cxxstring_abi)))))) +$(foreach project,$(BB_CXX_EXPANDED_PROJECTS),$(foreach triplet,$(CLANG_TRIPLETS),$(eval $(call checksum_dep,$(project),$(triplet))))) + +# Special libLLVM_asserts_jll/LLVM_assert_jll targets +$(foreach triplet,$(NON_CLANG_TRIPLETS),$(foreach cxxstring_abi,cxx11 cxx03,$(eval $(call checksum_dep,llvm,$(triplet)-$(cxxstring_abi),assert)))) +$(foreach triplet,$(NON_CLANG_TRIPLETS),$(foreach cxxstring_abi,cxx11 cxx03,$(eval $(call checksum_dep,llvm-tools,$(triplet)-$(cxxstring_abi),assert)))) +$(foreach triplet,$(CLANG_TRIPLETS),$(eval $(call checksum_dep,llvm,$(triplet),assert))) +$(foreach triplet,$(CLANG_TRIPLETS),$(eval $(call checksum_dep,llvm-tools,$(triplet),assert))) + +# External stdlibs +checksum-stdlibs: + -+$(MAKE) $(QUIET_MAKE) -C "$(JULIAHOME)/stdlib" checksumall +all: checksum-stdlibs +.PHONY: checksum-stdlibs + +# doc unicode data +checksum-doc-unicodedata: + -+$(MAKE) $(QUIET_MAKE) -C "$(JULIAHOME)/doc" checksum-unicodedata +all: checksum-doc-unicodedata +.PHONY: checksum-doc-unicodedata + +# merge substring project names to avoid races +pack-checksum-llvm-tools: | pack-checksum-llvm + @# nothing to do but disable the prefix rule +pack-checksum-llvm: | checksum-llvm-tools +pack-checksum-csl: | pack-checksum-compilersupportlibraries + @# nothing to do but disable the prefix rule +pack-checksum-compilersupportlibraries: | checksum-csl +pack-checksum-libsuitesparse: | pack-checksum-suitesparse + @# nothing to do but disable the prefix rule +pack-checksum-suitesparse: | checksum-libsuitesparse +# This is a bit tricky: we want llvmunwind to be separate from unwind and llvm, +# so we add a rule to process those first +pack-checksum-llvm pack-checksum-unwind: | pack-checksum-llvmunwind +# and the name for LLVMLibUnwind is awkward, so handle that with a regex +pack-checksum-llvmunwind: | pack-checksum-llvm.*unwind + cd "$(JULIAHOME)/deps/checksums" && mv 'llvm.*unwind' llvmunwind + +clean-%: FORCE + -rm "$(JULIAHOME)/deps/checksums"/'$*' + +# define how to pack parallel checksums into a single file format +pack-checksum-%: FORCE + @echo making "$(JULIAHOME)/deps/checksums/"'$*' + @cd "$(JULIAHOME)/deps/checksums" && \ + for each in $$(ls | grep -i '$*'); do \ + if [ -d "$$each" ]; then \ + for type in $$(ls "$$each"); do \ + echo "$$each"/"$$type"/$$(cat "$$each"/"$$type"); \ + rm "$$each"/"$$type"; \ + done; \ + rmdir "$$each"; \ + fi; \ + done >> '$*' + @cd "$(JULIAHOME)/deps/checksums" && \ + sort '$*' > '$*.tmp' && \ + mv '$*.tmp' '$*' + +# This file is completely phony +FORCE: +.PHONY: FORCE + +# Debugging helper +print-%: + @echo '$*=$(subst ','\'',$($*))' diff --git a/contrib/relative_path.py b/contrib/relative_path.py index b9d3d1e5bca7ec..9a60607d64d9b3 100755 --- a/contrib/relative_path.py +++ b/contrib/relative_path.py @@ -7,4 +7,4 @@ # shells and whatnot during the build are all POSIX shells/cygwin. We rely on the build # system itself to canonicalize to `\` when it needs to, and deal with the shell escaping # and whatnot at the latest possible moment. -sys.stdout.write(os.path.relpath(sys.argv[2], sys.argv[1]).replace(os.path.sep, '/')) \ No newline at end of file +sys.stdout.write(os.path.relpath(sys.argv[2], sys.argv[1]).replace(os.path.sep, '/')) diff --git a/contrib/travis_fastfail.sh b/contrib/travis_fastfail.sh deleted file mode 100755 index 410cbe2bccafc6..00000000000000 --- a/contrib/travis_fastfail.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -# This file is a part of Julia. License is MIT: https://julialang.org/license - -curlhdr="Accept: application/vnd.travis-ci.2+json" -endpoint="https://api.travis-ci.org/repos/$TRAVIS_REPO_SLUG" - -# Fail fast for superseded builds to PR's -if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - newestbuildforthisPR=$(curl -H "$curlhdr" $endpoint/builds?event_type=pull_request | \ - jq ".builds | map(select(.pull_request_number == $TRAVIS_PULL_REQUEST))[0].number") - if [ $newestbuildforthisPR != null -a $newestbuildforthisPR != \"$TRAVIS_BUILD_NUMBER\" ]; then - echo "There are newer queued builds for this pull request, failing early." - exit 1 - fi -else - # And for non-latest push builds in branches other than master or release* - case $TRAVIS_BRANCH in - master | release*) - ;; - *) - if [ \"$TRAVIS_BUILD_NUMBER\" != $(curl -H "$curlhdr" \ - $endpoint/branches/$TRAVIS_BRANCH | jq ".branch.number") ]; then - echo "There are newer queued builds for this branch, failing early." - exit 1 - fi - ;; - esac -fi diff --git a/contrib/tsan/Make.user.tsan b/contrib/tsan/Make.user.tsan new file mode 100644 index 00000000000000..01c9874a85182b --- /dev/null +++ b/contrib/tsan/Make.user.tsan @@ -0,0 +1,16 @@ +TOOLCHAIN=$(BUILDROOT)/../toolchain +BINDIR=$(TOOLCHAIN)/usr/bin +TOOLDIR=$(TOOLCHAIN)/usr/tools + +# use our new toolchain +USECLANG=1 +override CC=$(TOOLDIR)/clang +override CXX=$(TOOLDIR)/clang++ + +USE_BINARYBUILDER_LLVM=1 + +override SANITIZE=1 +override SANITIZE_THREAD=1 + +# default to a debug build for better line number reporting +override JULIA_BUILD_MODE=debug diff --git a/contrib/tsan/build.sh b/contrib/tsan/build.sh new file mode 100755 index 00000000000000..2c4ba3b1bde95d --- /dev/null +++ b/contrib/tsan/build.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# This file is a part of Julia. License is MIT: https://julialang.org/license + +# +# Usage: +# contrib/tsan/build.sh [...] +# +# Build TSAN-enabled julia. Given a workspace directory , build +# TSAN-enabled julia in /tsan. Required toolss are install under +# /toolchain. Note that the same passed to `contrib/asan/build.sh` +# can be used to share the toolchain used for ASAN. This scripts also takes +# optional arguments which are passed to `make`. The default +# make target is `debug`. + +set -ue + +# `$WORKSPACE` is a directory in which we create `toolchain` and `tsan` +# sub-directories. +WORKSPACE="$1" +shift +if [ "$WORKSPACE" = "" ]; then + echo "Workspace directory must be specified as the first argument" >&2 + exit 2 +fi + +mkdir -pv "$WORKSPACE" +WORKSPACE="$(cd "$WORKSPACE" && pwd)" +if [ "$WORKSPACE" = "" ]; then + echo "Failed to create the workspace directory." >&2 + exit 2 +fi + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +JULIA_HOME="$HERE/../../" + +echo +echo "Installing toolchain..." + +TOOLCHAIN="$WORKSPACE/toolchain" +if [ ! -d "$TOOLCHAIN" ]; then + make -C "$JULIA_HOME" configure O=$TOOLCHAIN + cp "$HERE/../asan/Make.user.tools" "$TOOLCHAIN/Make.user" +fi + +make -C "$TOOLCHAIN/deps" install-clang install-llvm-tools + +echo +echo "Building Julia..." + +BUILD="$WORKSPACE/tsan" +if [ ! -d "$BUILD" ]; then + make -C "$JULIA_HOME" configure O="$BUILD" + cp "$HERE/Make.user.tsan" "$BUILD/Make.user" +fi + +cd "$BUILD" # so that we can pass `-C src` to `make` +make "$@" diff --git a/contrib/updateSPDX.jl b/contrib/updateSPDX.jl new file mode 100644 index 00000000000000..94b428ac70748a --- /dev/null +++ b/contrib/updateSPDX.jl @@ -0,0 +1,31 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +# SPDX-License-Identifier: MIT +# Run this script with each new Julia release to update "../julia.spdx.json" + +using UUIDs +using Dates +using JSON +using TimeZones +using DataStructures + +spdxDocument= "../julia.spdx.json" +spdxData= JSON.parsefile(spdxDocument; dicttype=OrderedDict{String, Any}) + +# At the moment we can only update a few items automatically with each release. +# These are the crucial elements to make a new version of the SPDX file. +# Any other changes (ex. Adding or removing of external dependencies, updating copyright text, etc.) must be performed manually +spdxData["documentNamespace"]= "https://julialang.org/spdxdocs/julia-spdx-" * string(uuid4()) +spdxData["creationInfo"]["created"]= Dates.format(now(tz"UTC"), "yyyy-mm-ddTHH:MM:SS") * "Z" + +for pkg in spdxData["packages"] + if pkg["SPDXID"] == "SPDXRef-JuliaMain" + pkg["versionInfo"]= readline("../VERSION") + pkg["downloadLocation"]= "git+https://github.com/JuliaLang/julia.git@v" * pkg["versionInfo"] + break + end +end + +open(spdxDocument, "w") do f + JSON.print(f, spdxData, 4) +end diff --git a/contrib/valgrind-julia.supp b/contrib/valgrind-julia.supp index 86f843f3f4376e..408a48a2893cc4 100644 --- a/contrib/valgrind-julia.supp +++ b/contrib/valgrind-julia.supp @@ -2,9 +2,9 @@ { msync unwind Memcheck:Param - msync(start) + write(buf) ... - obj:*/libpthread*.so + fun:validate_mem ... - fun:rec_backtrace_ctx + fun:rec_backtrace } diff --git a/contrib/windows/Vagrantfile b/contrib/windows/Vagrantfile deleted file mode 100644 index dbd8aa0e3fb979..00000000000000 --- a/contrib/windows/Vagrantfile +++ /dev/null @@ -1,76 +0,0 @@ -# Vagrantfile for building Windows Julia via MSYS2 or Cygwin - -$script_cygwin = <