Skip to content

Commit

Permalink
effects: taint :nothrow effect on unknown :static_parameter (#46791)
Browse files Browse the repository at this point in the history
* effects: taint `:nothrow` effect on unknown `:static_parameter` (conservatively)

With this commit, we taint `:nothrow` effect property correctly on
access to unknown `:static_parameter`, e.g.:
```julia
unknown_sparam_throw(::Union{Nothing, Type{T}}) where T = (T; nothing)
@test Core.Compiler.is_nothrow(Base.infer_effects(unknown_sparam_throw, ((Type{Int},))))
@test !Core.Compiler.is_nothrow(Base.infer_effects(unknown_sparam_throw, ((Nothing,))))
```

This commit implements a very conservative analysis, and thus there is a
room for improvement still, e.g.:
```julia
unknown_sparam_nothrow(x::Ref{T}) where {T} = (T; nothing)
@test_broken Core.Compiler.is_nothrow(Base.infer_effects(unknown_sparam_nothrow, (Ref,)))
```

* inference: improve `:nothrow` modeling for `:static_parameter` (#46820)

* Fix test with free type params

* Test: Ignore ::Type{T} in detect_unbounded

These are only technically unbounded because of the existence of
ill-formed types. However, this function is supposed to be an API
sanity check and ordinary users should never have ill-formed types,
so for the purpose we want here, allow unboundedness in Type{T}.

---------

Co-authored-by: Keno Fischer <keno@juliacomputing.com>
Co-authored-by: Elliot Saba <staticfloat@gmail.com>
  • Loading branch information
3 people committed Feb 8, 2023
1 parent 3a92d38 commit b5d17ea
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 59 deletions.
11 changes: 9 additions & 2 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2189,9 +2189,17 @@ function abstract_eval_value_expr(interp::AbstractInterpreter, e::Expr, vtypes::
head = e.head
if head === :static_parameter
n = e.args[1]::Int
nothrow = false
if 1 <= n <= length(sv.sptypes)
rt = sv.sptypes[n]
if is_maybeundefsp(rt)
rt = unwrap_maybeundefsp(rt)
else
nothrow = true
end
end
merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; nothrow))
return rt
elseif head === :boundscheck
if isa(sv, InferenceState)
# If there is no particular `@inbounds` for this function, then we only taint `:noinbounds`,
Expand Down Expand Up @@ -2452,8 +2460,7 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp
elseif isexpr(sym, :static_parameter)
n = sym.args[1]::Int
if 1 <= n <= length(sv.sptypes)
spty = sv.sptypes[n]
if isa(spty, Const)
if !is_maybeundefsp(sv.sptypes, n)
t = Const(true)
end
end
Expand Down
103 changes: 95 additions & 8 deletions base/compiler/inferencestate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -348,23 +348,109 @@ function InferenceState(result::InferenceResult, cache::Symbol, interp::Abstract
return InferenceState(result, src, cache, interp)
end

"""
constrains_param(var::TypeVar, sig, covariant::Bool, type_constrains::Bool)
Check if `var` will be constrained to have a definite value
in any concrete leaftype subtype of `sig`.
It is used as a helper to determine whether type intersection is guaranteed to be able to
find a value for a particular type parameter.
A necessary condition for type intersection to not assign a parameter is that it only
appears in a `Union[All]` and during subtyping some other union component (that does not
constrain the type parameter) is selected.
The `type_constrains` flag determines whether Type{T} is considered to be constraining
`T`. This is not true in general, because of the existence of types with free type
parameters, however, some callers would like to ignore this corner case.
"""
function constrains_param(var::TypeVar, @nospecialize(typ), covariant::Bool, type_constrains::Bool=false)
typ === var && return true
while typ isa UnionAll
covariant && constrains_param(var, typ.var.ub, covariant, type_constrains) && return true
# typ.var.lb doesn't constrain var
typ = typ.body
end
if typ isa Union
# for unions, verify that both options would constrain var
ba = constrains_param(var, typ.a, covariant, type_constrains)
bb = constrains_param(var, typ.b, covariant, type_constrains)
(ba && bb) && return true
elseif typ isa DataType
# return true if any param constrains var
fc = length(typ.parameters)
if fc > 0
if typ.name === Tuple.name
# vararg tuple needs special handling
for i in 1:(fc - 1)
p = typ.parameters[i]
constrains_param(var, p, covariant, type_constrains) && return true
end
lastp = typ.parameters[fc]
vararg = unwrap_unionall(lastp)
if vararg isa Core.TypeofVararg && isdefined(vararg, :N)
constrains_param(var, vararg.N, covariant, type_constrains) && return true
# T = vararg.parameters[1] doesn't constrain var
else
constrains_param(var, lastp, covariant, type_constrains) && return true
end
else
if typ.name === typename(Type) && typ.parameters[1] === var && var.ub === Any
# Types with free type parameters are <: Type cause the typevar
# to be unconstrained because Type{T} with free typevars is illegal
return type_constrains
end
for i in 1:fc
p = typ.parameters[i]
constrains_param(var, p, false, type_constrains) && return true
end
end
end
end
return false
end

"""
MaybeUndefSP(typ)
is_maybeundefsp(typ) -> Bool
unwrap_maybeundefsp(typ) -> Any
A special wrapper that represents a static parameter that could be undefined at runtime.
This does not participate in the native type system nor the inference lattice,
and it thus should be always unwrapped when performing any type or lattice operations on it.
"""
struct MaybeUndefSP
typ
MaybeUndefSP(@nospecialize typ) = new(typ)
end
is_maybeundefsp(@nospecialize typ) = isa(typ, MaybeUndefSP)
unwrap_maybeundefsp(@nospecialize typ) = isa(typ, MaybeUndefSP) ? typ.typ : typ
is_maybeundefsp(sptypes::Vector{Any}, idx::Int) = is_maybeundefsp(sptypes[idx])
unwrap_maybeundefsp(sptypes::Vector{Any}, idx::Int) = unwrap_maybeundefsp(sptypes[idx])

const EMPTY_SPTYPES = Any[]

function sptypes_from_meth_instance(linfo::MethodInstance)
toplevel = !isa(linfo.def, Method)
if !toplevel && isempty(linfo.sparam_vals) && isa(linfo.def.sig, UnionAll)
def = linfo.def
isa(def, Method) || return EMPTY_SPTYPES # toplevel
sig = def.sig
if isempty(linfo.sparam_vals)
isa(sig, UnionAll) || return EMPTY_SPTYPES
# linfo is unspecialized
sp = Any[]
sig = linfo.def.sig
while isa(sig, UnionAll)
push!(sp, sig.var)
sig = sig.body
sig = sig
while isa(sig, UnionAll)
push!(sp, sig.var)
sig = sig.body
end
else
sp = collect(Any, linfo.sparam_vals)
end
for i = 1:length(sp)
v = sp[i]
if v isa TypeVar
temp = linfo.def.sig
maybe_undef = !constrains_param(v, linfo.specTypes, #=covariant=#true)
temp = sig
for j = 1:i-1
temp = temp.body
end
Expand Down Expand Up @@ -402,12 +488,13 @@ function sptypes_from_meth_instance(linfo::MethodInstance)
tv = TypeVar(v.name, lb, ub)
ty = UnionAll(tv, Type{tv})
end
@label ty_computed
maybe_undef && (ty = MaybeUndefSP(ty))
elseif isvarargtype(v)
ty = Int
else
ty = Const(v)
end
@label ty_computed
sp[i] = ty
end
return sp
Expand Down
6 changes: 3 additions & 3 deletions base/compiler/optimize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,9 @@ function stmt_effect_flags(𝕃ₒ::AbstractLattice, @nospecialize(stmt), @nospe
if isa(stmt, Expr)
(; head, args) = stmt
if head === :static_parameter
etyp = (isa(src, IRCode) ? src.sptypes : src.ir.sptypes)[args[1]::Int]
# if we aren't certain enough about the type, it might be an UndefVarError at runtime
nothrow = isa(etyp, Const)
sptypes = isa(src, IRCode) ? src.sptypes : src.ir.sptypes
nothrow = !is_maybeundefsp(sptypes, args[1]::Int)
return (true, nothrow, nothrow)
end
if head === :call
Expand Down Expand Up @@ -377,7 +377,7 @@ function argextype(
sptypes::Vector{Any}, slottypes::Vector{Any})
if isa(x, Expr)
if x.head === :static_parameter
return sptypes[x.args[1]::Int]
return unwrap_maybeundefsp(sptypes, x.args[1]::Int)
elseif x.head === :boundscheck
return Bool
elseif x.head === :copyast
Expand Down
2 changes: 1 addition & 1 deletion base/compiler/ssair/slot2ssa.jl
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ end
function typ_for_val(@nospecialize(x), ci::CodeInfo, sptypes::Vector{Any}, idx::Int, slottypes::Vector{Any})
if isa(x, Expr)
if x.head === :static_parameter
return sptypes[x.args[1]::Int]
return unwrap_maybeundefsp(sptypes, x.args[1]::Int)
elseif x.head === :boundscheck
return Bool
elseif x.head === :copyast
Expand Down
2 changes: 1 addition & 1 deletion base/compiler/ssair/verify.jl
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ function verify_ir(ir::IRCode, print::Bool=true,
elseif stmt.head === :foreigncall
isforeigncall = true
elseif stmt.head === :isdefined && length(stmt.args) == 1 &&
(stmt.args[1] isa GlobalRef || (stmt.args[1] isa Expr && stmt.args[1].head === :static_parameter))
(stmt.args[1] isa GlobalRef || isexpr(stmt.args[1], :static_parameter))
# a GlobalRef or static_parameter isdefined check does not evaluate its argument
continue
elseif stmt.head === :call
Expand Down
45 changes: 1 addition & 44 deletions stdlib/Test/src/Test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1970,54 +1970,11 @@ function detect_unbound_args(mods...;
return collect(ambs)
end

# find if var will be constrained to have a definite value
# in any concrete leaftype subtype of typ
function constrains_param(var::TypeVar, @nospecialize(typ), covariant::Bool)
typ === var && return true
while typ isa UnionAll
covariant && constrains_param(var, typ.var.ub, covariant) && return true
# typ.var.lb doesn't constrain var
typ = typ.body
end
if typ isa Union
# for unions, verify that both options would constrain var
ba = constrains_param(var, typ.a, covariant)
bb = constrains_param(var, typ.b, covariant)
(ba && bb) && return true
elseif typ isa DataType
# return true if any param constrains var
fc = length(typ.parameters)
if fc > 0
if typ.name === Tuple.name
# vararg tuple needs special handling
for i in 1:(fc - 1)
p = typ.parameters[i]
constrains_param(var, p, covariant) && return true
end
lastp = typ.parameters[fc]
vararg = Base.unwrap_unionall(lastp)
if vararg isa Core.TypeofVararg && isdefined(vararg, :N)
constrains_param(var, vararg.N, covariant) && return true
# T = vararg.parameters[1] doesn't constrain var
else
constrains_param(var, lastp, covariant) && return true
end
else
for i in 1:fc
p = typ.parameters[i]
constrains_param(var, p, false) && return true
end
end
end
end
return false
end

function has_unbound_vars(@nospecialize sig)
while sig isa UnionAll
var = sig.var
sig = sig.body
if !constrains_param(var, sig, true)
if !Core.Compiler.constrains_param(var, sig, #=covariant=#true, #=type_constrains=#true)
return true
end
end
Expand Down
15 changes: 15 additions & 0 deletions test/compiler/effects.jl
Original file line number Diff line number Diff line change
Expand Up @@ -770,3 +770,18 @@ gotoifnot_throw_check_48583(x) = x ? x : 0
@test !Core.Compiler.is_nothrow(Base.infer_effects(gotoifnot_throw_check_48583, (Missing,)))
@test !Core.Compiler.is_nothrow(Base.infer_effects(gotoifnot_throw_check_48583, (Any,)))
@test Core.Compiler.is_nothrow(Base.infer_effects(gotoifnot_throw_check_48583, (Bool,)))


# unknown :static_parameter should taint :nothrow
# https://github.com/JuliaLang/julia/issues/46771
unknown_sparam_throw(::Union{Nothing, Type{T}}) where T = (T; nothing)
unknown_sparam_nothrow1(x::Ref{T}) where T = (T; nothing)
unknown_sparam_nothrow2(x::Ref{Ref{T}}) where T = (T; nothing)
@test Core.Compiler.is_nothrow(Base.infer_effects(unknown_sparam_throw, (Type{Int},)))
@test Core.Compiler.is_nothrow(Base.infer_effects(unknown_sparam_throw, (Type{<:Integer},)))
@test !Core.Compiler.is_nothrow(Base.infer_effects(unknown_sparam_throw, (Type,)))
@test !Core.Compiler.is_nothrow(Base.infer_effects(unknown_sparam_throw, (Nothing,)))
@test !Core.Compiler.is_nothrow(Base.infer_effects(unknown_sparam_throw, (Union{Type{Int},Nothing},)))
@test !Core.Compiler.is_nothrow(Base.infer_effects(unknown_sparam_throw, (Any,)))
@test Core.Compiler.is_nothrow(Base.infer_effects(unknown_sparam_nothrow1, (Ref,)))
@test Core.Compiler.is_nothrow(Base.infer_effects(unknown_sparam_nothrow2, (Ref{Ref{T}} where T,)))
13 changes: 13 additions & 0 deletions test/compiler/inference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4761,3 +4761,16 @@ g_no_bail_effects_any(x::Any) = f_no_bail_effects_any(x)

# issue #48374
@test (() -> Union{<:Nothing})() == Nothing

# :static_parameter accuracy
unknown_sparam_throw(::Union{Nothing, Type{T}}) where T = @isdefined(T) ? T::Type : nothing
unknown_sparam_nothrow1(x::Ref{T}) where T = @isdefined(T) ? T::Type : nothing
unknown_sparam_nothrow2(x::Ref{Ref{T}}) where T = @isdefined(T) ? T::Type : nothing
@test only(Base.return_types(unknown_sparam_throw, (Type{Int},))) == Type{Int}
@test only(Base.return_types(unknown_sparam_throw, (Type{<:Integer},))) == Type{<:Integer}
@test only(Base.return_types(unknown_sparam_throw, (Type,))) == Union{Nothing, Type}
@test_broken only(Base.return_types(unknown_sparam_throw, (Nothing,))) === Nothing
@test_broken only(Base.return_types(unknown_sparam_throw, (Union{Type{Int},Nothing},))) === Union{Nothing,Type{Int}}
@test only(Base.return_types(unknown_sparam_throw, (Any,))) === Union{Nothing,Type}
@test only(Base.return_types(unknown_sparam_nothrow1, (Ref,))) === Type
@test only(Base.return_types(unknown_sparam_nothrow2, (Ref{Ref{T}} where T,))) === Type

0 comments on commit b5d17ea

Please sign in to comment.