From 31e0ba003f5e419c7aa70283d38af495238294c8 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Wed, 3 Jul 2024 21:42:01 +0200 Subject: [PATCH] Squashed 'packages/JuliaInterpreter/' changes from b2ebd69..96ed09c 96ed09c version 0.9.32 0a8da3a minimum adjustments to make JuliaInterpreter work with v1.12 (#631) 397ea70 `public` as an identifier is deprecated (#612) ce6e341 update builtins.jl (#628) 1024848 Merge pull request #629 from JuliaDebug/avi/54678 5700dcc adjustments to JuliaLang/julia#54428 ea522b8 adjustments to JuliaLang/julia#54678 eae3b4c Some more 1.12 compat (#625) fc4aeca version 0.9.31 a265c14 [incomplete] changes needed to adapt to compressed line table format in Julia (#606) 31253a0 version 0.9.30 1b1b6d6 adjustments for Julia v1.11 (#615) 8043dbc add support for preset `current_scope` (#619) 55e33d0 use child `@testset` to get nicer test summary (#618) e0e34be adapt to implicit leave change in Julia (#614) f7138f9 v0.9.29 82b1552 Adjust to upcoming compiler change (#611) 011edf9 improve test code 78f766b minor improvements 713c768 implement support for `current_scope` (#605) 580b95c version 0.9.28 d7d4ced Fix revise#718 (#609) 0089e4b update docs and convert `jldoctest` to `@repl` blocks (#607) d319168 Remove buggy linearization pass (#604) 1efae18 remove `AbstractFrameInstance` (#608) 0138e60 update CI.yml 6b1c476 version 0.9.27 ce20820 adjust to the `:enter` IR changes made in JuliaLang/julia#52300 (#599) 9afdf71 follow up #596 (#600) 9d50726 adapt to array changes in Julia base (#596) 68fa8be NFC: harden some internal ccalls (#595) ccc1c95 remove old compats (#598) 15ad1c7 set CI timeout (#597) 7beca92 version 0.9.26 a0d0d33 Adjust to upcoming julia lowering change (#592) a3cf18e Add a second link to the docs from the README (#589) 6da0b26 version 0.9.25 c8d1ef7 adjust to JuliaLang/julia#50943 (#585) c93dedf adjust tests for latest Julia master (#584) 910cb6f Align arguments number in breakpoints hook docstring (#583) 7849d4a Bump actions/checkout from 2 to 3 (#576) 0169df2 Version 0.9.24 14e454b Ignore `:aliasscope` and `:popaliasscope` (#581) 3ab2674 Bump codecov/codecov-action from 1 to 3 (#577) cc1bace Bump actions/cache from 1 to 3 (#578) 1d87867 Update some failing doctests (#579) 43f2041 enable dependabot for GitHub actions (#575) aefaa30 use an explicit Any comprehension (#572) 475512b Version 0.9.23 8fecf35 Fix kw pattern matching, other changes on 1.9+ (#568) cf7f437 also test on 1.9 57dbc98 version 0.9.22 d7a3dd4 fixup b4d133d adjust to JuliaLang/julia#48693 (#566) 9026819 fix test on nightly (#564) 9c5454c Protect `error` calls from invalidation (#565) da3fee2 remove unused import 2a1c076 bump version 6d2fbaf rejigger the code to compute the method instance in stacktraces (#563) git-subtree-dir: packages/JuliaInterpreter git-subtree-split: 96ed09c7127475d391b1a4f20906072f482278eb --- .gitattributes | 1 + .github/dependabot.yml | 7 + .github/workflows/CI.yml | 42 +++-- .github/workflows/Documenter.yml | 7 +- .github/workflows/check_builtins.yml | 4 +- .gitignore | 1 + Project.toml | 2 +- README.md | 4 + bin/generate_builtins.jl | 78 ++++++--- docs/Manifest.toml | 193 +++++++++++++++++++--- docs/make.jl | 1 - docs/src/index.md | 151 ++++++----------- docs/src/internals.md | 16 +- src/breakpoints.jl | 9 +- src/builtins.jl | 236 ++++++++++++++++++++++----- src/commands.jl | 40 +++-- src/construct.jl | 83 +++++----- src/interpret.jl | 210 ++++++++++++++---------- src/optimize.jl | 221 +++++++------------------ src/packagedef.jl | 10 +- src/precompile.jl | 4 +- src/types.jl | 69 ++++---- src/utils.jl | 168 ++++++++++++------- test/breakpoints.jl | 10 +- test/code_coverage/code_coverage.jl | 12 +- test/core.jl | 4 +- test/debug.jl | 89 +++++----- test/eval_code.jl | 28 +++- test/interpret.jl | 131 ++++++++------- test/interpret_scopedvalues.jl | 55 +++++++ test/limits.jl | 21 ++- test/runtests.jl | 19 +-- test/toplevel.jl | 62 +++++-- test/toplevel_script.jl | 2 +- test/utils.jl | 9 +- 35 files changed, 1248 insertions(+), 751 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 test/interpret_scopedvalues.jl diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4d88c69 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +docs/Manifest.toml linguist-generated=true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d60f070 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" # Location of package manifests + schedule: + interval: "monthly" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7d9fdd6..b3771d8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,28 +9,44 @@ jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} runs-on: ${{ matrix.os }} + timeout-minutes: 30 strategy: fail-fast: false matrix: - version: - - '1.6' # LTS - - '1' # current stable - - 'nightly' - os: - - ubuntu-latest - - macOS-latest - - windows-latest - arch: - - x64 + include: + - version: '1.6' # old LTS + os: ubuntu-latest + arch: x64 + - version: '1.10' # current stable + os: ubuntu-latest + arch: x64 + - version: '~1.11.0-0' # next release + os: ubuntu-latest + arch: x64 + - version: 'nightly' # dev + os: ubuntu-latest + arch: x64 + #- version: '1' # x86 ubuntu -- disabled since PyCall/conda is broken on this platform + # os: ubuntu-latest + # arch: x86 + - version: '1' # x86 windows + os: windows-latest + arch: x86 + - version: '1' # x64 windows + os: windows-latest + arch: x64 + - version: '1' # x64 macOS + os: macos-latest + arch: x64 env: PYTHON: "" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 + - uses: actions/cache@v3 env: cache-name: cache-artifacts with: @@ -43,6 +59,6 @@ jobs: - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: file: lcov.info diff --git a/.github/workflows/Documenter.yml b/.github/workflows/Documenter.yml index 8faa2e2..b6a896a 100644 --- a/.github/workflows/Documenter.yml +++ b/.github/workflows/Documenter.yml @@ -9,8 +9,13 @@ jobs: Documenter: name: Documentation runs-on: ubuntu-latest + timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: '1' + show-versioninfo: true # print versioninfo in the action log + - uses: actions/checkout@v3 - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-docdeploy@latest env: diff --git a/.github/workflows/check_builtins.yml b/.github/workflows/check_builtins.yml index fd3b6e7..5337fc3 100644 --- a/.github/workflows/check_builtins.yml +++ b/.github/workflows/check_builtins.yml @@ -10,12 +10,12 @@ jobs: name: 'Check builtins.jl consistency' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: julia-actions/setup-julia@v1 with: version: nightly arch: x64 - - uses: actions/cache@v1 + - uses: actions/cache@v3 env: cache-name: cache-artifacts with: diff --git a/.gitignore b/.gitignore index c037984..8c81dfa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ deps/build.log docs/build/ test/results.md Manifest.toml +Manifest-*.toml !/test/code_coverage/coverage_example.jl.cov diff --git a/Project.toml b/Project.toml index fce96d8..be61324 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "JuliaInterpreter" uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" -version = "0.9.20" +version = "0.9.32" [deps] CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" diff --git a/README.md b/README.md index 6170c40..0e5aa99 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ ]add JuliaInterpreter ``` +## Usage + +See the [documentation][docs-stable-url]. + [docs-stable-img]: https://img.shields.io/badge/docs-stable-blue.svg [docs-stable-url]: https://JuliaDebug.github.io/JuliaInterpreter.jl/stable diff --git a/bin/generate_builtins.jl b/bin/generate_builtins.jl index bc6da0d..bfd84bd 100644 --- a/bin/generate_builtins.jl +++ b/bin/generate_builtins.jl @@ -2,15 +2,19 @@ # Should be run on the latest Julia nightly using InteractiveUtils -# All bultins added since 1.6. Needs to be updated whenever a new builtin is added -const RECENTLY_ADDED = Core.Builtin[ - Core._call_in_world_total, Core.donotdelete, - Core.get_binding_type, Core.set_binding_type!, - Core.getglobal, Core.setglobal!, - Core.modifyfield!, Core.replacefield!, Core.swapfield!, - Core.finalizer, Core._compute_sparams, Core._svec_ref, - Core.compilerbarrier +# All builtins present in 1.6 +const ALWAYS_PRESENT = Core.Builtin[ + (<:), (===), Core._abstracttype, Core._apply_iterate, Core._apply_pure, + Core._call_in_world, Core._call_latest, Core._equiv_typedef, Core._expr, + Core._primitivetype, Core._setsuper!, Core._structtype, Core._typebody!, + Core._typevar, Core.apply_type, Core.ifelse, Core.sizeof, Core.svec, + applicable, fieldtype, getfield, invoke, isa, isdefined, nfields, + setfield!, throw, tuple, typeassert, typeof ] +# Builtins present from 1.6, not builtins (potentially still normal functions) anymore +const RECENTLY_REMOVED = GlobalRef.(Ref(Core), [ + :arrayref, :arrayset, :arrayset, :const_arrayref, :memoryref, +]) const kwinvoke = Core.kwfunc(Core.invoke) function scopedname(f) @@ -33,11 +37,10 @@ function nargs(f, table, id) minarg = 0 maxarg = typemax(Int) end - # Specialize arrayref and arrayset for small numbers of arguments - if f == Core.arrayref + # Specialize ~arrayref and arrayset~ memoryrefnew for small numbers of arguments + # TODO: how about other memory intrinsics? + if (@static isdefined(Core, :memoryrefnew) ? f == Core.memoryrefnew : f == Core.memoryref) maxarg = 5 - elseif f == Core.arrayset - maxarg = 6 end return minarg, maxarg end @@ -183,13 +186,13 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) print(io, """ $head f === $fstr - if !expand - argswrapped = getargs(args, frame) - return Some{Any}($fstr(argswrapped...)) - end - # This uses the original arguments to avoid looking them up twice - # See #442 - return Expr(:call, invoke, args[2:end]...) + if !expand + argswrapped = getargs(args, frame) + return Some{Any}($fstr(argswrapped...)) + end + # This uses the original arguments to avoid looking them up twice + # See #442 + return Expr(:call, invoke, args[2:end]...) """) continue elseif f === Core._call_latest @@ -207,11 +210,27 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) end return maybe_recurse_expanded_builtin(frame, new_expr) """) + continue + elseif f === Core.current_scope + print(io, +""" + elseif @static isdefined(Core, :current_scope) && f === Core.current_scope + if nargs == 0 + currscope = Core.current_scope() + for scope in frame.framedata.current_scopes + currscope = Scope(currscope, scope.values...) + end + return Some{Any}(currscope) + else + return Some{Any}(Core.current_scope(getargs(args, frame)...)) + end +""") + continue end id = findfirst(isequal(f), Core.Compiler.T_FFUNC_KEY) fcall = generate_fcall(f, Core.Compiler.T_FFUNC_VAL, id) - if f in RECENTLY_ADDED + if !(f in ALWAYS_PRESENT) print(io, """ $head @static isdefined($(ft.name.module), $(repr(nameof(f)))) && f === $fname @@ -246,6 +265,25 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) return Some{Any}(Core.eval(moduleof(frame), call_expr)) end """) + # recently removed builtins + for (; mod, name) in RECENTLY_REMOVED + minarg = 1 + if name in (:arrayref, :const_arrayref, :memoryref) + maxarg = 5 + elseif name === :arrayset + maxarg = 6 + elseif name === :arraysize + maxarg = 2 + end + _scopedname = "$mod.$name" + fcall = generate_fcall_nargs(_scopedname, minarg, maxarg) + rname = repr(name) + print(io, +""" + elseif @static (isdefined($mod, $rname) && $_scopedname isa Core.Builtin) && f === $_scopedname + $fcall +""") + end # Extract any intrinsics that support varargs fva = [] minmin, maxmax = typemax(Int), 0 diff --git a/docs/Manifest.toml b/docs/Manifest.toml index fad80f8..f060196 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -1,6 +1,6 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.8.2" +julia_version = "1.10.0" manifest_format = "2.0" project_hash = "f8f17ea8030e6083899e9cc89b9b04cd28b94813" @@ -9,14 +9,26 @@ git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" version = "0.0.1" +[[deps.AbstractTrees]] +git-tree-sha1 = "faa260e4cb5aba097a73fab382dd4b5819d8ec8c" +uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +version = "0.4.4" + +[[deps.ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.1" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" + [[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" [[deps.CodeTracking]] deps = ["InteractiveUtils", "UUIDs"] -git-tree-sha1 = "cc4bd91eba9cdbbb4df4746124c22c0832a460d6" +git-tree-sha1 = "c0216e792f518b39b22212127d4a84dc31e4e386" uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" -version = "1.1.1" +version = "1.3.5" [[deps.Dates]] deps = ["Printf"] @@ -24,42 +36,108 @@ uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" [[deps.DocStringExtensions]] deps = ["LibGit2"] -git-tree-sha1 = "c36550cb29cbe373e95b3f40486b9a4148f89ffd" +git-tree-sha1 = "2fb1e02f2b635d0845df5d7c167fec4dd739b00d" uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -version = "0.9.2" +version = "0.9.3" [[deps.Documenter]] -deps = ["ANSIColoredPrinters", "Base64", "Dates", "DocStringExtensions", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] -git-tree-sha1 = "6030186b00a38e9d0434518627426570aac2ef95" +deps = ["ANSIColoredPrinters", "AbstractTrees", "Base64", "Dates", "DocStringExtensions", "Downloads", "Git", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "MarkdownAST", "Pkg", "PrecompileTools", "REPL", "RegistryInstances", "SHA", "Test", "Unicode"] +git-tree-sha1 = "2613dbec8f4748273bbe30ba71fd5cb369966bac" uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "0.27.23" +version = "1.2.1" + +[[deps.Downloads]] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.6.0" + +[[deps.Expat_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "4558ab818dcceaab612d1bb8c19cee87eda2b83c" +uuid = "2e619515-83b5-522b-bb60-26c02a35a201" +version = "2.5.0+0" + +[[deps.FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" + +[[deps.Git]] +deps = ["Git_jll"] +git-tree-sha1 = "51764e6c2e84c37055e846c516e9015b4a291c7d" +uuid = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" +version = "1.3.0" + +[[deps.Git_jll]] +deps = ["Artifacts", "Expat_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libiconv_jll", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"] +git-tree-sha1 = "bb8f7cc77ec1152414b2af6db533d9471cfbb2d1" +uuid = "f8c6e375-362e-5223-8a59-34ff63f689eb" +version = "2.42.0+0" [[deps.IOCapture]] deps = ["Logging", "Random"] -git-tree-sha1 = "f7be53659ab06ddc986428d3a9dcc95f6fa6705a" +git-tree-sha1 = "d75853a0bdbfb1ac815478bacd89cd27b550ace6" uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" -version = "0.2.2" +version = "0.2.3" [[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +[[deps.JLLWrappers]] +deps = ["Artifacts", "Preferences"] +git-tree-sha1 = "7e5d6779a1e09a36db2a7b6cff50942a0a7d0fca" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.5.0" + [[deps.JSON]] deps = ["Dates", "Mmap", "Parsers", "Unicode"] -git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e" +git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.21.3" +version = "0.21.4" [[deps.JuliaInterpreter]] deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] path = ".." uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" -version = "0.9.15" +version = "0.9.27" + +[[deps.LazilyInitializedFields]] +git-tree-sha1 = "8f7f3cabab0fd1800699663533b6d5cb3fc0e612" +uuid = "0e77f7df-68c5-4e49-93ce-4cd80f5598bf" +version = "1.2.2" + +[[deps.LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.4" + +[[deps.LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.4.0+0" [[deps.LibGit2]] -deps = ["Base64", "NetworkOptions", "Printf", "SHA"] +deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +[[deps.LibGit2_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.6.4+0" + +[[deps.LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "MbedTLS_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.0+1" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[deps.Libiconv_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "f9557a255370125b405568f9767d6d195822a175" +uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" +version = "1.17.0+0" + [[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" @@ -67,18 +145,61 @@ uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +[[deps.MarkdownAST]] +deps = ["AbstractTrees", "Markdown"] +git-tree-sha1 = "465a70f0fc7d443a00dcdc3267a497397b8a3899" +uuid = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +version = "0.1.2" + +[[deps.MbedTLS_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +version = "2.28.2+1" + [[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" +[[deps.MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2023.1.10" + [[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" version = "1.2.0" +[[deps.OpenSSL_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "cc6e1927ac521b659af340e0ca45828a3ffc748f" +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "3.0.12+0" + +[[deps.PCRE2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" +version = "10.42.0+1" + [[deps.Parsers]] -deps = ["Dates", "SnoopPrecompile"] -git-tree-sha1 = "cceb0257b662528ecdf0b4b4302eb00e767b38e7" +deps = ["Dates", "PrecompileTools", "UUIDs"] +git-tree-sha1 = "8489905bcdbcfac64d1daa51ca07c0d8f0283821" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.5.0" +version = "2.8.1" + +[[deps.Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.10.0" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "03b4c25b43cb84cee5c90aa9b5ea0a78fd848d2f" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.2.0" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "00805cd429dcb4870060ff49ef443486c262e38e" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.4.1" [[deps.Printf]] deps = ["Unicode"] @@ -89,9 +210,15 @@ deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" [[deps.Random]] -deps = ["SHA", "Serialization"] +deps = ["SHA"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +[[deps.RegistryInstances]] +deps = ["LazilyInitializedFields", "Pkg", "TOML", "Tar"] +git-tree-sha1 = "ffd19052caf598b8653b99404058fce14828be51" +uuid = "2792f1a3-b283-48e8-9a74-f99dce5104f3" +version = "0.1.0" + [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" version = "0.7.0" @@ -99,14 +226,19 @@ version = "0.7.0" [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -[[deps.SnoopPrecompile]] -git-tree-sha1 = "f604441450a3c0569830946e5b33b78c928e1a85" -uuid = "66db9d55-30c0-4569-8b51-7e840670fc0c" -version = "1.0.1" - [[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" + +[[deps.Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" + [[deps.Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" @@ -117,3 +249,18 @@ uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[deps.Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.2.13+1" + +[[deps.nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.52.0+1" + +[[deps.p7zip_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.4.0+2" diff --git a/docs/make.jl b/docs/make.jl index ded4791..5cd2b34 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -10,7 +10,6 @@ DocMeta.setdocmeta!(JuliaInterpreter, :DocTestSetup, :( makedocs( modules = [JuliaInterpreter], clean = false, - strict = true, format = Documenter.HTML(prettyurls = get(ENV, "CI", nothing) == "true"), sitename = "JuliaInterpreter.jl", authors = "Keno Fischer, Tim Holy, Kristoffer Carlsson, and others", diff --git a/docs/src/index.md b/docs/src/index.md index 0b67a04..87c3616 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -9,20 +9,14 @@ Interpreters have a number of applications, including support for stepping debug Using this package as an interpreter is straightforward: -```jldoctest demo1 -julia> using JuliaInterpreter +```@repl index +using JuliaInterpreter -julia> list = [1, 2, 5] -3-element Vector{Int64}: - 1 - 2 - 5 +list = [1, 2, 5] -julia> sum(list) -8 +sum(list) -julia> @interpret sum(list) -8 +@interpret sum(list) ``` ## Breakpoints @@ -37,8 +31,8 @@ so here we'll illustrate it without using any of these other packages. Let's set a conditional breakpoint, to be triggered any time one of the elements in the argument to `sum` is bigger than 4: -```jldoctest demo1; filter = r"in Base at .*$" -julia> bp = @breakpoint sum([1, 2]) any(x->x>4, a); +```@repl index +bp = @breakpoint sum([1, 2]) any(x->x>4, a); ``` Note that in writing the condition, we used `a`, the name of the argument to the relevant @@ -48,17 +42,10 @@ globally-available name (as used here with the `any` function). Now let's see what happens: -```jldoctest demo1; filter = [r"in Base at .*$", r"[^\d]\d\d\d[^\d]"] -julia> @interpret sum([1,2,3]) # no element bigger than 4, breakpoint should not trigger -6 - -julia> frame, bpref = @interpret sum([1,2,5]) # should trigger breakpoint -(Frame for sum(a::AbstractArray; dims, kw...) in Base at reducedim.jl:873 -c 1* 873 1 ─ nothing - 2 873 │ %2 = ($(QuoteNode(NamedTuple)))() - 3 873 │ %3 = Base.pairs(%2) -⋮ -a = [1, 2, 5], breakpoint(sum(a::AbstractArray; dims, kw...) in Base at reducedim.jl:873, line 873)) +```@repl index +@interpret sum([1,2,3]) # no element bigger than 4, breakpoint should not trigger + +frame, bpref = @interpret sum([1,2,5]) # should trigger breakpoint ``` `frame` is described in more detail on the next page; for now, suffice it to say @@ -66,21 +53,14 @@ that the `c` in the leftmost column indicates the presence of a conditional brea upon entry to `sum`. `bpref` is a reference to the breakpoint of type [`BreakpointRef`](@ref). The breakpoint `bp` we created can be manipulated at the command line -```jldoctest demo1; filter = [r"in Base at .*$", r"[^\d]\d\d\d[^\d]"] -julia> disable(bp) +```@repl index +disable(bp) -julia> @interpret sum([1,2,5]) -8 +@interpret sum([1,2,5]) -julia> enable(bp) +enable(bp) -julia> @interpret sum([1,2,5]) -(Frame for sum(a::AbstractArray; dims, kw...) in Base at reducedim.jl:873 -c 1* 873 1 ─ nothing - 2 873 │ %2 = ($(QuoteNode(NamedTuple)))() - 3 873 │ %3 = Base.pairs(%2) -⋮ -a = [1, 2, 5], breakpoint(sum(a::AbstractArray; dims, kw...) in Base at reducedim.jl:873, line 873)) +@interpret sum([1,2,5]) ``` [`disable`](@ref) and [`enable`](@ref) allow you to turn breakpoints off and on without losing any @@ -96,76 +76,43 @@ At present, note that some of this functionality requires that you be running It is, in addition, possible to halt execution when otherwise an error would be thrown. This functionality is enabled using [`break_on`](@ref) and disabled with [`break_off`](@ref): -```jldoctest demo1; filter=r"none:\d" -julia> function f_outer() - println("before error") - f_inner() - println("after error") - end; - -julia> f_inner() = error("inner error"); - -julia> break_on(:error) - -julia> fr, pc = @interpret f_outer() -before error -(Frame for f_outer() in Main at none:1 - 1 2 1 ─ Base.println("before error") - 2* 3 │ f_inner() - 3 4 │ %3 = Base.println("after error") - 4 4 └── return %3 -callee: f_inner() in Main at none:1, breakpoint(error(s::AbstractString) in Base at error.jl:35, line 35, ErrorException("inner error"))) - -julia> leaf(fr) -Frame for error(s::AbstractString) in Base at error.jl:35 - 1 35 1 ─ %1 = ($(QuoteNode(ErrorException)))(s) - 2* 35 │ %2 = Core.throw(%1) - 3 35 └── return %2 -s = "inner error" -caller: f_inner() in Main at none:1 - -julia> typeof(pc) -BreakpointRef - -julia> pc.err -ErrorException("inner error") - -julia> break_off(:error) - -julia> @interpret f_outer() -before error -ERROR: inner error -Stacktrace: -[...] +```@repl index +function f_outer() + println("before error") + f_inner() + println("after error") +end; + +f_inner() = error("inner error"); + +break_on(:error) + +fr, pc = @interpret f_outer() + +leaf(fr) + +typeof(pc) + +pc.err + +break_off(:error) + +@interpret f_outer() ``` Finally, you can set breakpoints using [`@bp`](@ref): -```jldoctest demo1; filter=r"none:\d" -julia> function myfunction(x, y) - a = 1 - b = 2 - x > 3 && @bp - return a + b + x + y - end -myfunction (generic function with 1 method) - -julia> @interpret myfunction(1, 2) -6 - -julia> @interpret myfunction(5, 6) -(Frame for myfunction(x, y) in Main at none:1 -⋮ - 3 4 │ %3 = x > 3 - 4 4 └── goto #3 if not %3 -b 5* 4 2 ─ nothing - 6 4 └── goto #3 - 7 5 3 ┄ %7 = a + b + x + y -⋮ -x = 5 -y = 6 -b = 2 -a = 1, breakpoint(myfunction(x, y) in Main at none:1, line 4)) +```@repl index +function myfunction(x, y) + a = 1 + b = 2 + x > 3 && @bp + return a + b + x + y +end; + +@interpret myfunction(1, 2) + +@interpret myfunction(5, 6) ``` Here the breakpoint is marked with a `b` indicating that it is an unconditional breakpoint. diff --git a/docs/src/internals.md b/docs/src/internals.md index 32a8473..3bc798b 100644 --- a/docs/src/internals.md +++ b/docs/src/internals.md @@ -146,20 +146,22 @@ Sometimes you might have a whole sequence of expressions you want to run. In such cases, your first thought should be to construct the `Frame` manually. Here's a demonstration: -```jldoctest; setup=(using JuliaInterpreter; JuliaInterpreter.clear_caches()) +```@setup internals +using JuliaInterpreter; +JuliaInterpreter.clear_caches() +``` + +```@repl internals using Test ex = quote x, y = 1, 2 @test x + y == 3 -end +end; -frame = Frame(Main, ex) -JuliaInterpreter.finish_and_return!(frame) - -# output +frame = Frame(Main, ex); -Test Passed +JuliaInterpreter.finish_and_return!(frame) ``` ## Toplevel code and world age diff --git a/src/breakpoints.jl b/src/breakpoints.jl index 1ea011b..f8d47aa 100644 --- a/src/breakpoints.jl +++ b/src/breakpoints.jl @@ -14,7 +14,7 @@ const breakpoint_update_hooks = [] """ on_breakpoints_updated(f) -Register a one-argument function to be called after any update to the set of all +Register a two-argument function to be called after any update to the set of all breakpoints. This includes their creation, deletion, enabling and disabling. The function `f` should take two inputs: @@ -247,7 +247,7 @@ function breakpoint!(framecode::FrameCode, pc, condition::Condition=nothing, ena framecode.breakpoints[stmtidx] = BreakpointState(enabled, Core.eval(mod, fex)) end end -breakpoint!(framecode::FrameCode, pcs::AbstractArray, condition::Condition=nothing, enabled=true) = +breakpoint!(framecode::FrameCode, pcs::AbstractArray, condition::Condition=nothing, enabled=true) = foreach(pc -> breakpoint!(framecode, pc, condition, enabled), pcs) breakpoint!(frame::Frame, pc=frame.pc, condition::Condition=nothing) = breakpoint!(frame.framecode, pc, condition) @@ -426,7 +426,8 @@ macro breakpoint(call_expr, args...) end end -const __BREAKPOINT_MARKER__ = nothing +struct BreakPointMarker end +const __BREAK_POINT_MARKER__ = BreakPointMarker() """ @bp @@ -434,5 +435,5 @@ const __BREAKPOINT_MARKER__ = nothing Insert a breakpoint at a location in the source code. """ macro bp() - return esc(:($(JuliaInterpreter).__BREAKPOINT_MARKER__)) + return :(__BREAK_POINT_MARKER__) end diff --git a/src/builtins.jl b/src/builtins.jl index 90fb6ad..aac504b 100644 --- a/src/builtins.jl +++ b/src/builtins.jl @@ -94,8 +94,6 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) push!(new_expr.args, QuoteNode(x)) end return maybe_recurse_expanded_builtin(frame, new_expr) - elseif f === Core._call_latest - return Some{Any}(Core._call_latest(getargs(args, frame)...)) elseif @static isdefined(Core, :_compute_sparams) && f === Core._compute_sparams return Some{Any}(Core._compute_sparams(getargs(args, frame)...)) elseif f === Core._equiv_typedef @@ -120,40 +118,22 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) end elseif f === Core.apply_type return Some{Any}(Core.apply_type(getargs(args, frame)...)) - elseif f === Core.arrayref - if nargs == 3 - return Some{Any}(Core.arrayref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) - elseif nargs == 4 - return Some{Any}(Core.arrayref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) - elseif nargs == 5 - return Some{Any}(Core.arrayref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) - else - return Some{Any}(Core.arrayref(getargs(args, frame)...)) - end - elseif f === Core.arrayset - if nargs == 4 - return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) - elseif nargs == 5 - return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) - elseif nargs == 6 - return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]), @lookup(frame, args[7]))) - else - return Some{Any}(Core.arrayset(getargs(args, frame)...)) - end - elseif f === Core.arraysize - if nargs == 2 - return Some{Any}(Core.arraysize(@lookup(frame, args[2]), @lookup(frame, args[3]))) - else - return Some{Any}(Core.arraysize(getargs(args, frame)...)) - end elseif @static isdefined(Core, :compilerbarrier) && f === Core.compilerbarrier if nargs == 2 return Some{Any}(Core.compilerbarrier(@lookup(frame, args[2]), @lookup(frame, args[3]))) else return Some{Any}(Core.compilerbarrier(getargs(args, frame)...)) end - elseif f === Core.const_arrayref - return Some{Any}(Core.const_arrayref(getargs(args, frame)...)) + elseif @static isdefined(Core, :current_scope) && f === Core.current_scope + if nargs == 0 + currscope = Core.current_scope() + for scope in frame.framedata.current_scopes + currscope = Scope(currscope, scope.values...) + end + return Some{Any}(currscope) + else + return Some{Any}(Core.current_scope(getargs(args, frame)...)) + end elseif @static isdefined(Core, :donotdelete) && f === Core.donotdelete return Some{Any}(Core.donotdelete(getargs(args, frame)...)) elseif @static isdefined(Core, :finalizer) && f === Core.finalizer @@ -178,6 +158,68 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) else return Some{Any}(Core.ifelse(getargs(args, frame)...)) end + elseif @static isdefined(Core, :memoryref_isassigned) && f === Core.memoryref_isassigned + if nargs == 3 + return Some{Any}(Core.memoryref_isassigned(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) + else + return Some{Any}(Core.memoryref_isassigned(getargs(args, frame)...)) + end + elseif @static isdefined(Core, :memoryrefget) && f === Core.memoryrefget + if nargs == 3 + return Some{Any}(Core.memoryrefget(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) + else + return Some{Any}(Core.memoryrefget(getargs(args, frame)...)) + end + elseif @static isdefined(Core, :memoryrefmodify!) && f === Core.memoryrefmodify! + if nargs == 5 + return Some{Any}(Core.memoryrefmodify!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + else + return Some{Any}(Core.memoryrefmodify!(getargs(args, frame)...)) + end + elseif @static isdefined(Core, :memoryrefnew) && f === Core.memoryrefnew + if nargs == 1 + return Some{Any}(Core.memoryrefnew(@lookup(frame, args[2]))) + elseif nargs == 2 + return Some{Any}(Core.memoryrefnew(@lookup(frame, args[2]), @lookup(frame, args[3]))) + elseif nargs == 3 + return Some{Any}(Core.memoryrefnew(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) + elseif nargs == 4 + return Some{Any}(Core.memoryrefnew(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + elseif nargs == 5 + return Some{Any}(Core.memoryrefnew(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + else + return Some{Any}(Core.memoryrefnew(getargs(args, frame)...)) + end + elseif @static isdefined(Core, :memoryrefoffset) && f === Core.memoryrefoffset + if nargs == 1 + return Some{Any}(Core.memoryrefoffset(@lookup(frame, args[2]))) + else + return Some{Any}(Core.memoryrefoffset(getargs(args, frame)...)) + end + elseif @static isdefined(Core, :memoryrefreplace!) && f === Core.memoryrefreplace! + if nargs == 6 + return Some{Any}(Core.memoryrefreplace!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]), @lookup(frame, args[7]))) + else + return Some{Any}(Core.memoryrefreplace!(getargs(args, frame)...)) + end + elseif @static isdefined(Core, :memoryrefset!) && f === Core.memoryrefset! + if nargs == 4 + return Some{Any}(Core.memoryrefset!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + else + return Some{Any}(Core.memoryrefset!(getargs(args, frame)...)) + end + elseif @static isdefined(Core, :memoryrefsetonce!) && f === Core.memoryrefsetonce! + if nargs == 5 + return Some{Any}(Core.memoryrefsetonce!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + else + return Some{Any}(Core.memoryrefsetonce!(getargs(args, frame)...)) + end + elseif @static isdefined(Core, :memoryrefswap!) && f === Core.memoryrefswap! + if nargs == 4 + return Some{Any}(Core.memoryrefswap!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + else + return Some{Any}(Core.memoryrefswap!(getargs(args, frame)...)) + end elseif @static isdefined(Core, :set_binding_type!) && f === Core.set_binding_type! return Some{Any}(Core.set_binding_type!(getargs(args, frame)...)) elseif f === Core.sizeof @@ -217,13 +259,13 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) return Some{Any}(getglobal(getargs(args, frame)...)) end elseif f === invoke - if !expand - argswrapped = getargs(args, frame) - return Some{Any}(invoke(argswrapped...)) - end - # This uses the original arguments to avoid looking them up twice - # See #442 - return Expr(:call, invoke, args[2:end]...) + if !expand + argswrapped = getargs(args, frame) + return Some{Any}(invoke(argswrapped...)) + end + # This uses the original arguments to avoid looking them up twice + # See #442 + return Expr(:call, invoke, args[2:end]...) elseif f === isa if nargs == 2 return Some{Any}(isa(@lookup(frame, args[2]), @lookup(frame, args[3]))) @@ -246,6 +288,14 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) else return Some{Any}(modifyfield!(getargs(args, frame)...)) end + elseif @static isdefined(Core, :modifyglobal!) && f === modifyglobal! + if nargs == 4 + return Some{Any}(modifyglobal!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + elseif nargs == 5 + return Some{Any}(modifyglobal!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + else + return Some{Any}(modifyglobal!(getargs(args, frame)...)) + end elseif f === nfields if nargs == 1 return Some{Any}(nfields(@lookup(frame, args[2]))) @@ -262,6 +312,16 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) else return Some{Any}(replacefield!(getargs(args, frame)...)) end + elseif @static isdefined(Core, :replaceglobal!) && f === replaceglobal! + if nargs == 4 + return Some{Any}(replaceglobal!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + elseif nargs == 5 + return Some{Any}(replaceglobal!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + elseif nargs == 6 + return Some{Any}(replaceglobal!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]), @lookup(frame, args[7]))) + else + return Some{Any}(replaceglobal!(getargs(args, frame)...)) + end elseif f === setfield! if nargs == 3 return Some{Any}(setfield!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) @@ -270,6 +330,16 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) else return Some{Any}(setfield!(getargs(args, frame)...)) end + elseif @static isdefined(Core, :setfieldonce!) && f === setfieldonce! + if nargs == 3 + return Some{Any}(setfieldonce!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) + elseif nargs == 4 + return Some{Any}(setfieldonce!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + elseif nargs == 5 + return Some{Any}(setfieldonce!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + else + return Some{Any}(setfieldonce!(getargs(args, frame)...)) + end elseif @static isdefined(Core, :setglobal!) && f === setglobal! if nargs == 3 return Some{Any}(setglobal!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) @@ -278,6 +348,16 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) else return Some{Any}(setglobal!(getargs(args, frame)...)) end + elseif @static isdefined(Core, :setglobalonce!) && f === setglobalonce! + if nargs == 3 + return Some{Any}(setglobalonce!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) + elseif nargs == 4 + return Some{Any}(setglobalonce!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + elseif nargs == 5 + return Some{Any}(setglobalonce!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + else + return Some{Any}(setglobalonce!(getargs(args, frame)...)) + end elseif @static isdefined(Core, :swapfield!) && f === swapfield! if nargs == 3 return Some{Any}(swapfield!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) @@ -286,6 +366,14 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) else return Some{Any}(swapfield!(getargs(args, frame)...)) end + elseif @static isdefined(Core, :swapglobal!) && f === swapglobal! + if nargs == 3 + return Some{Any}(swapglobal!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) + elseif nargs == 4 + return Some{Any}(swapglobal!(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + else + return Some{Any}(swapglobal!(getargs(args, frame)...)) + end elseif f === throw if nargs == 1 return Some{Any}(throw(@lookup(frame, args[2]))) @@ -320,6 +408,80 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool) call_expr.args[3] = @lookup(frame, args[3]) return Some{Any}(Core.eval(moduleof(frame), call_expr)) end + elseif @static (isdefined(Core, :arrayref) && Core.arrayref isa Core.Builtin) && f === Core.arrayref + if nargs == 1 + return Some{Any}(Core.arrayref(@lookup(frame, args[2]))) + elseif nargs == 2 + return Some{Any}(Core.arrayref(@lookup(frame, args[2]), @lookup(frame, args[3]))) + elseif nargs == 3 + return Some{Any}(Core.arrayref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) + elseif nargs == 4 + return Some{Any}(Core.arrayref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + elseif nargs == 5 + return Some{Any}(Core.arrayref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + else + return Some{Any}(Core.arrayref(getargs(args, frame)...)) + end + elseif @static (isdefined(Core, :arrayset) && Core.arrayset isa Core.Builtin) && f === Core.arrayset + if nargs == 1 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]))) + elseif nargs == 2 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]))) + elseif nargs == 3 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) + elseif nargs == 4 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + elseif nargs == 5 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + elseif nargs == 6 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]), @lookup(frame, args[7]))) + else + return Some{Any}(Core.arrayset(getargs(args, frame)...)) + end + elseif @static (isdefined(Core, :arrayset) && Core.arrayset isa Core.Builtin) && f === Core.arrayset + if nargs == 1 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]))) + elseif nargs == 2 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]))) + elseif nargs == 3 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) + elseif nargs == 4 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + elseif nargs == 5 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + elseif nargs == 6 + return Some{Any}(Core.arrayset(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]), @lookup(frame, args[7]))) + else + return Some{Any}(Core.arrayset(getargs(args, frame)...)) + end + elseif @static (isdefined(Core, :const_arrayref) && Core.const_arrayref isa Core.Builtin) && f === Core.const_arrayref + if nargs == 1 + return Some{Any}(Core.const_arrayref(@lookup(frame, args[2]))) + elseif nargs == 2 + return Some{Any}(Core.const_arrayref(@lookup(frame, args[2]), @lookup(frame, args[3]))) + elseif nargs == 3 + return Some{Any}(Core.const_arrayref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) + elseif nargs == 4 + return Some{Any}(Core.const_arrayref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + elseif nargs == 5 + return Some{Any}(Core.const_arrayref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + else + return Some{Any}(Core.const_arrayref(getargs(args, frame)...)) + end + elseif @static (isdefined(Core, :memoryref) && Core.memoryref isa Core.Builtin) && f === Core.memoryref + if nargs == 1 + return Some{Any}(Core.memoryref(@lookup(frame, args[2]))) + elseif nargs == 2 + return Some{Any}(Core.memoryref(@lookup(frame, args[2]), @lookup(frame, args[3]))) + elseif nargs == 3 + return Some{Any}(Core.memoryref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]))) + elseif nargs == 4 + return Some{Any}(Core.memoryref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]))) + elseif nargs == 5 + return Some{Any}(Core.memoryref(@lookup(frame, args[2]), @lookup(frame, args[3]), @lookup(frame, args[4]), @lookup(frame, args[5]), @lookup(frame, args[6]))) + else + return Some{Any}(Core.memoryref(getargs(args, frame)...)) + end elseif f === Core.Intrinsics.llvmcall return Some{Any}(Core.Intrinsics.llvmcall(getargs(args, frame)...)) end diff --git a/src/commands.jl b/src/commands.jl index 3d6b542..77ed110 100644 --- a/src/commands.jl +++ b/src/commands.jl @@ -119,7 +119,7 @@ maybe_next_until!(@nospecialize(predicate), frame::Frame, istoplevel::Bool=false pc = next_call!(frame, istoplevel=false) Execute the current statement. Continue stepping through `frame` until the next -`:return` or `:call` expression. +`ReturnNode` or `:call` expression. """ next_call!(@nospecialize(recurse), frame::Frame, istoplevel::Bool=false) = next_until!(frame -> is_call_or_return(pc_expr(frame)), recurse, frame, istoplevel) @@ -129,8 +129,8 @@ next_call!(frame::Frame, istoplevel::Bool=false) = next_call!(finish_and_return! pc = maybe_next_call!(recurse, frame, istoplevel=false) pc = maybe_next_call!(frame, istoplevel=false) -Return the current program counter of `frame` if it is a `:return` or `:call` expression. -Otherwise, step through the statements of `frame` until the next `:return` or `:call` expression. +Return the current program counter of `frame` if it is a `ReturnNode` or `:call` expression. +Otherwise, step through the statements of `frame` until the next `ReturnNode` or `:call` expression. """ maybe_next_call!(@nospecialize(recurse), frame::Frame, istoplevel::Bool=false) = maybe_next_until!(frame -> is_call_or_return(pc_expr(frame)), recurse, frame, istoplevel) @@ -214,7 +214,8 @@ which execution should start. """ function maybe_step_through_wrapper!(@nospecialize(recurse), frame::Frame) code = frame.framecode - stmts, scope = code.src.code, code.scope::Method + src = code.src + stmts, scope = src.code, code.scope::Method length(stmts) < 2 && return frame last = stmts[end-1] isexpr(last, :(=)) && (last = last.args[2]) @@ -225,13 +226,14 @@ function maybe_step_through_wrapper!(@nospecialize(recurse), frame::Frame) if unwrap1 isa DataType param1 = Base.unwrap_unionall(unwrap1.parameters[1]) if param1 isa DataType - is_kw = endswith(String(param1.name.name), "#kw") + is_kw = isdefined(Core, :kwcall) ? param1.name.name === Symbol("#kwcall") : + endswith(String(param1.name.name), "#kw") end end end has_selfarg = isexpr(last, :call) && any(@nospecialize(x) -> isa(x, SlotNumber) && x.id == 1, last.args) # isequal(SlotNumber(1)) vulnerable to invalidation - issplatcall, _callee = unpack_splatcall(last) + issplatcall, _callee = unpack_splatcall(last, src) if is_kw || has_selfarg || (issplatcall && is_bodyfunc(_callee)) # If the last expr calls #self# or passes it to an implementation method, # this is a wrapper function that we might want to step through @@ -253,6 +255,13 @@ function maybe_step_through_wrapper!(@nospecialize(recurse), frame::Frame) end maybe_step_through_wrapper!(frame::Frame) = maybe_step_through_wrapper!(finish_and_return!, frame) +if isdefined(Core, :kwcall) + const kwhandler = Core.kwcall + const kwextrastep = 0 +else + const kwhandler = Core.kwfunc + const kwextrastep = 1 +end """ frame = maybe_step_through_kwprep!(recurse, frame) @@ -262,24 +271,29 @@ If `frame.pc` points to the beginning of preparatory work for calling a keyword- function, advance forward until the actual call. """ function maybe_step_through_kwprep!(@nospecialize(recurse), frame::Frame, istoplevel::Bool=false) + # XXX This code just does pattern-matching based on the current state of the compiler + # internals, which means this is very fragile against any future changes to those + # internals. We really need a more general and robust solution, but achieving that + # would mean simplifying and unifying how "keyword function" is represented and + # implemented. For the time being, our best bet is to keep tweaking it as best as we can. pc, src = frame.pc, frame.framecode.src n = length(src.code) stmt = pc_expr(frame, pc) if isa(stmt, Tuple{Symbol,Vararg{Symbol}}) # Check to see if we're creating a NamedTuple followed by kwfunc call - pccall = pc + 5 + pccall = pc + 4 + kwextrastep if pccall <= n stmt1 = src.code[pc+1] # We deliberately check isexpr(stmt, :call) rather than is_call(stmt): if it's # assigned to a local, it's *not* kwarg preparation. if isexpr(stmt1, :call) && is_quotenode_egal(stmt1.args[1], Core.apply_type) && is_quoted_type(stmt1.args[2], :NamedTuple) stmt4, stmt5 = src.code[pc+4], src.code[pc+5] - if isexpr(stmt4, :call) && is_quotenode_egal(stmt4.args[1], Core.kwfunc) + if isexpr(stmt4, :call) && is_quotenode_egal(stmt4.args[1], kwhandler) while pc < pccall pc = step_expr!(recurse, frame, istoplevel) end return frame - elseif isexpr(stmt5, :call) && is_quotenode_egal(stmt5.args[1], Core.kwfunc) && pccall+1 <= n + elseif isexpr(stmt5, :call) && is_quotenode_egal(stmt5.args[1], kwhandler) && pccall+1 <= n # This happens when the call is scoped by a module pccall += 1 while pc < pccall @@ -300,7 +314,7 @@ function maybe_step_through_kwprep!(@nospecialize(recurse), frame::Frame, istopl # No supplied kwargs pcsplat = pc + 3 if pcsplat <= n - issplatcall, callee = unpack_splatcall(src.code[pcsplat]) + issplatcall, callee = unpack_splatcall(src.code[pcsplat], src) if issplatcall && is_bodyfunc(callee) while pc < pcsplat pc = step_expr!(recurse, frame, istoplevel) @@ -321,12 +335,12 @@ function maybe_step_through_kwprep!(@nospecialize(recurse), frame::Frame, istopl end elseif is_quotenode_egal(f, Base.merge) && ((pccall = pc + 7) <= n) stmtk = src.code[pccall-1] - if isexpr(stmtk, :call) && is_quotenode_egal(stmtk.args[1], Core.kwfunc) + if isexpr(stmtk, :call) && is_quotenode_egal(stmtk.args[1], kwhandler) for i = 1:4 pc = step_expr!(recurse, frame, istoplevel) end stmti = src.code[pc] - if isexpr(stmti, :call) && is_quotenode_egal(stmti.args[1], Core.kwfunc) + if isexpr(stmti, :call) && is_quotenode_egal(stmti.args[1], #= deliberately not kwhandler =# Core.kwfunc) pc = step_expr!(recurse, frame, istoplevel) end end @@ -378,7 +392,7 @@ maybe_reset_frame!(frame::Frame, @nospecialize(pc), rootistoplevel::Bool) = # Unwind the stack until an exc is eventually caught, thereby # returning the frame that caught the exception at the pc of the catch # or rethrow the error -function unwind_exception(frame::Frame, exc) +function unwind_exception(frame::Frame, @nospecialize(exc)) while frame !== nothing if !isempty(frame.framedata.exception_frames) # Exception caught diff --git a/src/construct.jl b/src/construct.jl index 6ebd5d1..cd56593 100644 --- a/src/construct.jl +++ b/src/construct.jl @@ -88,10 +88,18 @@ end get_source(meth::Method) = Base.uncompressed_ast(meth) -function get_source(g::GeneratedFunctionStub, env) - b = g(env..., g.argnames...) - b isa CodeInfo && return b - return eval(b) +@static if VERSION < v"1.10.0-DEV.873" # julia#48766 + function get_source(g::GeneratedFunctionStub, env, file, line) + b = g(env..., g.argnames...) + b isa CodeInfo && return b + return eval(b) + end +else + function get_source(g::GeneratedFunctionStub, env, file, line::Int) + b = g(Base.get_world_counter(), LineNumberNode(line, file), env..., g.argnames...) + b isa CodeInfo && return b + return eval(b) + end end """ @@ -108,17 +116,15 @@ keyword-sorter function for `fcall`. # Example ```jldoctest -julia> mymethod(x) = 1 -mymethod (generic function with 1 method) +julia> mymethod(x) = 1; -julia> mymethod(x, y; verbose=false) = nothing -mymethod (generic function with 2 methods) +julia> mymethod(x, y; verbose=false) = nothing; julia> JuliaInterpreter.prepare_args(mymethod, [mymethod, 15], ()) (mymethod, Any[mymethod, 15]) julia> JuliaInterpreter.prepare_args(mymethod, [mymethod, 1, 2], [:verbose=>true]) -(var"#mymethod##kw"(), Any[var"#mymethod##kw"(), (verbose = true,), mymethod, 1, 2]) +(Core.kwcall, Any[Core.kwcall, (verbose = true,), mymethod, 1, 2]) ``` """ function prepare_args(@nospecialize(f), allargs, kwargs) @@ -148,12 +154,12 @@ function prepare_framecode(method::Method, @nospecialize(argtypes); enter_genera # If we're stepping into a staged function, we need to use # the specialization, rather than stepping through the # unspecialized method. - code = Core.Compiler.get_staged(Core.Compiler.specialize_method(method, argtypes, lenv)) + code = get_staged(Core.Compiler.specialize_method(method, argtypes, lenv)) code === nothing && return nothing generator = false else if is_generated(method) - code = get_source(method.generator, lenv) + code = get_source(method.generator, lenv, method.file, Int(method.line)) generator = true else code = get_source(method) @@ -166,7 +172,7 @@ function prepare_framecode(method::Method, @nospecialize(argtypes); enter_genera if (!isempty(lenv) && (hasarg(isidentical(:llvmcall), code.code) || hasarg(isidentical(Base.llvmcall), code.code) || hasarg(a->is_global_ref(a, Base, :llvmcall), code.code))) || - hasarg(isidentical(:iolock_begin), code.code) + hasarg(isidentical(:iolock_begin), code.code) return Compiled() end framecode = FrameCode(method, code; generator=generator) @@ -207,8 +213,7 @@ this will be the types of `allargs`); # Example ```jldoctest -julia> mymethod(x::Vector{T}) where T = 1 -mymethod (generic function with 1 method) +julia> mymethod(x::Vector{T}) where T = 1; julia> framecode, frameargs, lenv, argtypes = JuliaInterpreter.prepare_call(mymethod, [mymethod, [1.0,2.0]]); @@ -264,7 +269,7 @@ function prepare_framedata(framecode, argvals::Vector{Any}, lenv::SimpleVector=e if length(junk_framedata) > 0 olddata = pop!(junk_framedata) locals, ssavalues, sparams = olddata.locals, olddata.ssavalues, olddata.sparams - exception_frames, last_reference = olddata.exception_frames, olddata.last_reference + exception_frames, current_scopes, last_reference = olddata.exception_frames, olddata.current_scopes, olddata.last_reference last_exception = olddata.last_exception callargs = olddata.callargs resize!(locals, ns) @@ -274,6 +279,7 @@ function prepare_framedata(framecode, argvals::Vector{Any}, lenv::SimpleVector=e # for check_isdefined to work properly, we need sparams to start out unassigned resize!(sparams, 0) empty!(exception_frames) + empty!(current_scopes) resize!(last_reference, ns) last_exception[] = _INACTIVE_EXCEPTION.instance else @@ -281,6 +287,7 @@ function prepare_framedata(framecode, argvals::Vector{Any}, lenv::SimpleVector=e ssavalues = Vector{Any}(undef, ng) sparams = Vector{Any}(undef, 0) exception_frames = Int[] + current_scopes = Scope[] last_reference = Vector{Int}(undef, ns) callargs = Any[] last_exception = Ref{Any}(_INACTIVE_EXCEPTION.instance) @@ -309,7 +316,8 @@ function prepare_framedata(framecode, argvals::Vector{Any}, lenv::SimpleVector=e isa(T, TypeVar) && continue # only fill concrete types sparams[i] = T end - FrameData(locals, ssavalues, sparams, exception_frames, last_exception, caller_will_catch_err, last_reference, callargs) + return FrameData(locals, ssavalues, sparams, exception_frames, current_scopes, + last_exception, caller_will_catch_err, last_reference, callargs) end """ @@ -326,6 +334,7 @@ end function prepare_frame_caller(caller::Frame, framecode::FrameCode, args::Vector{Any}, lenv::SimpleVector) caller_will_catch_err = !isempty(caller.framedata.exception_frames) || caller.framedata.caller_will_catch_err caller.callee = frame = prepare_frame(framecode, args, lenv, caller_will_catch_err) + copy!(frame.framedata.current_scopes, caller.framedata.current_scopes) frame.caller = caller return frame end @@ -333,14 +342,17 @@ end """ ExprSplitter(mod::Module, ex::Expr; lnn=nothing) -Create an iterable that returns individual expressions together with their module of evaluation. +Given a module `mod` and a top-level expression `ex` in `mod`, create an iterable that returns +individual expressions together with their module of evaluation. Optionally supply an initial `LineNumberNode` `lnn`. # Example +In a fresh session, + ``` julia> expr = quote - public(x::Integer) = true + public_fn(x::Integer) = true module Private private(y::String) = false end @@ -353,7 +365,7 @@ julia> for (mod, ex) in ExprSplitter(Main, expr) mod = Main ex = quote #= REPL[7]:2 =# - public(x::Integer) = begin + public_fn(x::Integer) = begin #= REPL[7]:2 =# true end @@ -370,13 +382,13 @@ mod = Main ex = :($(Expr(:toplevel, :(#= REPL[7]:6 =#), :(const threshold = 0.1)))) ``` -Note that `Main.Private` was created for you so that its internal expressions could be evaluated. +`ExprSplitter` created `Main.Private` was created for you so that its internal expressions could be evaluated. `ExprSplitter` will check to see whether the module already exists and if so return it rather than try to create a new module with the same name. In general each returned expression is a block with two parts: a `LineNumberNode` followed by a single expression. In some cases the returned expression may be `:toplevel`, as shown in the `const` declaration, -but otherwise it will be a `:block`. +but otherwise it will preserve its parent's `head` (e.g., `expr.head`). # World age, frame creation, and evaluation @@ -403,7 +415,7 @@ julia> for (mod, ex) in ExprSplitter(Main, expr) julia> threshold 0.1 -julia> public(3) +julia> public_fn(3) true ``` @@ -436,7 +448,7 @@ function push_modex!(iter::ExprSplitter, mod::Module, ex::Expr) modifies_scope = false if ex.head === :block for a in ex.args - if isa(a, Expr) && a.head ∈ (:local, :global) + if isa(a, Expr) && a.head === :local modifies_scope = true break end @@ -580,17 +592,15 @@ would be created by the generator. # Example ```jldoctest -julia> mymethod(x) = x+1 -mymethod (generic function with 1 method) +julia> mymethod(x) = x+1; julia> JuliaInterpreter.enter_call_expr(:(\$mymethod(1))) -Frame for mymethod(x) in Main at none:1 +Frame for mymethod(x) @ Main none:1 1* 1 1 ─ %1 = x + 1 2 1 └── return %1 x = 1 -julia> mymethod(x::Vector{T}) where T = 1 -mymethod (generic function with 2 methods) +julia> mymethod(x::Vector{T}) where T = 1; julia> a = [1.0, 2.0] 2-element Vector{Float64}: @@ -598,7 +608,7 @@ julia> a = [1.0, 2.0] 2.0 julia> JuliaInterpreter.enter_call_expr(:(\$mymethod(\$a))) -Frame for mymethod(x::Vector{T}) where T in Main at none:1 +Frame for mymethod(x::Vector{T}) where T @ Main none:1 1* 1 1 ─ return 1 x = [1.0, 2.0] T = Float64 @@ -623,20 +633,18 @@ Build a `Frame` ready to execute `f` with the specified positional and keyword a # Example ```jldoctest -julia> mymethod(x) = x+1 -mymethod (generic function with 1 method) +julia> mymethod(x) = x+1; julia> JuliaInterpreter.enter_call(mymethod, 1) -Frame for mymethod(x) in Main at none:1 +Frame for mymethod(x) @ Main none:1 1* 1 1 ─ %1 = x + 1 2 1 └── return %1 x = 1 -julia> mymethod(x::Vector{T}) where T = 1 -mymethod (generic function with 2 methods) +julia> mymethod(x::Vector{T}) where T = 1; julia> JuliaInterpreter.enter_call(mymethod, [1.0, 2.0]) -Frame for mymethod(x::Vector{T}) where T in Main at none:1 +Frame for mymethod(x::Vector{T}) where T @ Main none:1 1* 1 1 ─ return 1 x = [1.0, 2.0] T = Float64 @@ -721,10 +729,7 @@ Evaluate `f` on the specified arguments using the interpreter. # Example ```jldoctest -julia> a = [1, 7] -2-element Vector{Int64}: - 1 - 7 +julia> a = [1, 7]; julia> sum(a) 8 diff --git a/src/interpret.jl b/src/interpret.jl index 250f634..26afb80 100644 --- a/src/interpret.jl +++ b/src/interpret.jl @@ -9,22 +9,6 @@ function lookup_var(frame, slot::SlotNumber) throw(UndefVarError(frame.framecode.src.slotnames[slot.id])) end -function lookup_expr(frame, e::Expr) - head = e.head - head === :the_exception && return frame.framedata.last_exception[] - if head === :static_parameter - arg = e.args[1]::Int - if isassigned(frame.framedata.sparams, arg) - return frame.framedata.sparams[arg] - else - syms = sparam_syms(frame.framecode.scope::Method) - throw(UndefVarError(syms[arg])) - end - end - head === :boundscheck && length(e.args) == 0 && return true - error("invalid lookup expr ", e) -end - """ rhs = @lookup(frame, node) rhs = @lookup(mod, frame, node) @@ -67,6 +51,32 @@ macro lookup(args...) end end +function lookup_expr(frame, e::Expr) + head = e.head + head === :the_exception && return frame.framedata.last_exception[] + if head === :static_parameter + arg = e.args[1]::Int + if isassigned(frame.framedata.sparams, arg) + return frame.framedata.sparams[arg] + else + syms = sparam_syms(frame.framecode.scope::Method) + throw(UndefVarError(syms[arg])) + end + end + head === :boundscheck && length(e.args) == 0 && return true + if head === :call + f = @lookup frame e.args[1] + if (@static VERSION < v"1.11.0-DEV.1180" && true) && f === Core.svec + # work around for a linearization bug in Julia (https://github.com/JuliaLang/julia/pull/52497) + return f(Any[@lookup(frame, e.args[i]) for i in 2:length(e.args)]...) + elseif f === Core.tuple + # handling for ccall literal syntax + return f(Any[@lookup(frame, e.args[i]) for i in 2:length(e.args)]...) + end + end + error("invalid lookup expr ", e) +end + # This is used only for new struct/abstract/primitive nodes. # The most important issue is that in these expressions, :call Exprs can be nested, # and hence our re-use of the `callargs` field of Frame would introduce @@ -91,18 +101,26 @@ function lookup_or_eval(@nospecialize(recurse), frame, @nospecialize(node)) if ex.head === :call f = ex.args[1] if f === Core.svec - return Core.svec(ex.args[2:end]...) + popfirst!(ex.args) + return Core.svec(ex.args...) elseif f === Core.apply_type - return Core.apply_type(ex.args[2:end]...) - elseif f === Core.typeof - return Core.typeof(ex.args[2]) - elseif f === Base.getproperty + popfirst!(ex.args) + return Core.apply_type(ex.args...) + elseif f === typeof && length(ex.args) == 2 + return typeof(ex.args[2]) + elseif f === typeassert && length(ex.args) == 3 + return typeassert(ex.args[2], ex.args[3]) + elseif f === Base.getproperty && length(ex.args) == 3 return Base.getproperty(ex.args[2], ex.args[3]) + elseif f === Core.Compiler.Val && length(ex.args) == 2 + return Core.Compiler.Val(ex.args[2]) + elseif f === Val && length(ex.args) == 2 + return Val(ex.args[2]) else - error("unknown call f ", f) + Base.invokelatest(error, "unknown call f introduced by ccall lowering ", f) end else - error("unknown expr ", ex) + return lookup_expr(frame, ex) end elseif isa(node, Int) || isa(node, Number) # Number is slow, requires subtyping return node @@ -129,7 +147,7 @@ function resolvefc(frame, @nospecialize(expr)) (isa(a, QuoteNode) && a.value === Core.tuple) || error("unexpected ccall to ", expr) return Expr(:call, GlobalRef(Core, :tuple), (expr::Expr).args[2:end]...) end - error("unexpected ccall to ", expr) + Base.invokelatest(error, "unexpected ccall to ", expr) end function collect_args(@nospecialize(recurse), frame::Frame, call_expr::Expr; isfc::Bool=false) @@ -204,6 +222,21 @@ function bypass_builtins(@nospecialize(recurse), frame, call_expr, pc) return nothing end +function native_call(fargs::Vector{Any}, frame::Frame) + f = popfirst!(fargs) # now it's really just `args` + if (@static isdefined(Core.IR, :EnterNode) && true) + newscope = Core.current_scope() + if newscope !== nothing || !isempty(frame.framedata.current_scopes) + for scope in frame.framedata.current_scopes + newscope = Scope(newscope, scope.values...) + end + ex = Expr(:tryfinally, :($f($fargs...)), nothing, newscope) + return Core.eval(moduleof(frame), ex) + end + end + return Base.invokelatest(f, fargs...) +end + function evaluate_call_compiled!(::Compiled, frame::Frame, call_expr::Expr; enter_generated::Bool=false) # @assert !enter_generated pc = frame.pc @@ -212,9 +245,7 @@ function evaluate_call_compiled!(::Compiled, frame::Frame, call_expr::Expr; ente ret = maybe_evaluate_builtin(frame, call_expr, false) isa(ret, Some{Any}) && return ret.value fargs = collect_args(Compiled(), frame, call_expr) - f = fargs[1] - popfirst!(fargs) # now it's really just `args` - return f(fargs...) + return native_call(fargs, frame) end function evaluate_call_recurse!(@nospecialize(recurse), frame::Frame, call_expr::Expr; enter_generated::Bool=false) @@ -245,8 +276,7 @@ function evaluate_call_recurse!(@nospecialize(recurse), frame::Frame, call_expr: framecode, lenv = get_call_framecode(fargs, frame.framecode, frame.pc; enter_generated=enter_generated) if lenv === nothing if isa(framecode, Compiled) - f = popfirst!(fargs) # now it's really just `args` - return Base.invokelatest(f, fargs...) + return native_call(fargs, frame) end return framecode # this was a Builtin end @@ -283,24 +313,23 @@ evaluate_call!(frame::Frame, call_expr::Expr; kwargs...) = evaluate_call!(finish # The following come up only when evaluating toplevel code function evaluate_methoddef(frame, node) f = node.args[1] - if isa(f, Symbol) - mod = moduleof(frame) - if Base.isbindingresolved(mod, f) && isdefined(mod, f) # `isdefined` accesses the binding, making it impossible to create a new one - f = getfield(mod, f) + if f isa Symbol || f isa GlobalRef + mod = f isa Symbol ? moduleof(frame) : f.mod + name = f isa Symbol ? f : f.name + if Base.isbindingresolved(mod, name) && isdefined(mod, name) # `isdefined` accesses the binding, making it impossible to create a new one + f = getfield(mod, name) else - f = Core.eval(moduleof(frame), Expr(:function, f)) # create a new function + f = Core.eval(mod, Expr(:function, name)) # create a new function end - elseif isa(f, GlobalRef) - f = getfield(f.mod, f.name) end length(node.args) == 1 && return f sig = @lookup(frame, node.args[2])::SimpleVector - body = @lookup(frame, node.args[3]) + body = @lookup(frame, node.args[3])::Union{CodeInfo, Expr} # branching on https://github.com/JuliaLang/julia/pull/41137 @static if isdefined(Core.Compiler, :OverlayMethodTable) - ccall(:jl_method_def, Cvoid, (Any, Ptr{Cvoid}, Any, Any), sig, C_NULL, body, moduleof(frame)) + ccall(:jl_method_def, Cvoid, (Any, Ptr{Cvoid}, Any, Any), sig, C_NULL, body, moduleof(frame)::Module) else - ccall(:jl_method_def, Cvoid, (Any, Any, Any), sig, body, moduleof(frame)) + ccall(:jl_method_def, Cvoid, (Any, Any, Any), sig, body, moduleof(frame)::Module) end return f end @@ -318,8 +347,8 @@ function structname(frame, node) end function set_structtype_const(mod::Module, name::Symbol) - dt = Base.unwrap_unionall(getfield(mod, name)) - ccall(:jl_set_const, Cvoid, (Any, Any, Any), mod, dt.name.name, dt.name.wrapper) + dt = Base.unwrap_unionall(getfield(mod, name))::DataType + ccall(:jl_set_const, Cvoid, (Any, Any, Any), mod, dt.name.name::Symbol, dt.name.wrapper) end function inplace_lookup!(ex, i, frame) @@ -342,17 +371,14 @@ function do_assignment!(frame, @nospecialize(lhs), @nospecialize(rhs)) counter = (frame.assignment_counter += 1) data.locals[lhs.id] = Some{Any}(rhs) data.last_reference[lhs.id] = counter - elseif isa(lhs, GlobalRef) - @static if @isdefined setglobal! - setglobal!(lhs.mod, lhs.name, rhs) - else - ccall(:jl_set_global, Cvoid, (Any, Any, Any), lhs.mod, lhs.name, rhs) - end - elseif isa(lhs, Symbol) + elseif isa(lhs, Symbol) || isa(lhs, GlobalRef) + mod = lhs isa Symbol ? moduleof(frame) : lhs.mod + name = lhs isa Symbol ? lhs : lhs.name + Core.eval(mod, Expr(:global, name)) @static if @isdefined setglobal! - setglobal!(moduleof(code), lhs, rhs) + setglobal!(mod, name, rhs) else - ccall(:jl_set_global, Cvoid, (Any, Any, Any), moduleof(code), lhs, rhs) + ccall(:jl_set_global, Cvoid, (Any, Any, Any), mod, name, rhs) end end end @@ -370,7 +396,6 @@ function maybe_assign!(frame, @nospecialize(stmt), @nospecialize(val)) end maybe_assign!(frame, @nospecialize(val)) = maybe_assign!(frame, pc_expr(frame), val) - function eval_rhs(@nospecialize(recurse), frame, node::Expr) head = node.head if head === :new @@ -378,12 +403,14 @@ function eval_rhs(@nospecialize(recurse), frame, node::Expr) args = let mod=mod Any[@lookup(mod, frame, arg) for arg in node.args] end - T = popfirst!(args) + T = popfirst!(args)::DataType rhs = ccall(:jl_new_structv, Any, (Any, Ptr{Any}, UInt32), T, args, length(args)) return rhs elseif head === :splatnew # Julia 1.2+ mod = moduleof(frame) - rhs = ccall(:jl_new_structt, Any, (Any, Any), @lookup(mod, frame, node.args[1]), @lookup(mod, frame, node.args[2])) + T = @lookup(mod, frame, node.args[1])::DataType + args = @lookup(mod, frame, node.args[2])::Tuple + rhs = ccall(:jl_new_structt, Any, (Any, Any), T, args) return rhs elseif head === :isdefined return check_isdefined(frame, node.args[1]) @@ -396,12 +423,11 @@ function eval_rhs(@nospecialize(recurse), frame, node::Expr) elseif head === :copyast val = (node.args[1]::QuoteNode).value return isa(val, Expr) ? copy(val) : val - elseif head === :enter - return length(frame.framedata.exception_frames) elseif head === :boundscheck return true elseif head === :meta || head === :inbounds || head === :loopinfo || - head === :gc_preserve_begin || head === :gc_preserve_end + head === :gc_preserve_begin || head === :gc_preserve_end || + head === :aliasscope || head === :popaliasscope return nothing elseif head === :method && length(node.args) == 1 return evaluate_methoddef(frame, node) @@ -430,14 +456,25 @@ function coverage_visit_line!(frame::Frame) pc, code = frame.pc, frame.framecode code.report_coverage || return src = code.src + @static if VERSION ≥ v"1.12.0-DEV.173" + lineinfo = linetable(src, pc) + file, line = lineinfo.file, lineinfo.line + if line != frame.last_codeloc + file isa Symbol || (file = Symbol(file)::Symbol) + @ccall jl_coverage_visit_line(file::Cstring, sizeof(file)::Csize_t, line::Cint)::Cvoid + frame.last_codeloc = line + end + else # VERSION < v"1.12.0-DEV.173" codeloc = src.codelocs[pc] - if codeloc != frame.last_codeloc + if codeloc != frame.last_codeloc && codeloc != 0 linetable = src.linetable::Vector{Any} lineinfo = linetable[codeloc]::Core.LineInfoNode - file, line = String(lineinfo.file), lineinfo.line - ccall(:jl_coverage_visit_line, Cvoid, (Cstring, Csize_t, Cint), file, sizeof(file), line) + file, line = lineinfo.file, lineinfo.line + file isa Symbol || (file = Symbol(file)::Symbol) + @ccall jl_coverage_visit_line(file::Cstring, sizeof(file)::Csize_t, line::Cint)::Cvoid frame.last_codeloc = codeloc end + end # @static if end # For "profiling" where JuliaInterpreter spends its time. See the commented-out block @@ -470,26 +507,30 @@ function step_expr!(@nospecialize(recurse), frame, @nospecialize(node), istoplev end isa(rhs, BreakpointRef) && return rhs do_assignment!(frame, lhs, rhs) - elseif node.head === :gotoifnot - arg = @lookup(frame, node.args[1]) - if !isa(arg, Bool) - throw(TypeError(nameof(frame), "if", Bool, arg)) - end - if !arg - return (frame.pc = node.args[2]::Int) - end elseif node.head === :enter rhs = node.args[1]::Int push!(data.exception_frames, rhs) elseif node.head === :leave - for _ = 1:node.args[1]::Int - pop!(data.exception_frames) + if length(node.args) == 1 && isa(node.args[1], Int) + arg = node.args[1]::Int + for _ = 1:arg + pop!(data.exception_frames) + end + else + for i = 1:length(node.args) + targ = node.args[i] + targ === nothing && continue + enterstmt = frame.framecode.src.code[(targ::SSAValue).id] + enterstmt === nothing && continue + pop!(data.exception_frames) + if isdefined(enterstmt, :scope) + pop!(data.current_scopes) + end + end end elseif node.head === :pop_exception - n = lookup_var(frame, node.args[1]::SSAValue)::Int - deleteat!(data.exception_frames, n+1:length(data.exception_frames)) - elseif node.head === :return - return nothing + # TODO: This needs to handle the exception stack properly + # (https://github.com/JuliaDebug/JuliaInterpreter.jl/issues/591) elseif istoplevel if node.head === :method && length(node.args) > 1 evaluate_methoddef(frame, node) @@ -547,8 +588,7 @@ function step_expr!(@nospecialize(recurse), frame, @nospecialize(node), istoplev end elseif isa(node, GotoNode) return (frame.pc = node.label) - elseif is_GotoIfNot(node) - node = node::Core.GotoIfNot + elseif isa(node, GotoIfNot) arg = @lookup(frame, node.cond) if !isa(arg, Bool) throw(TypeError(nameof(frame), "if", Bool, arg)) @@ -556,13 +596,19 @@ function step_expr!(@nospecialize(recurse), frame, @nospecialize(node), istoplev if !arg return (frame.pc = node.dest) end - elseif is_ReturnNode(node) + elseif isa(node, ReturnNode) return nothing elseif isa(node, NewvarNode) # FIXME: undefine the slot? elseif istoplevel && isa(node, LineNumberNode) elseif istoplevel && isa(node, Symbol) rhs = getfield(moduleof(frame), node) + elseif @static (isdefined(Core.IR, :EnterNode) && true) && isa(node, Core.IR.EnterNode) + rhs = node.catch_dest + push!(data.exception_frames, rhs) + if isdefined(node, :scope) + push!(data.current_scopes, @lookup(frame, node.scope)) + end else rhs = @lookup(frame, node) end @@ -633,14 +679,12 @@ function handle_err(@nospecialize(recurse), frame, err) rethrow(err) end data.last_exception[] = err - return (frame.pc = data.exception_frames[end]) + pc = @static VERSION >= v"1.11-" ? pop!(data.exception_frames) : data.exception_frames[end] # implicit :leave after https://github.com/JuliaLang/julia/pull/52245 + frame.pc = pc + return pc end -if isdefined(Core, :ReturnNode) - lookup_return(frame, node::Core.ReturnNode) = @lookup(frame, node.val) -else - lookup_return(frame, node::Expr) = @lookup(frame, node.args[1]) -end +lookup_return(frame, node::ReturnNode) = @lookup(frame, node.val) """ ret = get_return(frame) @@ -651,7 +695,7 @@ e.g., [`JuliaInterpreter.finish!`](@ref)). """ function get_return(frame) node = pc_expr(frame) - is_return(node) || error("expected return statement, got ", node) + is_return(node) || Base.invokelatest(error, "expected return statement, got ", node) return lookup_return(frame, node) end get_return(t::Tuple{Module,Expr,Frame}) = get_return(t[end]) diff --git a/src/optimize.jl b/src/optimize.jl index 7a2dda2..2ae9871 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -1,100 +1,12 @@ -const calllike = (:call, :foreigncall) - const compiled_calls = Dict{Any,Any}() -function extract_inner_call!(stmt::Expr, idx, once::Bool=false) - (stmt.head === :toplevel || stmt.head === :thunk) && return nothing - once |= stmt.head ∈ calllike - for (i, a) in enumerate(stmt.args) - isa(a, Expr) || continue - # Make sure we don't "damage" special syntax that requires literals - if i == 1 && stmt.head === :foreigncall - continue - end - if i == 2 && stmt.head === :call && stmt.args[1] === :cglobal - continue - end - ret = extract_inner_call!(a, idx, once) # doing this first extracts innermost calls - ret !== nothing && return ret - iscalllike = a.head ∈ calllike - if once && iscalllike - stmt.args[i] = NewSSAValue(idx) - return a - end - end - return nothing -end - -function replace_ssa(@nospecialize(stmt), ssalookup) - isa(stmt, Expr) || return stmt - return Expr(stmt.head, Any[ - if isa(a, SSAValue) - SSAValue(ssalookup[a.id]) - elseif isa(a, NewSSAValue) - SSAValue(a.id) - else - replace_ssa(a, ssalookup) - end - for a in stmt.args - ]...) -end - -function renumber_ssa!(stmts::Vector{Any}, ssalookup) - # When updating jumps, when lines get split into multiple lines - # (see "Un-nest :call expressions" below), we need to jump to the first of them. - # Consequently we use the previous "old-code" offset and add one. - # Fixes #455. - jumplookup(l, idx) = idx > 1 ? l[idx-1] + 1 : idx - - for (i, stmt) in enumerate(stmts) - if isa(stmt, GotoNode) - stmts[i] = GotoNode(jumplookup(ssalookup, stmt.label)) - elseif isa(stmt, SSAValue) - stmts[i] = SSAValue(ssalookup[stmt.id]) - elseif isa(stmt, NewSSAValue) - stmts[i] = SSAValue(stmt.id) - elseif isa(stmt, Expr) - stmt = replace_ssa(stmt, ssalookup) - if (stmt.head === :gotoifnot || stmt.head === :enter) && isa(stmt.args[end], Int) - stmt.args[end] = jumplookup(ssalookup, stmt.args[end]) - end - stmts[i] = stmt - elseif is_GotoIfNot(stmt) - cond = (stmt::Core.GotoIfNot).cond - if isa(cond, SSAValue) - cond = SSAValue(ssalookup[cond.id]) - end - stmts[i] = Core.GotoIfNot(cond, jumplookup(ssalookup, stmt.dest)) - elseif is_ReturnNode(stmt) - val = (stmt::Core.ReturnNode).val - if isa(val, SSAValue) - stmts[i] = Core.ReturnNode(SSAValue(ssalookup[val.id])) - end - end - end - return stmts -end - -function compute_ssa_mapping_delete_statements!(code::CodeInfo, stmts::Vector{Int}) - stmts = unique!(sort!(stmts)) - ssalookup = collect(1:length(codelocs(code))) - cnt = 1 - for i in 1:length(stmts) - start = stmts[i] + 1 - stop = i == length(stmts) ? length(codelocs(code)) : stmts[i+1] - ssalookup[start:stop] .-= cnt - cnt += 1 - end - return ssalookup -end - # Pre-frame-construction lookup -function lookup_stmt(stmts, arg) +function lookup_stmt(stmts::Vector{Any}, @nospecialize arg) if isa(arg, SSAValue) arg = stmts[arg.id] end if isa(arg, QuoteNode) - arg = arg.value + return arg.value end return arg end @@ -112,16 +24,16 @@ function smallest_ref(stmts, arg, idmin) end function lookup_global_ref(a::GlobalRef) - if isdefined(a.mod, a.name) && isconst(a.mod, a.name) - r = getfield(a.mod, a.name) - return QuoteNode(r) - else - return a + if Base.isbindingresolved(a.mod, a.name) && isdefined(a.mod, a.name) + return QuoteNode(getfield(a.mod, a.name)) end + return a end function lookup_global_refs!(ex::Expr) - (ex.head === :isdefined || ex.head === :thunk || ex.head === :toplevel) && return nothing + if isexpr(ex, (:isdefined, :thunk, :toplevel, :method, :global, :const)) + return nothing + end for (i, a) in enumerate(ex.args) ex.head === :(=) && i == 1 && continue # Don't look up globalrefs on the LHS of an assignment (issue #98) if isa(a, GlobalRef) @@ -133,16 +45,27 @@ function lookup_global_refs!(ex::Expr) return nothing end -function lookup_getproperties(a::Expr) - if a.head === :call && length(a.args) == 3 && - a.args[1] isa QuoteNode && a.args[1].value === Base.getproperty && - a.args[2] isa QuoteNode && a.args[2].value isa Module && - a.args[3] isa QuoteNode && a.args[3].value isa Symbol - return lookup_global_ref(Core.GlobalRef(a.args[2].value, a.args[3].value)) - end - return a +function lookup_getproperties(code::Vector{Any}, @nospecialize a) + isexpr(a, :call) || return a + length(a.args) == 3 || return a + arg1 = lookup_stmt(code, a.args[1]) + arg1 === Base.getproperty || return a + arg2 = lookup_stmt(code, a.args[2]) + arg2 isa Module || return a + arg3 = lookup_stmt(code, a.args[3]) + arg3 isa Symbol || return a + return lookup_global_ref(GlobalRef(arg2, arg3)) end +# HACK This isn't optimization really, but necessary to bypass llvmcall and foreigncall +# TODO This "optimization" should be refactored into a "minimum compilation" necessary to +# execute `llvmcall` and `foreigncall` and pure optimizations on the lowered code representation. +# In particular, the optimization that replaces `GlobalRef` with `QuoteNode` is invalid and +# should be removed: This is because it is not possible to know when and where the binding +# will be resolved without executing the code. +# Since the current `build_compiled_[llvmcall|foreigncall]!` relies on this replacement, +# they also need to be reimplemented. + """ optimize!(code::CodeInfo, mod::Module) @@ -156,8 +79,8 @@ function optimize!(code::CodeInfo, scope) mod = moduleof(scope) evalmod = mod == Core.Compiler ? Core.Compiler : CompiledCalls sparams = scope isa Method ? sparam_syms(scope) : Symbol[] - code.inferred && error("optimization of inferred code not implemented") replace_coretypes!(code) + # TODO: because of builtins.jl, for CodeInfos like # %1 = Core.apply_type # %2 = (%1)(args...) @@ -171,14 +94,15 @@ function optimize!(code::CodeInfo, scope) continue else lookup_global_refs!(stmt) - code.code[i] = lookup_getproperties(stmt) + code.code[i] = lookup_getproperties(code.code, stmt) end end end # Replace :llvmcall and :foreigncall with compiled variants. See # https://github.com/JuliaDebug/JuliaInterpreter.jl/issues/13#issuecomment-464880123 - foreigncalls_idx = Int[] + # Insert the foreigncall wrappers at the updated idxs + methodtables = Vector{Union{Compiled,DispatchableMethod}}(undef, length(code.code)) for (idx, stmt) in enumerate(code.code) # Foregincalls can be rhs of assignments if isexpr(stmt, :(=)) @@ -191,45 +115,14 @@ function optimize!(code::CodeInfo, scope) if (arg1 === :llvmcall || lookup_stmt(code.code, arg1) === Base.llvmcall) && isempty(sparams) && scope isa Method # Call via `invokelatest` to avoid compiling it until we need it Base.invokelatest(build_compiled_llvmcall!, stmt, code, idx, evalmod) - push!(foreigncalls_idx, idx) + methodtables[idx] = Compiled() end elseif stmt.head === :foreigncall && scope isa Method # Call via `invokelatest` to avoid compiling it until we need it Base.invokelatest(build_compiled_foreigncall!, stmt, code, sparams, evalmod) - push!(foreigncalls_idx, idx) - end - end - end - - ## Un-nest :call expressions (so that there will be only one :call per line) - # This will allow us to re-use args-buffers rather than having to allocate new ones each time. - old_code, old_codelocs = code.code, codelocs(code) - code.code = new_code = eltype(old_code)[] - code.codelocs = new_codelocs = Int32[] - ssainc = fill(1, length(old_code)) - for (i, stmt) in enumerate(old_code) - loc = old_codelocs[i] - if isa(stmt, Expr) - inner = extract_inner_call!(stmt, length(new_code)+1) - while inner !== nothing - push!(new_code, inner) - push!(new_codelocs, loc) - ssainc[i] += 1 - inner = extract_inner_call!(stmt, length(new_code)+1) + methodtables[idx] = Compiled() end end - push!(new_code, stmt) - push!(new_codelocs, loc) - end - # Fix all the SSAValues and GotoNodes - ssalookup = cumsum(ssainc) - renumber_ssa!(new_code, ssalookup) - code.ssavaluetypes = length(new_code) - - # Insert the foreigncall wrappers at the updated idxs - methodtables = Vector{Union{Compiled,DispatchableMethod}}(undef, length(code.code)) - for idx in foreigncalls_idx - methodtables[ssalookup[idx]] = Compiled() end return code, methodtables @@ -254,7 +147,7 @@ function parametric_type_to_expr(@nospecialize(t::Type)) return t end -function build_compiled_llvmcall!(stmt::Expr, code, idx, evalmod) +function build_compiled_llvmcall!(stmt::Expr, code::CodeInfo, idx::Int, evalmod::Module) # Run a mini-interpreter to extract the types framecode = FrameCode(CompiledCalls, code; optimize=false) frame = Frame(framecode, prepare_framedata(framecode, [])) @@ -291,9 +184,8 @@ function build_compiled_llvmcall!(stmt::Expr, code, idx, evalmod) append!(stmt.args, args) end - # Handle :llvmcall & :foreigncall (issue #28) -function build_compiled_foreigncall!(stmt::Expr, code, sparams::Vector{Symbol}, evalmod) +function build_compiled_foreigncall!(stmt::Expr, code::CodeInfo, sparams::Vector{Symbol}, evalmod::Module) TVal = evalmod == Core.Compiler ? Core.Compiler.Val : Val cfunc, RetType, ArgType = lookup_stmt(code.code, stmt.args[1]), stmt.args[2], stmt.args[3]::SimpleVector @@ -360,7 +252,7 @@ function build_compiled_foreigncall!(stmt::Expr, code, sparams::Vector{Symbol}, return nothing end -function replace_coretypes!(src; rev::Bool=false) +function replace_coretypes!(@nospecialize(src); rev::Bool=false) if isa(src, CodeInfo) replace_coretypes_list!(src.code; rev=rev) elseif isa(src, Expr) @@ -369,41 +261,48 @@ function replace_coretypes!(src; rev::Bool=false) return src end -function replace_coretypes_list!(list::AbstractVector; rev::Bool) +function replace_coretypes_list!(list::AbstractVector; rev::Bool=false) function rep(@nospecialize(x), rev::Bool) if rev - if isa(x, SSAValue) - return Core.SSAValue(x.id) - elseif isa(x, SlotNumber) - return Core.SlotNumber(x.id) + isa(x, SSAValue) && return Core.SSAValue(x.id) + isa(x, SlotNumber) && return Core.SlotNumber(x.id) + return x + else + isa(x, Core.SSAValue) && return SSAValue(x.id) + isa(x, Core.SlotNumber) && return SlotNumber(x.id) + @static if VERSION < v"1.11.0-DEV.337" + @static if VERSION ≥ v"1.10.0-DEV.631" + isa(x, Core.Compiler.TypedSlot) && return SlotNumber(x.id) + else + isa(x, Core.TypedSlot) && return SlotNumber(x.id) + end end return x end - if isa(x, Core.SSAValue) - return SSAValue(x.id) - elseif isa(x, Core.SlotNumber) || isa(x, Core.TypedSlot) - return SlotNumber(x.id) - end - return x end for (i, stmt) in enumerate(list) rstmt = rep(stmt, rev) if rstmt !== stmt list[i] = rstmt - elseif is_GotoIfNot(stmt) - stmt = stmt::Core.GotoIfNot + elseif isa(stmt, GotoIfNot) cond = stmt.cond rcond = rep(cond, rev) if rcond !== cond - list[i] = Core.GotoIfNot(rcond, stmt.dest) + list[i] = GotoIfNot(rcond, stmt.dest) end - elseif is_ReturnNode(stmt) - stmt = stmt::Core.ReturnNode + elseif isa(stmt, ReturnNode) val = stmt.val rval = rep(val, rev) if rval !== val - list[i] = Core.ReturnNode(rval) + list[i] = ReturnNode(rval) + end + elseif @static (isdefined(Core.IR, :EnterNode) && true) && isa(stmt, Core.IR.EnterNode) + if isdefined(stmt, :scope) + rscope = rep(stmt.scope, rev) + if rscope !== stmt.scope + list[i] = Core.IR.EnterNode(stmt.catch_dest, rscope) + end end elseif isa(stmt, Expr) replace_coretypes!(stmt; rev=rev) diff --git a/src/packagedef.jl b/src/packagedef.jl index f5e01c3..db0f786 100644 --- a/src/packagedef.jl +++ b/src/packagedef.jl @@ -1,6 +1,6 @@ using Base.Meta import Base: +, -, convert, isless, get_world_counter -using Core: CodeInfo, SimpleVector, LineInfoNode, GotoNode, Slot, +using Core: CodeInfo, SimpleVector, LineInfoNode, GotoNode, GotoIfNot, ReturnNode, GeneratedFunctionStub, MethodInstance, NewvarNode, TypeName using UUIDs @@ -43,6 +43,12 @@ if !isdefined(Base, Symbol("@something")) end end +if isdefined(Base, :ScopedValues) + using Base: ScopedValues.Scope +else + const Scope = Any +end + include("types.jl") include("utils.jl") include("construct.jl") @@ -150,7 +156,7 @@ function __init__() # compiled_calls[(qsym, RT, Core.svec(AT...), Core.Compiler)] = f # precompile(f, AT) # end - + @static if isdefined(Base, :have_fma) FMA_FLOAT64[] = _have_fma_compiled(Float64) FMA_FLOAT32[] = _have_fma_compiled(Float32) diff --git a/src/precompile.jl b/src/precompile.jl index f929cde..5fa9189 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -1,12 +1,12 @@ module var"#Internal" -public(x::String) = false +public_fn(x::String) = false end function _precompile_() ccall(:jl_generating_output, Cint, ()) == 1 || return nothing @interpret sum(rand(10)) expr = quote - public(x::Integer) = true + public_fn(x::Integer) = true module Private private(y::String) = false end diff --git a/src/types.jl b/src/types.jl index 6bb16af..041a5b8 100644 --- a/src/types.jl +++ b/src/types.jl @@ -6,11 +6,6 @@ which will cause all calls to be evaluated via the interpreter. struct Compiled end Base.similar(::Compiled, sz) = Compiled() # to support similar(stack, 0) -# A type used transiently in renumbering CodeInfo SSAValues (to distinguish a new SSAValue from an old one) -struct NewSSAValue - id::Int -end - # Our own replacements for Core types. We need to do this to ensure we can tell the difference # between "data" (Core types) and "code" (our types) if we step into Core.Compiler struct SSAValue @@ -56,10 +51,17 @@ function breakpointchar(bps::BreakpointState) return bps.condition === falsecondition ? ' ' : 'd' # no breakpoint : disabled end -abstract type AbstractFrameInstance end -mutable struct DispatchableMethod - next::Union{Nothing,DispatchableMethod} # linked-list representation - frameinstance::Union{Compiled, AbstractFrameInstance} # really a Union{Compiled, FrameInstance} but we have a cyclic dependency +struct _FrameInstance{FrameCode} + framecode::FrameCode + sparam_vals::SimpleVector + enter_generated::Bool +end +Base.show(io::IO, instance::_FrameInstance) = + print(io, "FrameInstance(", scopeof(instance.framecode), ", ", instance.sparam_vals, ", ", instance.enter_generated, ')') + +mutable struct _DispatchableMethod{FrameCode} + next::Union{Nothing,_DispatchableMethod{FrameCode}} # linked-list representation + frameinstance::Union{Compiled,_FrameInstance{FrameCode}} # really a Union{Compiled, FrameInstance} but we have a cyclic dependency sig::Type # for speed of matching, this is a *concrete* signature. `sig <: frameinstance.framecode.scope.sig` end @@ -91,7 +93,7 @@ Important fields: struct FrameCode scope::Union{Method,Module} src::CodeInfo - methodtables::Vector{Union{Compiled,DispatchableMethod}} # line-by-line method tables for generic-function :call Exprs + methodtables::Vector{Union{Compiled,_DispatchableMethod{FrameCode}}} # line-by-line method tables for generic-function :call Exprs breakpoints::Vector{BreakpointState} slotnamelists::Dict{Symbol,Vector{Int}} used::BitSet @@ -100,6 +102,16 @@ struct FrameCode unique_files::Set{Symbol} end +""" +`FrameInstance` represents a method specialized for particular argument types. + +Fields: +- `framecode`: the [`FrameCode`](@ref) for the method. +- `sparam_vals`: the static parameter values for the method. +""" +const FrameInstance = _FrameInstance{FrameCode} +const DispatchableMethod = _DispatchableMethod{FrameCode} + const BREAKPOINT_EXPR = :($(QuoteNode(getproperty))($JuliaInterpreter, :__BREAKPOINT_MARKER__)) function is_breakpoint_expr(ex::Expr) # Sadly, comparing QuoteNodes calls isequal(::Any, ::Any), and === seems not to work. @@ -120,7 +132,7 @@ function FrameCode(scope, src::CodeInfo; generator=false, optimize=true) end breakpoints = Vector{BreakpointState}(undef, length(src.code)) for (i, pc_expr) in enumerate(src.code) - if isa(pc_expr, Expr) && is_breakpoint_expr(pc_expr) + if lookup_stmt(src.code, pc_expr) === __BREAK_POINT_MARKER__ breakpoints[i] = BreakpointState() src.code[i] = nothing end @@ -135,9 +147,24 @@ function FrameCode(scope, src::CodeInfo; generator=false, optimize=true) lt = linetable(src) unique_files = Set{Symbol}() + @static if VERSION ≥ v"1.12.0-DEV.173" + function pushuniquefiles!(unique_files::Set{Symbol}, lt) + for edge in lt.edges + pushuniquefiles!(unique_files, edge) + end + linetable = lt.linetable + if linetable === nothing + push!(unique_files, Base.IRShow.debuginfo_file1(lt)) + else + pushuniquefiles!(unique_files, linetable) + end + end + pushuniquefiles!(unique_files, lt) + else # VERSION < v"1.12.0-DEV.173" for entry in lt push!(unique_files, entry.file) end + end # @static if framecode = FrameCode(scope, src, methodtables, breakpoints, slotnamelists, used, generator, report_coverage, unique_files) if scope isa Method @@ -166,22 +193,6 @@ nstatements(framecode::FrameCode) = length(framecode.src.code) Base.show(io::IO, framecode::FrameCode) = print_framecode(io, framecode) -""" -`FrameInstance` represents a method specialized for particular argument types. - -Fields: -- `framecode`: the [`FrameCode`](@ref) for the method. -- `sparam_vals`: the static parameter values for the method. -""" -struct FrameInstance <: AbstractFrameInstance - framecode::FrameCode - sparam_vals::SimpleVector - enter_generated::Bool -end - -Base.show(io::IO, instance::FrameInstance) = - print(io, "FrameInstance(", scopeof(instance.framecode), ", ", instance.sparam_vals, ", ", instance.enter_generated, ')') - """ `FrameData` holds the arguments, local variables, and intermediate execution state in a particular call frame. @@ -204,6 +215,7 @@ struct FrameData ssavalues::Vector{Any} sparams::Vector{Any} exception_frames::Vector{Int} + current_scopes::Vector{Scope} last_exception::Base.RefValue{Any} caller_will_catch_err::Bool last_reference::Vector{Int} @@ -240,7 +252,7 @@ mutable struct Frame assignment_counter::Int64 caller::Union{Frame,Nothing} callee::Union{Frame,Nothing} - last_codeloc::Int32 + last_codeloc::Int end function Frame(framecode::FrameCode, framedata::FrameData, pc=1, caller=nothing) if length(junk_frames) > 0 @@ -348,6 +360,7 @@ Base.show(io::IO, var::Variable) = (print(io, var.name, " = "); show(io,var.valu Base.isequal(var1::Variable, var2::Variable) = var1.value == var2.value && var1.name === var2.name && var1.isparam == var2.isparam && var1.is_captured_closure == var2.is_captured_closure +Base.:(==)(var1::Variable, var2::Variable) = isequal(var1, var2) # A type that is unique to this package for which there are no valid operations struct Unassigned end diff --git a/src/utils.jl b/src/utils.jl index e6166ca..8cf2b20 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -40,14 +40,14 @@ function whichtt(@nospecialize(tt)) match === nothing && return nothing return match.method else - m = ccall(:jl_gf_invoke_lookup, Any, (Any, UInt), tt, get_world_counter()) + m = ccall(:jl_gf_invoke_lookup, Any, (Any, Csize_t), tt, get_world_counter()) m === nothing && return nothing isa(m, Method) && return m return m.func::Method end end -instantiate_type_in_env(arg, spsig, spvals) = +instantiate_type_in_env(arg, spsig::UnionAll, spvals::Vector{Any}) = ccall(:jl_instantiate_type_in_env, Any, (Any, Any, Ptr{Any}), arg, spsig, spvals) function sparam_syms(meth::Method) @@ -85,6 +85,10 @@ function scan_ssa_use!(used::BitSet, @nospecialize(stmt)) while iterval !== nothing useref, state = iterval val = Core.Compiler.getindex(useref) + if (@static VERSION < v"1.11.0-DEV.1180" && true) && isexpr(val, :call) + # work around for a linearization bug in Julia (https://github.com/JuliaLang/julia/pull/52497) + scan_ssa_use!(used, val) + end if isa(val, SSAValue) push!(used, val.id) end @@ -132,25 +136,7 @@ end isidentical(x) = Base.Fix2(===, x) # recommended over isequal(::Symbol) since it cannot be invalidated -# is_goto_node(@nospecialize(node)) = isa(node, GotoNode) || isexpr(node, :gotoifnot) - -if isdefined(Core, :GotoIfNot) - is_GotoIfNot(@nospecialize(node)) = isa(node, Core.GotoIfNot) - is_gotoifnot(@nospecialize(node)) = is_GotoIfNot(node) -else - is_GotoIfNot(@nospecialize(node)) = false - is_gotoifnot(@nospecialize(node)) = isexpr(node, :gotoifnot) -end - -if isdefined(Core, :ReturnNode) - is_ReturnNode(@nospecialize(node)) = isa(node, Core.ReturnNode) - is_return(@nospecialize(node)) = is_ReturnNode(node) - get_return_node(@nospecialize(node)) = (node::Core.ReturnNode).val -else - is_ReturnNode(@nospecialize(node)) = false - is_return(@nospecialize(node)) = isexpr(node, :return) - get_return_node(@nospecialize(node)) = node.args[1] -end +is_return(@nospecialize(node)) = node isa ReturnNode is_loc_meta(@nospecialize(expr), @nospecialize(kind)) = isexpr(expr, :meta) && length(expr.args) >= 1 && expr.args[1] === kind @@ -184,7 +170,7 @@ function is_call(@nospecialize(node)) (isexpr(node, :(=)) && (isexpr(node.args[2], :call))) end -is_call_or_return(@nospecialize(node)) = is_call(node) || is_return(node) +is_call_or_return(@nospecialize(node)) = is_call(node) || node isa ReturnNode is_dummy(bpref::BreakpointRef) = bpref.stmtidx == 0 && bpref.err === nothing @@ -194,6 +180,13 @@ function unpack_splatcall(stmt) end return false, nothing end +function unpack_splatcall(stmt, src::CodeInfo) + issplatcall, callee = unpack_splatcall(stmt) + if isa(callee, SSAValue) + callee = src.code[callee.id] + end + return issplatcall, callee +end function is_bodyfunc(@nospecialize(arg)) if isa(arg, QuoteNode) @@ -217,6 +210,12 @@ end is_generated(meth::Method) = isdefined(meth, :generator) +@static if VERSION < v"1.10.0-DEV.873" # julia#48766 + get_staged(mi::MethodInstance) = Core.Compiler.get_staged(mi) +else + get_staged(mi::MethodInstance) = Core.Compiler.get_staged(mi, Base.get_world_counter()) +end + """ is_doc_expr(ex) @@ -259,7 +258,11 @@ end # These getters improve inference since fieldtype(CodeInfo, :linetable) # and fieldtype(CodeInfo, :codelocs) are both Any -const LineTypes = Union{LineNumberNode,Core.LineInfoNode} +@static if VERSION ≥ v"1.12.0-DEV.173" + const LineTypes = Union{LineNumberNode,Base.IRShow.LineInfoNode} +else + const LineTypes = Union{LineNumberNode,Core.LineInfoNode} +end function linetable(arg) if isa(arg, Frame) arg = arg.framecode @@ -267,20 +270,63 @@ function linetable(arg) if isa(arg, FrameCode) arg = arg.src end - return (arg::CodeInfo).linetable::Union{Vector{Core.LineInfoNode},Vector{Any}} # issue #264 + ci = arg::CodeInfo + @static if VERSION ≥ v"1.12.0-DEV.173" + return ci.debuginfo + else # VERSION < v"1.12.0-DEV.173" + return ci.linetable::Union{Vector{Core.LineInfoNode},Vector{Any}} # issue #264 + end # @static if end -_linetable(list::Vector, i::Integer) = list[i]::Union{Expr,LineTypes} function linetable(arg, i::Integer; macro_caller::Bool=false)::Union{Expr,LineTypes} lt = linetable(arg) - lineinfo = _linetable(lt, i) + @static if VERSION ≥ v"1.12.0-DEV.173" + # TODO: decode the linetable at this frame efficiently by reimplementing this here + # TODO: get the contextual name from the parent, rather than returning "n/a" (which breaks Cthulhu) + return Base.IRShow.buildLineInfoNode(lt, :var"n/a", i)[1] # ignore all inlining / macro expansion / etc :( + else # VERSION < v"1.12.0-DEV.173" + lin = lt[i]::Union{Expr,LineTypes} if macro_caller - while lineinfo isa Core.LineInfoNode && lineinfo.method === Symbol("macro expansion") && lineinfo.inlined_at != 0 - lineinfo = _linetable(lt, lineinfo.inlined_at) + while lin isa Core.LineInfoNode && lin.method === Symbol("macro expansion") && lin.inlined_at != 0 + lin = lt[lin.inlined_at]::Union{Expr,LineTypes} end end - return lineinfo + return lin + end # @static if end +@static if VERSION ≥ v"1.12.0-DEV.173" + +getlastline(arg) = getlastline(linetable(arg)) +function getlastline(debuginfo::Core.DebugInfo) + while true + ltnext = debuginfo.linetable + ltnext === nothing && break + debuginfo = ltnext + end + lastline = 0 + for k = 0:typemax(Int) + codeloc = Core.Compiler.getdebugidx(debuginfo, k) + line::Int = codeloc[1] + line < 0 && break + lastline = max(lastline, line) + end + return lastline +end +function codelocs(arg, i::Integer) + debuginfo = linetable(arg) + codeloc = Core.Compiler.getdebugidx(debuginfo, i) + line::Int = codeloc[1] + line < 0 && return 0# broken or disabled debug info? + if line == 0 && codeloc[2] == 0 + return 0 # no line number update + end + return i +end + +else # VERSION < v"1.12.0-DEV.173" + +getfirstline(arg) = getline(linetable(arg)[begin]) +getlastline(arg) = getline(linetable(arg)[end]) function codelocs(arg) if isa(arg, Frame) arg = arg.framecode @@ -288,9 +334,12 @@ function codelocs(arg) if isa(arg, FrameCode) arg = arg.src end - return (arg::CodeInfo).codelocs::Vector{Int32} + ci = arg::CodeInfo + return ci.codelocs end -codelocs(arg, i::Integer) = codelocs(arg)[i] # for consistency with linetable (but no extra benefit here) +codelocs(arg, i::Integer) = codelocs(arg)[i] + +end # @static if function lineoffset(framecode::FrameCode) offset = 0 @@ -303,9 +352,9 @@ function lineoffset(framecode::FrameCode) end function getline(ln::Union{LineTypes,Expr}) - _getline(ln::LineTypes) = ln.line - _getline(ln::Expr) = ln.args[1] # assuming ln.head === :line - return Int(_getline(ln))::Int + _getline(ln::LineTypes) = Int(ln.line) + _getline(ln::Expr) = ln.args[1]::Int # assuming ln.head === :line + return _getline(ln) end function getfile(ln::Union{LineTypes,Expr}) _getfile(ln::LineTypes) = ln.file::Symbol @@ -365,12 +414,25 @@ end getfile(frame::Frame, pc=frame.pc) = getfile(frame.framecode, pc) function codelocation(code::CodeInfo, idx::Int) - codeloc = codelocs(code)[idx] - while codeloc == 0 && (code.code[idx] === nothing || isexpr(code.code[idx], :meta)) && idx < length(code.code) - idx += 1 - codeloc = codelocs(code)[idx] - end - return codeloc + idx′ = idx + # look ahead if we are on a meta line + while idx′ < length(code.code) + codeloc = codelocs(code, idx′) + codeloc == 0 || return codeloc + ex = code.code[idx′] + ex === nothing || isexpr(ex, :meta) || break + idx′ += 1 + end + idx′ = idx - 1 + # if zero, look behind until we find where we last might have had a line + while idx′ > 0 + ex = code.code[idx′] + codeloc = codelocs(code, idx′) + codeloc == 0 || return codeloc + idx′ -= 1 + end + # for the start of the function, return index 1 + return 1 end function compute_corrected_linerange(method::Method) @@ -378,13 +440,11 @@ function compute_corrected_linerange(method::Method) offset = line1 - method.line @assert !is_generated(method) src = JuliaInterpreter.get_source(method) - lastline = linetable(src)[end]::LineTypes - return line1:getline(lastline) + offset + lastline = getlastline(src) + return line1:lastline + offset end -function compute_linerange(framecode) - getline(linetable(framecode, 1)):getline(last(linetable(framecode))) -end +compute_linerange(framecode) = getfirstline(framecode):getlastline(framecode) function statementnumbers(framecode::FrameCode, line::Integer, file::Symbol) # Check to see if this framecode really contains that line. Methods that fill in a default positional argument, @@ -422,7 +482,6 @@ function statementnumbers(framecode::FrameCode, line::Integer, file::Symbol) return stmtidxs end - # If the exact line number does not exist in the line table, take the one that is closest after that line # restricted to the line range of the current scope. scope = framecode.scope @@ -494,9 +553,7 @@ breakpointchar(framecode, stmtidx) = function print_framecode(io::IO, framecode::FrameCode; pc=0, range=1:nstatements(framecode), kwargs...) iscolor = get(io, :color, false) ndstmt = ndigits(nstatements(framecode)) - lt = linetable(framecode) - offset = lineoffset(framecode) - ndline = isempty(lt) ? 0 : ndigits(getline(lt[end]) + offset) + ndline = ndigits(getlastline(framecode) + lineoffset(framecode)) nullline = " "^ndline src = copy(framecode.src) replace_coretypes!(src; rev=true) @@ -734,11 +791,11 @@ function Base.StackTraces.StackFrame(frame::Frame) scope = scopeof(frame) if scope isa Method method = scope - method_args = [something(frame.framedata.locals[i]) for i in 1:method.nargs] - atypes = Tuple{mapany(_Typeof, method_args)...} + method_args = Any[something(frame.framedata.locals[i]) for i in 1:method.nargs] + argt = Tuple{mapany(_Typeof, method_args)...} sig = method.sig - sparams = Core.svec(frame.framedata.sparams...) - mi = Core.Compiler.specialize_method(method, atypes, sparams) + atype, sparams = ccall(:jl_type_intersection_with_env, Any, (Any, Any), argt, sig)::SimpleVector + mi = Core.Compiler.specialize_method(method, atype, sparams::SimpleVector) fname = method.name else mi = frame.framecode.src @@ -747,12 +804,11 @@ function Base.StackTraces.StackFrame(frame::Frame) Base.StackFrame( fname, Symbol(getfile(frame)), - @something(linenumber(frame), getline(linetable(frame, 1))), + @something(linenumber(frame), getfirstline(frame)), mi, false, false, - C_NULL - ) + C_NULL) end function Base.show_backtrace(io::IO, frame::Frame) diff --git a/test/breakpoints.jl b/test/breakpoints.jl index 1070ec4..8a03c33 100644 --- a/test/breakpoints.jl +++ b/test/breakpoints.jl @@ -195,10 +195,10 @@ struct Squarer end # Breakpoint display io = IOBuffer() frame = JuliaInterpreter.enter_call(loop_radius2, 2) - if VERSION < v"1.9.0-DEV.846" # https://github.com/JuliaLang/julia/pull/45069 + @static if VERSION < v"1.9.0-DEV.846" # https://github.com/JuliaLang/julia/pull/45069 LOC = " in $(@__MODULE__) at $(@__FILE__)" else - LOC = "\n @ $(@__MODULE__) $(contractuser(@__FILE__))" + LOC = " @ $(@__MODULE__) $(contractuser(@__FILE__))" end bp = JuliaInterpreter.BreakpointRef(frame.framecode, 1) @test repr(bp) == "breakpoint(loop_radius2(n)$LOC:$(3-Δ), line 3)" @@ -219,7 +219,7 @@ struct Squarer end end fr, bp = @interpret f_outer_bp(3) @test leaf(fr).framecode.scope.name === :g_inner_bp - @test bp.stmtidx == 3 + @test bp.stmtidx == (@static VERSION >= v"1.11-" ? 4 : 3) # Breakpoints on types remove() @@ -520,7 +520,7 @@ end return 2 end frame = JuliaInterpreter.enter_call(f_macro) - file_logging = "logging.jl" + file_logging = String(only(methods(var"@info")).file) line_logging = 0 for entry in frame.framecode.src.linetable if entry.file === Symbol(file_logging) @@ -534,7 +534,7 @@ end @test bp isa BreakpointRef file, ln = JuliaInterpreter.whereis(frame) @test ln == line_logging - @test basename(file) == file_logging + @test basename(file) == basename(file_logging) bp = JuliaInterpreter.finish_stack!(frame) @test bp isa BreakpointRef frame = leaf(frame) diff --git a/test/code_coverage/code_coverage.jl b/test/code_coverage/code_coverage.jl index 67dd319..6222a3e 100644 --- a/test/code_coverage/code_coverage.jl +++ b/test/code_coverage/code_coverage.jl @@ -24,22 +24,22 @@ let local pid try @testset "code coverage" begin - io = IOBuffer() + io = Base.PipeEndpoint() filepath = normpath(@__DIR__, "coverage_example.jl") cmd = `$(Base.julia_cmd()) --startup=no --project=$(dirname(dirname(@__DIR__))) --code-coverage=user $filepath` - p = run(pipeline(cmd; stdout=io); wait=false) + p = run(cmd, devnull, io, stderr; wait=false) pid = Libc.getpid(p) - wait(p) - out = String(take!(io)) - @test out == "1 2 fizz 4 " + @test read(io, String) == "1 2 fizz 4 " + @test success(p) dir, _, files = first(walkdir(@__DIR__)) i = findfirst(contains(r"coverage_example\.jl\.\d+\.cov"), files) i === nothing && error("no coverage files found in $dir: $files") cov_file = joinpath(dir, files[i]) cov_data = read(cov_file, String) - expected = read(joinpath(dir, "coverage_example.jl.cov"), String) + expected = "coverage_example.jl.cov" + expected = read(joinpath(dir, expected), String) if Sys.iswindows() cov_data = replace(cov_data, "\r\n" => "\n") expected = replace(cov_data, "\r\n" => "\n") diff --git a/test/core.jl b/test/core.jl index 42b1cbd..b845f7a 100644 --- a/test/core.jl +++ b/test/core.jl @@ -25,8 +25,8 @@ using Test end thunk = Meta.lower(Main, :(return 1+2)) - stmt = thunk.args[1].code[end] # the return - @test JuliaInterpreter.get_return_node(stmt) isa Core.SSAValue + stmt = thunk.args[1].code[end]::Core.ReturnNode # the return + @test stmt.val isa Core.SSAValue @test string(JuliaInterpreter.parametric_type_to_expr(Base.Iterators.Stateful{String})) ∈ ("Base.Iterators.Stateful{String, VS}", "(Base.Iterators).Stateful{String, VS}", "Base.Iterators.Stateful{String, VS, N}") diff --git a/test/debug.jl b/test/debug.jl index 68c78c2..78fafb4 100644 --- a/test/debug.jl +++ b/test/debug.jl @@ -81,7 +81,7 @@ end oframe = frame = enter_call(func, args...; kwargs...) frame = JuliaInterpreter.maybe_step_through_kwprep!(frame, false) frame = JuliaInterpreter.maybe_step_through_wrapper!(frame) - @test any(stmt->isa(stmt, Expr) && JuliaInterpreter.hasarg(isequal(QuoteNode(==)), stmt.args), frame.framecode.src.code) + @test JuliaInterpreter.hasarg(JuliaInterpreter.isidentical(QuoteNode(==)), frame.framecode.src.code) f, pc = debug_command(frame, :n) @test f === frame @test isa(pc, Int) @@ -133,42 +133,42 @@ end end @testset "generated" begin - frame = enter_call_expr(:($(callgenerated)())) - f, pc = debug_command(frame, :s) - @test isa(pc, BreakpointRef) - @test JuliaInterpreter.scopeof(f).name === :generatedfoo - stmt = JuliaInterpreter.pc_expr(f) - @test JuliaInterpreter.is_return(stmt) && JuliaInterpreter.lookup_return(frame, stmt) === Int - @test debug_command(frame, :c) === nothing - @test frame.callee === nothing - @test get_return(frame) === Int + let frame = enter_call_expr(:($(callgenerated)())) + cframe, pc = debug_command(frame, :s) + @test isa(pc, BreakpointRef) + @test JuliaInterpreter.scopeof(cframe).name === :generatedfoo + @test debug_command(cframe, :c) === nothing + @test cframe.callee === nothing + @test get_return(cframe) === Int + end # This time, step into the generated function itself - frame = enter_call_expr(:($(callgenerated)())) - f, pc = debug_command(frame, :sg) + let frame = enter_call_expr(:($(callgenerated)())) + cframe, pc = debug_command(frame, :sg) # Aside: generators can have `Expr(:line, ...)` in their line tables, test that this is OK - lt = JuliaInterpreter.linetable(f, 2) - @test isexpr(lt, :line) || isa(lt, Core.LineInfoNode) - @test isa(pc, BreakpointRef) - @test JuliaInterpreter.scopeof(f).name === :generatedfoo - stmt = JuliaInterpreter.pc_expr(f) - @test JuliaInterpreter.is_return(stmt) && JuliaInterpreter.lookup_return(f, stmt) === 1 - f2, pc = debug_command(f, :finish) - @test JuliaInterpreter.scopeof(f2).name === :callgenerated - # Now finish the regular function - @test debug_command(frame, :finish) === nothing - @test frame.callee === nothing - @test get_return(frame) === 1 + lt = JuliaInterpreter.linetable(cframe, 2) + @test isexpr(lt, :line) || isa(lt, Core.LineInfoNode) || + (isdefined(Base.IRShow, :LineInfoNode) && isa(lt, Base.IRShow.LineInfoNode)) + @test isa(pc, BreakpointRef) + @test JuliaInterpreter.scopeof(cframe).name === :generatedfoo + cframe, pc = debug_command(cframe, :finish) + @test JuliaInterpreter.scopeof(cframe).name === :callgenerated + # Now finish the regular function + @test debug_command(cframe, :finish) === nothing + @test cframe.callee === nothing + @test get_return(cframe) === 1 + end # Parametric generated function (see #157) - frame = fr = JuliaInterpreter.enter_call(callgeneratedparams) - while fr.pc < JuliaInterpreter.nstatements(fr.framecode) - 1 - fr, pc = debug_command(fr, :se) + let frame = fr = JuliaInterpreter.enter_call(callgeneratedparams) + while fr.pc < JuliaInterpreter.nstatements(fr.framecode) - 1 + fr, pc = debug_command(fr, :se) + end + fr, pc = debug_command(fr, :sg) + @test JuliaInterpreter.scopeof(fr).name === :generatedparams + fr, pc = debug_command(fr, :finish) + @test debug_command(fr, :finish) === nothing + @test JuliaInterpreter.get_return(fr) == (Int, 2) end - fr, pc = debug_command(fr, :sg) - @test JuliaInterpreter.scopeof(fr).name === :generatedparams - fr, pc = debug_command(fr, :finish) - @test debug_command(fr, :finish) === nothing - @test JuliaInterpreter.get_return(fr) == (Int, 2) end @testset "Optional arguments" begin @@ -427,7 +427,12 @@ end frame = JuliaInterpreter.enter_call(sort, a) frame = stepkw!(frame) - @test frame.pc == JuliaInterpreter.nstatements(frame.framecode) - 1 + @static if VERSION ≥ v"1.7" + # TODO fix this broken test (@aviatesk) + @test frame.pc == JuliaInterpreter.nstatements(frame.framecode) - 1 broken=VERSION≥v"1.11-" + else + @test frame.pc == JuliaInterpreter.nstatements(frame.framecode) - 1 + end frame, pc = debug_command(frame, :s) frame, pc = debug_command(frame, :se) # get past copymutable @@ -472,7 +477,7 @@ end @testset "si should not step through wrappers or kwprep" begin frame = JuliaInterpreter.enter_call(h_1, 2, 1) frame, pc = debug_command(frame, :si) - @test frame.pc == 1 + @test frame.pc == (VERSION >= v"1.11-" ? 2 : 1) end @testset "breakpoints hit during wrapper step through" begin @@ -520,7 +525,7 @@ end end # end -module Foo +module InterpretedModuleTest using ..JuliaInterpreter function f(x) x @@ -529,15 +534,17 @@ module Foo end end @testset "interpreted methods" begin - g(x) = Foo.f(x) - - push!(JuliaInterpreter.compiled_modules, Foo) - frame = JuliaInterpreter.enter_call(g, 5) + push!(JuliaInterpreter.compiled_modules, InterpretedModuleTest) + frame = JuliaInterpreter.enter_call(5) do x + InterpretedModuleTest.f(5) + end frame, pc = JuliaInterpreter.debug_command(frame, :n) @test !(pc isa BreakpointRef) - push!(JuliaInterpreter.interpreted_methods, first(methods(Foo.f))) - frame = JuliaInterpreter.enter_call(g, 5) + push!(JuliaInterpreter.interpreted_methods, first(methods(InterpretedModuleTest.f))) + frame = JuliaInterpreter.enter_call(5) do x + InterpretedModuleTest.f(5) + end frame, pc = JuliaInterpreter.debug_command(frame, :n) @test pc isa BreakpointRef end diff --git a/test/eval_code.jl b/test/eval_code.jl index d0aee34..cf9f879 100644 --- a/test/eval_code.jl +++ b/test/eval_code.jl @@ -1,4 +1,4 @@ -import JuliaInterpreter.eval_code +using JuliaInterpreter: eval_code # Simple evaling of function argument function evalfoo1(x,y) @@ -51,6 +51,9 @@ end frame = JuliaInterpreter.enter_call(f) JuliaInterpreter.step_expr!(frame) JuliaInterpreter.step_expr!(frame) +@static if VERSION >= v"1.11-" + JuliaInterpreter.step_expr!(frame) +end @test eval_code(frame, "x") == 1 eval_code(frame, "x = 3") @test eval_code(frame, "x") == 3 @@ -77,10 +80,17 @@ eval_code(fr, "non_accessible_variable = 5.0") # Evaluating SSAValues f(x) = x^2 frame = JuliaInterpreter.enter_call(f, 5) -JuliaInterpreter.step_expr!(frame) -JuliaInterpreter.step_expr!(frame) +id = let + pc, n = frame.pc, length(frame.framecode.src.code) + while pc < n - 1 + pc = JuliaInterpreter.step_expr!(frame) + end + # Extract the SSAValue that corresponds to the power + stmt = frame.framecode.src.code[pc]::Expr # the `literal_pow` call + stmt.args[end].id +end # This could change with changes to Julia lowering -@test eval_code(frame, "var\"%2\"") == Val(2) +@test eval_code(frame, "var\"%$(id)\"") == Val(2) @test eval_code(frame, "var\"@_1\"") == f function fun(;output=:sym) @@ -96,9 +106,13 @@ eval_code(fr, "output = :foo") @test eval_code(fr, "output") === :foo let f() = GlobalRef(Main, :doesnotexist) - fr = JuliaInterpreter.enter_call(f) - JuliaInterpreter.step_expr!(fr) - @test eval_code(fr, "var\"%1\"") == GlobalRef(Main, :doesnotexist) + frame = JuliaInterpreter.enter_call(f) + retidx = findfirst(frame.framecode.src.code) do x + x isa Core.ReturnNode && x.val isa JuliaInterpreter.SSAValue + end + ssaidx = ((frame.framecode.src.code[retidx]::Core.ReturnNode).val::JuliaInterpreter.SSAValue).id + for _ = 1:ssaidx; JuliaInterpreter.step_expr!(frame); end + @test eval_code(frame, "var\"%$ssaidx\"") == GlobalRef(Main, :doesnotexist) end # Don't error on empty input string diff --git a/test/interpret.jl b/test/interpret.jl index d2a1763..4b38c9e 100644 --- a/test/interpret.jl +++ b/test/interpret.jl @@ -1,5 +1,4 @@ using JuliaInterpreter -using JuliaInterpreter: enter_call_expr using Test, InteractiveUtils, CodeTracking using Mmap using LinearAlgebra @@ -246,8 +245,8 @@ function f(x) end frame = JuliaInterpreter.enter_call(f, 3) @test whereis(frame, 1)[2] == defline + 1 -@test whereis(frame, 3)[2] == defline + 4 -@test whereis(frame, 5)[2] == defline + 6 +@test whereis(frame, (length(frame.framecode.src.code) + 1) ÷ 2)[2] == defline + 4 +@test whereis(frame, length(frame.framecode.src.code) - 1)[2] == defline + 6 m = which(iterate, Tuple{Dict}) # this method has `nothing` as its first statement and codeloc == 0 framecode = JuliaInterpreter.get_framecode(m) @test JuliaInterpreter.linenumber(framecode, 1) == m.line + CodeTracking.line_is_decl @@ -307,7 +306,7 @@ let TT = Union{UInt8, Int8} end # issue #92 -let x = Core.TypedSlot(1, Any) +let x = Core.SlotNumber(1) f(x) = objectid(x) @test isa(@interpret(f(x)), UInt) end @@ -374,6 +373,11 @@ f113(;x) = x @test JuliaInterpreter.Variable(1, :x, false) in locals JuliaInterpreter.step_expr!(stack, frame) JuliaInterpreter.step_expr!(stack, frame) + @static if VERSION >= v"1.11-" + locals = JuliaInterpreter.locals(frame) + @test length(locals) == 2 + JuliaInterpreter.step_expr!(stack, frame) + end locals = JuliaInterpreter.locals(frame) @test length(locals) == 3 @test JuliaInterpreter.Variable(1, :c, false) in locals @@ -457,8 +461,8 @@ end @test @interpret get(ENV, "THIS_IS_NOT_DEFINED_1234", "24") == "24" # Test return value of whereis -f() = nothing -fr = JuliaInterpreter.enter_call(f) +fnone() = nothing +fr = JuliaInterpreter.enter_call(fnone) file, line = JuliaInterpreter.whereis(fr) @test file == @__FILE__ @test line == (@__LINE__() - 4) @@ -468,7 +472,7 @@ fr = JuliaInterpreter.enter_call(Test.eval, 1) file, line = JuliaInterpreter.whereis(fr) @test isfile(file) @test isfile(JuliaInterpreter.getfile(fr.framecode.src.linetable[1])) -if VERSION < v"1.9.0-DEV.846" # https://github.com/JuliaLang/julia/pull/45069 +@static if VERSION < v"1.9.0-DEV.846" # https://github.com/JuliaLang/julia/pull/45069 @test occursin(Sys.STDLIB, repr(fr)) else @test occursin(contractuser(Sys.STDLIB), repr(fr)) @@ -565,6 +569,17 @@ finally break_off(:error) end +f_562(x::Union{Vector{T}, Nothing}) where {T} = x + 1 +try + break_on(:error) + local frame, bp = @interpret f_562(nothing) + + stacktrace_lines = split(sprint(Base.display_error, bp.err, leaf(frame)), '\n') + @test stacktrace_lines[1] == "ERROR: MethodError: no method matching +(::Nothing, ::$Int)" +finally + break_off(:error) +end + # https://github.com/JuliaDebug/JuliaInterpreter.jl/issues/154 q = QuoteNode([1]) @test @interpret deepcopy(q) == q @@ -581,7 +596,10 @@ end @test @interpret(hash220((Ptr{UInt8}(0),0), UInt(1))) == hash220((Ptr{UInt8}(0),0), UInt(1)) # ccall with type parameters -@test (@interpret Base.unsafe_convert(Ptr{Int}, [1,2])) isa Ptr{Int} +@static if VERSION < v"1.11-" + # TODO: in v1.11+ this function does not have a ccall + @test (@interpret Base.unsafe_convert(Ptr{Int}, [1,2])) isa Ptr{Int} +end # ccall with call to get the pointer cf = [@cfunction(fcfun, Int, (Int, Int))] @@ -599,20 +617,20 @@ end f_N() = Array{Float64, 4}(undef, 1, 3, 2, 1) @test (@interpret f_N()) isa Array{Float64, 4} -f() = ccall((:clock, "libc"), Int32, ()) +f_clock() = ccall((:clock, "libc"), Int32, ()) # See that the method gets compiled -try @interpret f() +try @interpret f_clock() catch end -let mt = JuliaInterpreter.enter_call(f).framecode.methodtables +let mt = JuliaInterpreter.enter_call(f_clock).framecode.methodtables @test any(1:length(mt)) do i isassigned(mt, i) && mt[i] === Compiled() end end # https://github.com/JuliaDebug/JuliaInterpreter.jl/issues/194 -f() = Meta.lower(Main, Meta.parse("(a=1,0)")) -@test @interpret f() == f() +f_parse() = Meta.lower(Main, Meta.parse("(a=1,0)")) +@test @interpret f_parse() == f_parse() # Test for vararg ccalls (used by mmap) function f_mmap() @@ -667,15 +685,15 @@ end @test @interpret(VecTest.f()) == [1 0 0; 0 1 0; 0 0 1] # Test exception type for undefined variables -f() = s = s + 1 -@test_throws UndefVarError @interpret f() +f_undefvar() = s = s + 1 +@test_throws UndefVarError @interpret f_undefvar() # Handling of SSAValues -function f() +function f_ssaval() z = [Core.SSAValue(5),] repr(z[1]) end -@test @interpret f() == f() +@test @interpret f_ssaval() == f_ssaval() # Test JuliaInterpreter version of #265 f(x) = x @@ -732,11 +750,12 @@ end @test @interpret(f(D)) === f(D) # issue #441 & #535 - flog() = @info "logging macros" - @test_logs (:info, "logging macros") @test @interpret flog() === nothing - flog2() = @error "this error is ok" - frame = JuliaInterpreter.enter_call(flog2) - @test_logs (:error, "this error is ok") @test debug_command(frame, :c) === nothing + f_log1() = @info "logging macros" + @test (@test_logs (:info, "logging macros") (@interpret f_log1())) === nothing + f_log2() = @error "this error is ok" + let frame = JuliaInterpreter.enter_call(f_log2) + @test (@test_logs (:error, "this error is ok") debug_command(frame, :c)) === nothing + end end struct A396 @@ -759,40 +778,20 @@ end end @testset "#476 isdefined QuoteNode" begin - f() = !true - - @generated function g() - ci = @code_lowered f() - ci.code[1] = Expr(:isdefined, QuoteNode(Float64)) - return ci + @eval function issue476() + return $(Expr(:isdefined, QuoteNode(Float64))) end - - @test @interpret(g()) === true + @test (true === @interpret issue476()) end -const override_world = typemax(Csize_t) - 1 -macro unreachable(ex) - quote - world_counter = cglobal(:jl_world_counter, Csize_t) - regular_world = unsafe_load(world_counter) - - $(Expr(:tryfinally, # don't introduce scope - quote - unsafe_store!(world_counter, $(override_world-1)) - $(esc(ex)) - end, - quote - unsafe_store!(world_counter, regular_world) - end - )) - end +@noinline foobar() = (GC.safepoint(); 42) +function run_foobar() + @eval foobar() = "nope" + return @interpret(foobar()), foobar() end - @testset "unreachable worlds" begin - foobar() = 42 - @unreachable foobar() = "nope" - - @test @interpret(foobar()) == foobar() + interpret, compiled = run_foobar() + @test_broken interpret == compiled == 42 end @testset "issue #479" begin @@ -813,7 +812,11 @@ end end # this shouldn't throw "type DataType has no field hasfreetypevars" # even after https://github.com/JuliaLang/julia/pull/41018 - @test Int === @interpret Core.Compiler.getfield_tfunc(m.Foo, Core.Compiler.Const(:foo)) + @static if VERSION ≥ v"1.9.0-DEV.1556" + @test Int === @interpret Core.Compiler.getfield_tfunc(Core.Compiler.fallback_lattice, m.Foo, Core.Compiler.Const(:foo)) + else + @test Int === @interpret Core.Compiler.getfield_tfunc(m.Foo, Core.Compiler.Const(:foo)) + end end @testset "https://github.com/JuliaDebug/JuliaInterpreter.jl/issues/488" begin @@ -833,7 +836,7 @@ end @static if VERSION >= v"1.7.0" @testset "issue #432" begin function f() - t = @ccall time()::Cint + t = @ccall time(C_NULL::Ptr{Cvoid})::Cint end @test @interpret(f()) !== 0 @test @interpret(f()) !== 0 @@ -841,12 +844,13 @@ end end @testset "issue #385" begin - using FunctionWrappers:FunctionWrapper - @interpret FunctionWrapper{Int,Tuple{}}(()->42) + using FunctionWrappers: FunctionWrapper + fw = @interpret FunctionWrapper{Int,Tuple{}}(()->42) + @test 42 === @interpret fw() end @testset "issue #550" begin - using FunctionWrappers:FunctionWrapper + using FunctionWrappers: FunctionWrapper f = (obs) -> (obs[1] = obs[3] * obs[4]; obs) Tout = Vector{Int} Tin = Tuple{Vector{Int}} @@ -868,7 +872,11 @@ end end ci = code_typed(foo, NTuple{2, Int}; optimize=false)[][1] - mi = Core.Compiler.method_instances(foo, NTuple{2, Int})[] + @static if VERSION ≥ v"1.10.0-DEV.873" + mi = Core.Compiler.method_instances(foo, NTuple{2, Int}, Base.get_world_counter())[] + else + mi = Core.Compiler.method_instances(foo, NTuple{2, Int})[] + end frameargs = Any[foo, 1, 2] framecode = JuliaInterpreter.FrameCode(mi.def, ci) @@ -878,7 +886,6 @@ end @testset "interpretation of unoptimized frame" begin let # should be able to interprete nested calls within `:foreigncall` expressions - # even if `JuliaInterpreter.optimize!` doesn't flatten them M = Module() lwr = Meta.@lower M begin global foo = @ccall strlen("foo"::Cstring)::Csize_t @@ -979,3 +986,11 @@ end @test (@interpret string("", "pcre_h.jl")) == string("", "pcre_h.jl") @test (@interpret Base.strcat("", "build_h.jl")) == Base.strcat("", "build_h.jl") end + +# test for using generic functions that were previously builtin +func_arrayref(a, i) = Core.arrayref(true, a, i) +@test 2 == @interpret func_arrayref([1,2,3], 2) + +@static if isdefined(Base, :ScopedValues) +@testset "interpret_scopedvalues.jl" include("interpret_scopedvalues.jl") +end diff --git a/test/interpret_scopedvalues.jl b/test/interpret_scopedvalues.jl new file mode 100644 index 0000000..25de35b --- /dev/null +++ b/test/interpret_scopedvalues.jl @@ -0,0 +1,55 @@ +module interpret_scopedvalues + +using Test, JuliaInterpreter +using Base.ScopedValues + +const sval1 = ScopedValue(1) +const sval2 = ScopedValue(1) +@test 1 == @interpret getindex(sval1) + +# current_scope support for interpretation +sval1_func1() = @with sval1 => 2 begin + return sval1[] +end +@test 2 == @interpret sval1_func1() +sval12_func1() = @with sval1 => 2 begin + @with sval2 => 3 begin + return sval1[], sval2[] + end +end +@test (2, 3) == @interpret sval12_func1() + +# current_scope support for compiled calls +_sval1_func2() = sval1[] +sval1_func2() = @with sval1 => 2 begin + return _sval1_func2() +end +let m = only(methods(_sval1_func2)) + push!(JuliaInterpreter.compiled_methods, m) + try + @test 2 == @interpret sval1_func2() + finally + delete!(JuliaInterpreter.compiled_methods, m) + end +end +let frame = JuliaInterpreter.enter_call(sval1_func2) + @test 2 == JuliaInterpreter.finish_and_return!(Compiled(), frame) +end + +# preset `current_scope` support +@test 2 == @with sval1 => 2 begin + @interpret getindex(sval1) +end +@test (2, 3) == @with sval1 => 2 sval2 => 3 begin + @interpret(getindex(sval1)), @interpret(getindex(sval2)) +end +@test (2, 3) == @with sval1 => 2 begin @with sval2 => 3 begin + @interpret(getindex(sval1)), @interpret(getindex(sval2)) +end end +let frame = JuliaInterpreter.enter_call() do + sval1[], sval2[] + end + @test (2, 3) == @with sval1 => 2 sval2 => 3 JuliaInterpreter.finish_and_return!(frame) +end + +end # module interpret_scopedvalues diff --git a/test/limits.jl b/test/limits.jl index 7ac0a6f..3cfe9a2 100644 --- a/test/limits.jl +++ b/test/limits.jl @@ -38,7 +38,7 @@ end @test Aborted(frame, i).at.line == 6 # Check conditional frame = Frame(modexs[4]...) - i = findfirst(stmt->JuliaInterpreter.is_gotoifnot(stmt), frame.framecode.src.code) + 1 + i = findfirst(stmt->isa(stmt, Core.GotoIfNot), frame.framecode.src.code) + 1 @test Aborted(frame, i).at.line == 9 # Check macro frame = Frame(modexs[5]...) @@ -85,10 +85,16 @@ module EvalLimited end insert!(ex.args, 1, LineNumberNode(1, Symbol("fake.jl"))) end modexs = collect(ExprSplitter(EvalLimited, ex)) - @static if isdefined(Core, :get_binding_type) - nstmts = 10*12 + 20 # 10 * 12 statements per iteration + α + @static if VERSION >= v"1.11-" + nstmts = 10*17 + 20 # 10 * 17 statements per iteration + α + elseif VERSION >= v"1.10-" + nstmts = 10*15 + 20 # 10 * 15 statements per iteration + α + elseif isdefined(Core, :get_binding_type) + nstmts = 10*14 + 20 # 10 * 14 statements per iteration + α + elseif VERSION >= v"1.7-" + nstmts = 10*11 + 20 # 10 * 9 statements per iteration + α else - nstmts = 9*12 + 20 # 10 * 9 statements per iteration + α + nstmts = 10*10 + 20 # 10 * 10 statements per iteration + α end for (mod, ex) in modexs frame = Frame(mod, ex) @@ -120,9 +126,6 @@ module EvalLimited end @test EvalLimited.s < 5 @test length(aborts) == 1 lin = aborts[1].at - if lin.file === Symbol("fake.jl") - @test lin.line ∈ (2, 3, 4, 5) - else - @test lin.method === :iterate || lin.method === :getproperty - end + @test lin.file === Symbol("fake.jl") + @test lin.line ∈ (2, 3, 4, 5) end diff --git a/test/runtests.jl b/test/runtests.jl index 65073af..f43c24d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,7 +2,6 @@ using JuliaInterpreter using Test using Logging - @test isempty(detect_ambiguities(JuliaInterpreter, Base, Core)) if !isdefined(@__MODULE__, :read_and_parse) @@ -12,14 +11,14 @@ end Core.eval(JuliaInterpreter, :(debug_mode() = true)) @testset "Main tests" begin - include("check_builtins.jl") - include("core.jl") - include("interpret.jl") - include("toplevel.jl") - include("limits.jl") - include("eval_code.jl") - include("breakpoints.jl") - @static VERSION >= v"1.8.0-DEV.370" && include("code_coverage/code_coverage.jl") + @testset "check_bulitins.jl" begin include("check_builtins.jl") end + @testset "core.jl" begin include("core.jl") end + @testset "interpret.jl" begin include("interpret.jl") end + @testset "toplevel.jl" begin include("toplevel.jl") end + @testset "limits.jl" begin include("limits.jl") end + @testset "eval_code.jl" begin include("eval_code.jl") end + @testset "breakpoints.jl" begin include("breakpoints.jl") end + @static VERSION >= v"1.8.0-DEV.370" && @testset "code_coverage/code_coverage.jl" begin include("code_coverage/code_coverage.jl") end remove() - include("debug.jl") + @testset "debug.jl" begin include("debug.jl") end end diff --git a/test/toplevel.jl b/test/toplevel.jl index 044059d..f30c6c8 100644 --- a/test/toplevel.jl +++ b/test/toplevel.jl @@ -29,13 +29,26 @@ end end end end - modexs = collect(ExprSplitter(Main, ex)) - m, ex = first(modexs) + modexs = collect(ExprSplitter(JIVisible, ex)) + m, ex = first(modexs) # FIXME don't use index in tests @test JuliaInterpreter.is_doc_expr(ex.args[2]) Core.eval(m, ex) io = IOBuffer() - show(io, @doc(Main.DocStringTest)) + show(io, @doc(JIVisible.DocStringTest)) @test occursin("Special", String(take!(io))) + + ex = Base.parse_input_line(""" + "docstring" + module OuterModDocstring + "docstring for InnerModDocstring" + module InnerModDocstring + end + end + """) + modexs = collect(ExprSplitter(JIVisible, ex)) + @test isdefined(JIVisible, :OuterModDocstring) + @test isdefined(JIVisible.OuterModDocstring, :InnerModDocstring) + # issue #538 @test !JuliaInterpreter.is_doc_expr(:(Core.@doc "string")) ex = quote @@ -44,11 +57,11 @@ end sum end modexs = collect(ExprSplitter(Main, ex)) - m, ex = first(modexs) + m, ex = first(modexs) # FIXME don't use index in tests @test !JuliaInterpreter.is_doc_expr(ex.args[2]) @test !isdefined(Main, :JIInvisible) - collect(ExprSplitter(JIVisible, :(module JIInvisible f() = 1 end))) + collect(ExprSplitter(JIVisible, :(module JIInvisible f() = 1 end))) # this looks up JIInvisible rather than create it @test !isdefined(Main, :JIInvisible) @test isdefined(JIVisible, :JIInvisible) mktempdir() do path @@ -71,14 +84,32 @@ end # Every package is technically parented in Main but the name may not be visible in Main @test isdefined(@__MODULE__, :TmpPkg1) @test !isdefined(@__MODULE__, :TmpPkg2) - collect(ExprSplitter(Main, quote - module TmpPkg2 - f() = 2 - end - end)) + collect(ExprSplitter(@__MODULE__, quote + module TmpPkg2 + f() = 2 + end + end)) @test isdefined(@__MODULE__, :TmpPkg1) @test !isdefined(@__MODULE__, :TmpPkg2) end + + # Revise issue #718 + ex = Base.parse_input_line(""" + module TestPkg718 + + module TestModule718 + export _VARIABLE_UNASSIGNED + global _VARIABLE_UNASSIGNED = -84.0 + end + + using .TestModule718 + + end + """) + for (mod, ex) in ExprSplitter(JIVisible, ex) + @test JuliaInterpreter.finish!(Frame(mod, ex), true) === nothing + end + @test JIVisible.TestPkg718._VARIABLE_UNASSIGNED == -84.0 end module Toplevel end @@ -495,6 +526,7 @@ end @testset "Recursive type definitions" begin # See https://github.com/timholy/Revise.jl/issues/417 + # See also the `Node` test above ex = :(struct RecursiveType x::Vector{RecursiveType} end) frame = Frame(Toplevel, ex) JuliaInterpreter.finish!(frame, true) @@ -523,9 +555,9 @@ end sin(foo) end) for (mod, ex) in ExprSplitter(@__MODULE__, ex) - @test JuliaInterpreter.finish!(Frame(mod, ex), true) === nothing + @test JuliaInterpreter.finish_and_return!(Frame(mod, ex), true) == sin(10) end - @test length(collect(ExprSplitter(@__MODULE__, ex))) == 1 + ex = :(begin 3 + 7 module Local @@ -534,7 +566,7 @@ end end end) modexs = collect(ExprSplitter(@__MODULE__, ex)) - @test length(modexs) == 2 + @test length(modexs) == 2 # FIXME don't use index in tests @test modexs[2][1] == getfield(@__MODULE__, :Local) for (mod, ex) in modexs @test JuliaInterpreter.finish!(Frame(mod, ex), true) === nothing @@ -560,7 +592,7 @@ end for (mod, ex) in modexs @test JuliaInterpreter.finish!(Frame(mod, ex), true) === nothing end - @test length(modexs) == 2 + @test length(modexs) == 2 # FIXME don't use index in tests ex = Base.parse_input_line(""" local foo = 10 @@ -570,5 +602,5 @@ end for (mod, ex) in modexs @test JuliaInterpreter.finish!(Frame(mod, ex), true) === nothing end - @test length(modexs) == 2 + @test length(modexs) == 2 # FIXME don't use index in tests end diff --git a/test/toplevel_script.jl b/test/toplevel_script.jl index de22a1c..069c2e2 100644 --- a/test/toplevel_script.jl +++ b/test/toplevel_script.jl @@ -27,7 +27,7 @@ f3(x::T, y::U...) where {T<:Integer,U} = U f3(x::Array{Float64,K}, y::Vararg{Symbol,K}) where K = K # Default args f4(x, y=0) = 1 -f4(x, y::Int=0) = 2 +f4(x, y::Int) = 2 f4(x::UInt, y="hello", z::Int=0) = 3 f4(x::Array{Float64,K}, y::Int=0) where K = K # Keyword args diff --git a/test/utils.jl b/test/utils.jl index 1a3f058..96729a0 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -31,13 +31,18 @@ end ## For running interpreter frames under resource limitations +if isdefined(Base.IRShow, :LineInfoNode) +struct Aborted # for signaling that some statement or test blocks were interrupted + at::Base.IRShow.LineInfoNode +end +else struct Aborted # for signaling that some statement or test blocks were interrupted at::Core.LineInfoNode end +end function Aborted(frame::Frame, pc) - src = frame.framecode.src - lineidx = src.codelocs[pc] + lineidx = JuliaInterpreter.codelocs(frame, pc) lineinfo = JuliaInterpreter.linetable(frame, lineidx; macro_caller=true) return Aborted(lineinfo) end