Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve precision of tmeet for vararg PartialStruct #39437

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

martinholters
Copy link
Member

Redo of #39402 against master. It was a good idea not to merge #39402 as it likely would have had problems in corner cases. I tried to be more defensive here, but wouldn't be surprised if some of my (implicit) assumptions still turn out to be wrong...

@martinholters martinholters added the compiler:inference Type inference label Jan 28, 2021
Comment on lines +562 to +610
if isa(ti, DataType) && ti.name === Tuple.name
num_fields = length(ti.parameters)
isva = isvatuple(ti)
else
nfields_ti = nfields_tfunc(ti)
isva = !isa(nfields_ti, Const)
num_fields = isva ? length(v.fields) : (nfields_ti::Const).val
end
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too happy about this. What I really want to know is: What is the minimum number of fields of ti and can there be more (vararg)? I get that for the simple case (if branch), but not quite for the else branch. This is likely not the only place we need this, so I probably just missed an elegant solution to this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, this means that if we started with Tuple{Int, Vararg{Float64}}, and ended up with something shorter somehow, that we'd compute Tuple{Vararg{Int}} below, instead of Tuple{Vararg{Int, Float64}}. So perhaps we should write this with max(length(v.fields), (nfields_ti::Const).val::Int)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me illustrate with an example: Let

v == PartialStruct(Tuple{Int64, Vararg{Number}}, Any[Core.Const(1), Vararg{Number}]

and

t == Union{Tuple{Int,Int,Int},Tuple{Int,Float64,Float64,Float64}}

then we get ti == t. And now I'd like to figure out that ti has at least 3 fields. At present tmeet (with this PR) gives

Core.PartialStruct(Tuple{Int64, Vararg{Union{Float64, Int64}}}, Any[Core.Const(1), Vararg{Union{Float64, Int64}}])

but it could even be

Core.PartialStruct(Tuple{Int64, Union{Float64, Int64}, Union{Float64, Int64}, Vararg{Float64}}, Any[Core.Const(1), Union{Float64, Int64}, Union{Float64, Int64}, Vararg{Union{Float64, Int64}}])

(But maybe that's a bit too greedy and we leave it as is.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datatype_min_ninitialized now gives you the "minimum number of fields" query, and isknownlength gives you the isva query (might want to add UnionAll and Union handling though, since currently they only accept DataType)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it is worthwhile to extend these for UnionAll and Union (and use them here)? Maybe postpone to a follow-up PR?

end
end
if isva && isvarargtype(v.fields[end])
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!isvarargtype(v.fields[end]) should imply !isva, but I wanted to be sure we don't unnecessarily form a vararg in case type intersection produces too wide a result.

end
new_fields = Vector{Any}(undef, num_fields)
for i = 1:num_fields
new_fields[i] = tmeet(getfield_tfunc(v, Const(i)), widenconst(getfield_tfunc(ti, Const(i))))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t is actually expected to be a slightly stronger constraint here. We already know that the LHS is already <: widev (by original construction), so there's nothing to be gained by computing the tmeet with it. There's a (small) chance that typeintersect was approximate however, and thus wider than t.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By similar logic, the final return operation of this function typeintersect(widenconst(v), t) can also be strengthen:

...
elseif isa(v, Type)
    return typeintersect(v, t)
elseif !has_free_typevars(t) && typeintersect(widenconst(v), t) === Union{}
    return Bottom
end
return v # this leaf type can't be improved by intersection with type `t`

(which also subsumes the conditionals for Const and Conditional)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t is actually expected to be a slightly stronger constraint here.

Um, yes, I had been pondering t vs. ti for a moment here, basically came to the same conclusion, and then chose ti for reasons I can't remember. I'll change this to t (or report back in case I remember what made we pick ti instead).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, I've even added a test for this: if v.typ == Tuple{Int,Vararg{Number}} and t = Union{Tuple{Int, Int, Int}, Tuple{Vararg{Float64}}}, then ti == Tuple{Int, Int, Int}. And tmeet(Number, getfield_tfunc(ti, Const(2)) == Int while tmeet(Number, getfield_tfunc(t, Const(2)) == Union{Int,Float64}. (And this doesn't depend on Vararg, that's just what the test does.)
Admittedly, a Union on the RHS of a type-assert might not be to worry about too much, so maybe still better to use t, live with the imprecision in this case but be safe against approximation in intersection? Not sure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the second comment (about the final return), I took that as inspiration for a slightly different restructuring of the code. Please take a look whether you like. That also made m wonder about the handling of t with free typevars here, see the respective comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getfield_tfunc(v, Const(i)) seems so much more costly than v.fields[i], though perhaps it isn't significant overall. How about:

Suggested change
new_fields[i] = tmeet(getfield_tfunc(v, Const(i)), widenconst(getfield_tfunc(ti, Const(i))))
new_fields[i] = tmeet(unwrapva(v.fields[min(i, end)]), widenconst(getfield_tfunc(ti, Const(i))))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Included in rebase.

@martinholters martinholters force-pushed the mh/improve-tmeet-for-vararg-partialstruct branch from 9a6774c to e1aaad6 Compare February 1, 2021 13:59
if isa(v, Type)
return typeintersect(v, t)
end
has_free_typevars(t) && return v
Copy link
Member Author

@martinholters martinholters Feb 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is basically an NFC, but it looks a bit funny this way: Is it really safe to rely on the type intersection if isa(v, Type) even if has_free_typevars(t)? And if so, why is it not safe if v is something else (then using widev = widenconst(v) of course)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to stop them from appearing, but that's a project for later. Mostly likely it would actually over-estimate the intersection (which is generally what we do here too), so we only try to avoid them most of the time.

if new_fields[i] === Bottom
return Bottom
end
if isa(ti, DataType) && ti.name === Tuple.name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we just assert that ti.name === Tuple.name on the previous line?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a somewhat indirect way and with some assumptions on the well-behavedness of typeinstersect. So file this under defensive programming(and the === should be cheap enough that it's ok).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, huh, is the assertion actually valid? I think it implies that if the PartialStruct is for something other than a tuple, its typ (aka widev) is concrete, so that either widev < t or typeintersect(widev, t) == Union{}. While that seems to hold at the moment, it also seems to be something that could be reasonably relaxed in the future. Maybe better replace the assertion with an if and just return the intersection result in the else branch?

Copy link
Member

@vtjnash vtjnash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I still see this as reasonable, but needs a rebase, and possibly some extra care on some edge cases?

@martinholters martinholters force-pushed the mh/improve-tmeet-for-vararg-partialstruct branch from e1aaad6 to b4434a6 Compare July 26, 2021 11:13
@martinholters
Copy link
Member Author

martinholters commented Jul 26, 2021

Overall, I still see this as reasonable, but needs a rebase

Done.

and possibly some extra care on some edge cases?

Anything specific you have in mind?

Copy link
Member

@vtjnash vtjnash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the remaining case to consider was:

julia> Core.Compiler.tmeet(
Core.Compiler.PartialStruct(Tuple{Integer, Vararg{Float64}}, Any[Core.Compiler.Const(3), Any, Any]),
Tuple{Int, Vararg{AbstractFloat}})

We likely should not be able to form that, since tuple_tfunc expects the number of parameters to equal the number of arguments, so we should not be able to get here with a varargs PartialTuple, but may need to make sure that case really does not appear.

@martinholters
Copy link
Member Author

Not quite sure what you are getting at. Of course we can form varargs PartialStructs (e.g. for (1, [2,3]...)), but their typ and fields should be consistent. With "need to make sure that case really does not appear", do you mean an audit of the calls to the PartialStruct constructor? Even add assertions there? Or assert in tmeet? Or just handle the case gracefully?

Just to be clear, the two minimally-modified-to-obtain-consistency versions of above example work as they should with this PR:

julia> Core.Compiler.tmeet(
       Core.Compiler.PartialStruct(Tuple{Integer, Vararg{Float64}}, Any[Core.Compiler.Const(3), Vararg{Any}]),
       Tuple{Int, Vararg{AbstractFloat}})
Core.PartialStruct(Tuple{Int64, Vararg{Float64}}, Any[Core.Const(3), Vararg{Float64}])

julia> Core.Compiler.tmeet(
       Core.Compiler.PartialStruct(Tuple{Integer, Float64, Float64}, Any[Core.Compiler.Const(3), Any, Any]),
       Tuple{Int, Vararg{AbstractFloat}})
Core.PartialStruct(Tuple{Int64, Float64, Float64}, Any[Core.Const(3), Float64, Float64])

(And it's unclear to me which one we should pick in case we go the handle-gracefully route.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler:inference Type inference
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants