Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/reach.jl
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,12 @@ function reach_script(
end
end
thunk = Core.eval(scratch, Expr(:function, Expr(:call, gensym(:script)), body))
r = reach(thunk, Tuple{}; maxdepth, maxcandidates, ignore)
# `invokelatest`, because the walk reads the bindings the script's own `const` lines were just
# evaluated into and this call's world age was fixed before they existed. Julia 1.12 warns —
# "Detected access to binding … in a world prior to its definition world" — and says it will
# be an error in a future version. The analysis reads globals out of the IR, which is what
# makes this the one place in the package that reaches a binding younger than its caller.
r = Base.invokelatest(reach, thunk, Tuple{}; maxdepth, maxcandidates, ignore)
return Reach(
path,
r.reached,
Expand Down
4 changes: 2 additions & 2 deletions test/spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ that is entirely `@test_broken` is a claim written down, not a check being run.
| `test_spec_foreign.jl` | 13 | 13 | 0 | marking a method on somebody else's generic — the `QAtlas.fetch` case |
| `test_spec_forms.jl` | 24 | 24 | 0 | the definition forms a real package hits on its second afternoon |
| `test_spec_integration.jl` | 19 | 19 | 0 | where the mark has to surface: docs, Aqua, releases, provenance, CI |
| `test_spec_lifecycle.jl` | 15 | 15 | 0 | the mark's EXIT, and an entry point that is a module rather than a function |
| `test_spec_lifecycle.jl` | 16 | 16 | 0 | the mark's EXIT, and an entry point that is a module rather than a function |
| `test_spec_profile.jl` | 43 | 43 | 0 | what a real run went through, how often, and how much of it |
| `test_spec_propagate.jl` | 20 | 20 | 0 | a caller that never names a marked thing still depends on it |
| `test_spec_verify.jl` | 9 | 9 | 0 | how well is a marked thing exercised by the tests |
| **10 files** | **179** | **179** | **0** | |
| **10 files** | **180** | **180** | **0** | |
<!-- END GENERATED -->

The table is generated and pinned by `test/test_spec_table.jl`, which fails if it goes stale —
Expand Down
47 changes: 47 additions & 0 deletions test/spec/test_spec_lifecycle.jl
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,53 @@ end
@test ExperimentalAPI.verdict(ExperimentalAPI.reach(Main.CleanModule)) === :clean
end

# Run in a child process and read its stderr, because the failure it guards is a WARNING today and
# an error later: reading a binding in a world prior to its definition world. `reach_script`
# evaluates a script's `const` lines into a scratch module and then analyses a thunk that names
# them, and the walk reads globals out of the IR — so it is the one place in this package that
# reaches a binding younger than its own caller.
#
# Two things about the child are load bearing, and the first cost a test that could not fail.
# `reach_script` is called from inside a FUNCTION: a caller's world age is fixed when it is
# entered, and at top level it moves with every statement, so nothing is caught there. And the
# child runs with the DEFAULT `depwarn`: measured on 1.12.2, `--depwarn=error` *suppresses* this
# warning rather than promoting it, which is the opposite of what Julia's own hint says.
const _WORLD_AGE_SCRIPT = """
using ExperimentalAPI
module L
using ExperimentalAPI
public marked, settled
@experimental "why" marked(x::Float64) = x * 1.0000001
"Settled."
settled(x::Float64) = x
end
# Called from inside a FUNCTION, which is the whole point: a caller's world age is fixed when it
# is entered, so the bindings `reach_script` creates while it runs are younger than the frame
# reading them. At top level the world age moves with each statement and nothing is caught.
function probe()
dir = mktempdir()
path = joinpath(dir, "figure.jl")
write(path, "using Main: L\nconst RESULT = L.marked(0.5)\nRESULT\n")
r = ExperimentalAPI.reach_script(path)
ExperimentalAPI.verdict(r) === :depends || error("expected :depends, got \$(ExperimentalAPI.verdict(r))")
return nothing
end
probe()
print("OK")
"""

@testset "reach_script does not read a binding younger than its caller" begin
cmd = `$(Base.julia_cmd()) --startup-file=no --project=$(Base.active_project()) -e $_WORLD_AGE_SCRIPT`
out = IOBuffer()
ok = success(pipeline(ignorestatus(cmd); stdout=out, stderr=out))
text = String(take!(out))
# Non-vacuity: the child really did the analysis, so a child that exited early for an
# unrelated reason cannot pass this.
@test occursin("OK", text)
@test ok
@test !occursin("world prior to its definition", text)
end

@testset "a script can be the entry point" begin
# The shape a researcher has: a file that produces a figure, not a package. The file must be
# written — `tempname()` alone throws regardless of the implementation.
Expand Down
Loading