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
10 changes: 9 additions & 1 deletion src/reach.jl
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,15 @@ function _callee_name(ci, @nospecialize(x))
v isa Type && return nameof(v)
end
w = _widen(t === nothing ? Any : t)
w isa DataType && isdefined(w, :instance) && return nameof(w.instance)
if w isa DataType && isdefined(w, :instance)
# `nameof` accepts a `Function`, a `Type` or a `Module` and nothing else. A struct whose
# fields are all singletons is itself a singleton, so `w.instance` exists for callables
# that are none of the three — `Base.MappingRF{…}`, which is what `sum(f(x) for x in xs)`
# lowers to. Asking that for a name threw a `MethodError` out of an analysis whose entire
# contract is to come back with one of three verdicts.
inst = w.instance
inst isa Union{Function,Type,Module} && return nameof(inst)
end
return :?
end

Expand Down
4 changes: 2 additions & 2 deletions test/spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ that is entirely `@test_broken` is a claim written down, not a check being run.
| `test_spec_integration.jl` | 19 | 19 | 0 | where the mark has to surface: docs, Aqua, releases, provenance, CI |
| `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` | 45 | 45 | 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_propagate.jl` | 21 | 21 | 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** | **186** | **186** | **0** | |
| **10 files** | **187** | **187** | **0** | |
<!-- END GENERATED -->

The table is generated and pinned by `test/test_spec_table.jl`, which fails if it goes stale —
Expand Down
43 changes: 42 additions & 1 deletion test/spec/test_spec_propagate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ public unstable,
CONSTANT_BAD,
top_uses_const,
MarkedStruct,
top_constructs
top_constructs,
gen_bad,
gen_good,
comp_bad,
map_bad,
loop_good

@experimental "convergence is not established below β ≈ 0.1" unstable(x::Float64) =
x * 1.0000001
Expand Down Expand Up @@ -89,6 +94,20 @@ top_uses_const(x::Float64) = x + CONSTANT_BAD
end
top_constructs(x::Float64) = MarkedStruct(x).v

# Higher-order shapes. `sum(f(x) for x in xs)` is the idiom this package's own docstrings use, and
# it reaches the marked definition through a closure the caller never names.
gen_bad(xs::Vector{Float64}) = sum(unstable(x) for x in xs)
gen_good(xs::Vector{Float64}) = sum(solid(x) for x in xs)
comp_bad(xs::Vector{Float64}) = [unstable(x) for x in xs]
map_bad(xs::Vector{Float64}) = sum(map(unstable, xs))
loop_good(xs::Vector{Float64}) = (
t=0.0;
for x in xs
t += solid(x)
end;
t
)

end # module Chain

const ENTRY = Tuple{Float64}
Expand Down Expand Up @@ -199,6 +218,28 @@ end

# ── termination ──────────────────────────────────────────────────────────────────────────────

@testset "a generator argument is answered, not thrown out of" begin
# `sum(f(x) for x in xs)` lowers to a `Base.MappingRF` whose two fields are both singletons,
# which makes the STRUCT a singleton — so `w.instance` exists for a callable that is neither a
# `Function` nor a `Type`. `nameof` has no method for that, and the analysis died with a
# `MethodError` instead of returning one of its three verdicts. Measured on the shape this
# package's own `@entered` docstring uses as its worked example.
#
# A throw is not a fourth verdict. `:unknown` is what "could not resolve this" is for.
for (f, want) in (
(Chain.gen_bad, :depends),
(Chain.gen_good, :clean),
(Chain.comp_bad, :depends),
(Chain.map_bad, :depends),
(Chain.loop_good, :clean),
)
@testset "$(nameof(f))" begin
r = ExperimentalAPI.reach(f, Tuple{Vector{Float64}})
@test ExperimentalAPI.verdict(r) === want
end
end
end

@testset "self-recursion terminates and still finds the mark" begin
@test ExperimentalAPI.verdict(
ExperimentalAPI.reach(Chain.top_recursive, Tuple{Int,Float64})
Expand Down
Loading