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

Make Vararg not a DataType #38136

Merged
merged 1 commit into from
Dec 15, 2020
Merged

Make Vararg not a DataType #38136

merged 1 commit into from
Dec 15, 2020

Conversation

Keno
Copy link
Member

@Keno Keno commented Oct 22, 2020

Currently Vararg is a DataType, but is special cased in a
bunch of places to give it special behavior (e.g. in subtyping
and of course in tuple construction). However, unlike all other
DataTypes, it cannot appear as a type parameter, which caused trouble in
PR #38071. Having it be a DataType is a bit of a pun of convenience
in the first place - it's a lot more similar to a tvar (which can
be considered an implementation detail of UnionAll in the same way
Vararg is an implementation detail of Tuple), which has its own
non-type object. This PR does the same to Vararg, and moves it
from being an abstract DataType with special cased behavior to
its own custom type (called Core.VarargMarker). The user facing
behavior should be mostly unchanged, since boot.jl now has:

const Vararg{T, N} = VarargMarker{T, N}

i.e. we have a handly UnionAll wrapper that looks just like it
used to. The biggest difference is probably that VarargMarker
does not have .parameters, so code that tries to reach into
that explicitly will need to be adjusted. We could provide
a compatibility getproperty method to adapt that, but I'd
prefer to see how many packages need updating first, before
going that route.

This is essentially complete, but a few cleanup items remain.

Closes #30995 #34690 #26625 #37316

@martinholters
Copy link
Member

👍 Completely agree, Vararg is sufficiently different from a data type that it should probably not be a DataType. The code changes needed to deal with the new definition in my eyes also tend to improve clarity, which is good sign. It also makes it more obvious where a Vararg is to be expected and that it usually deserves some extra thought how to deal with it.

Because it took me moment to wrap my head around it, some remarks that might help others grok what's happening:
With this PR Core.VarargMarker{Int,2} isa Core.VarargMarker, while for a normal unionall/data type, one would expect a <: instead of the isa. And indeed, previously Vararg{Int,2} <: Vararg. (This errors on this PR, but maybe should be restored?) But note that e.g. also Union{Int,Float64} isa Union, so it's not completely new and exotic. Furthermore Vararg{T,N} == Core.VarargMarker{T,N} does not imply Vararg == Core.VarargMarker. The former is a UnionAll, the latter a DataType.

src/dump.c Outdated
write_uint8(s->s, TAG_VARARG);
jl_serialize_value(s, ((jl_vararg_marker_t*)v)->T);
jl_serialize_value(s, ((jl_vararg_marker_t*)v)->N);
}
Copy link
Member

Choose a reason for hiding this comment

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

Did you encounter any particular reason this is needed? I would think the default case would work for these.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, I thought I did, but I think the default is fine. I'll try removing it and see if things work out.

src/subtype.c Outdated
// N.B.: This case is only used for raw varargs that are not part
// of a tuple (those that are have special handling in subtype_tuple).
// Vararg isn't really a proper type, but it does sometimes show up
// as e.g. Type{Vararg}, so we'd like to handle that correctly.
Copy link
Member

Choose a reason for hiding this comment

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

Type{Vararg} is not allowed. Ideally we would give an error here too, but if that's too breaking we can instead just change this comment to say it's provided for compatibility with code that uses <: to compare Vararg objects.

Copy link
Member Author

Choose a reason for hiding this comment

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

This comment is old, it just moved from below. I can try giving an error and see what needs to be updated.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, as Martin noticed above it looks like passing Vararg to <: is already an error in this PR :) We can try to go with that, and change this to an assertion.

@JeffBezanson JeffBezanson added the minor change Marginal behavior change acceptable for a minor release label Oct 22, 2020
@JeffBezanson
Copy link
Member

Hmm, at first I thought it would be more compatible to keep Vararg::UnionAll true, but especially if we're going to change the fields of Vararg maybe it doesn't really matter. Getting rid of that constraint allows us to have one object Vararg instead of both Vararg and VarargMarker, plus Vararg{Int} isa Vararg is more directly analogous to Union{Int, Float64} isa Union. So maybe that is the right way to go.

@Keno
Copy link
Member Author

Keno commented Oct 22, 2020

Ok, I can try that.

@Keno
Copy link
Member Author

Keno commented Oct 24, 2020

I've updated this along these lines, disallowing Vararg inside a UnionAll, which I think is the right way to go, but we still end up with Vararg and VarargMarker, which need to be two separate things, since otherwise, you can't refer to the type of a Vararg (and people use a raw Vararg without any parameters as in Tuple{Vararg} and want that to mean Tuple{Vararg{Any}}, so Vararg can't be that type).

@yurivish
Copy link
Contributor

yurivish commented Oct 24, 2020

Would this change close #34690?

julia> methods(Core.Vararg) # This does not
ERROR: TypeError: in Type, in parameter, expected Type, got Vararg
Stacktrace:
 [1] signature_type(::Any, ::Any) at ./reflection.jl:767
 [2] _methods at ./reflection.jl:830 [inlined]
 [3] methods(::Any, ::Any) at ./reflection.jl:875
 [4] methods(::Any) at ./reflection.jl:890
 [5] top-level scope at REPL[24]:1

@Keno
Copy link
Member Author

Keno commented Oct 24, 2020

julia> methods(Core.Vararg)
# 0 methods:

;)

@Keno
Copy link
Member Author

Keno commented Oct 24, 2020

@nanosoldier runtests(ALL, vs = ":master")

base/boot.jl Outdated

UnionAll(v::TypeVar, @nospecialize(t)) = ccall(:jl_type_unionall, Any, (Any, Any), v, t)

const Vararg = ccall(:jl_toplevel_eval_in, Any, (Any, Any), Core, _expr(:new, VarargMarker))
Copy link
Member

Choose a reason for hiding this comment

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

In this case, maybe call it TypeofVararg to go with TypeofBottom? This could also use jl_new_struct instead of eval.

@nanosoldier
Copy link
Collaborator

Your package evaluation job has completed - possible new issues were detected. A full report can be found here. cc @maleadt

@Keno
Copy link
Member Author

Keno commented Oct 26, 2020

Alright, fully disallowing Vararg in UnionAll turned out to be a little aggressive. Apparently people like to write Tuple{Vararg{<:Real}} even though the <: doesn't do anything here (we already used to normalize on tuple construction to move it inside the vararg, so it doesn't even implicate the diagonal rule). I've added back that normalization into the UnionAll constructor, though it'll complain if depwarns are enabled, so we can remove it whenever people stop using this pattern or in 2.0 at the latest (it's actually only a few packages, so it's not too bad, but enough that I want to add the compat).

@Keno
Copy link
Member Author

Keno commented Oct 26, 2020

Alright, @JeffBezanson this works now, but there's some decisions to make for subtyping. Essentially, we need some sentinel value to indicate that a typevar must remain unbound. Previously, we'd just set it to the inner typevar, but we a) don't have that anymore and b) I don't think it necessarily makes much sense. In the commit I just pushed I'm using the vararg itself as the typevar, but of course the assertion that I added that we're not putting a vararg into subtyping fires there, so basically we need to decide what to use as the sentinel value here.

@Keno
Copy link
Member Author

Keno commented Oct 26, 2020

(I've pushed something I think is reasonable, but this stuff is pretty messy, since it isn't really designed to reason about int ranges)

@Keno
Copy link
Member Author

Keno commented Oct 26, 2020

@nanosoldier runtests(ALL, vs = ":master")

@nanosoldier
Copy link
Collaborator

Your package evaluation job has completed - possible new issues were detected. A full report can be found here. cc @maleadt

@Keno
Copy link
Member Author

Keno commented Oct 27, 2020

Alright, looks like we're down to just the StaticArrays open-coding unwrapva issue, which I have a PR for at JuliaArrays/StaticArrays.jl#843.

@Keno
Copy link
Member Author

Keno commented Oct 27, 2020

MbedTLS segfault is unrelated: JuliaLang/MbedTLS.jl#227

@@ -192,6 +192,8 @@ function sptypes_from_meth_instance(linfo::MethodInstance)
ty = UnionAll(tv, Type{tv})
end
end
elseif isa(v, Core.VarargMarker)
ty = Int
Copy link
Member

Choose a reason for hiding this comment

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

Fixes #37316?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes

Copy link
Member

Choose a reason for hiding this comment

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

It doesn't look like this was enough:

julia> f(::NTuple{N}) where {N} = N
f (generic function with 1 method)

julia> code_warntype(f, Tuple{Tuple})
Variables
  #self#::Core.Const(f)
  #unused#::Tuple{Vararg{T, N}} where T where N

Body::Any
1return $(Expr(:static_parameter, 1))

Or is this check flawed in some way?

src/subtype.c Outdated Show resolved Hide resolved
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.

Looking very good so far

@@ -1045,10 +1045,11 @@ function sp_type_rewrap(@nospecialize(T), linfo::MethodInstance, isreturn::Bool)
spsig = linfo.def.sig
if isa(spsig, UnionAll)
if !isempty(linfo.sparam_vals)
env = pointer_from_objref(linfo.sparam_vals) + sizeof(Ptr{Cvoid})
T = ccall(:jl_instantiate_type_in_env, Any, (Any, Any, Ptr{Any}), T, spsig, env)
sparam_vals = Any[isa(v, Core.VarargMarker) ? TypeVar(:N, Union{}, Any) :
Copy link
Member

Choose a reason for hiding this comment

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

Why would an sparam ever have been a Vararg object?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's new in this PR to mark an unbound intvalued parameter (We used to use the TypeVar of the unbound Vararg as the sentinel, but we don't have that anymore). Any other non-Type, non-Int sentinel value would do also. I suppose we could change the C interface from spvals to sptypes as we've done elsewhere. Since Const is now a builtin, that should actually work ok.

tt = tuple_tfunc(new_fields)
if isa(tt, PartialStruct)
if !(widenconst(tt) <: ti)
return PartialStruct(ti, tt.fields)
Copy link
Member

Choose a reason for hiding this comment

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

ti is not the typeof this object. That is always know to be exactly tt.typ (currently guaranteed by construction)

Copy link
Member Author

Choose a reason for hiding this comment

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

So the concern here is that we used to typeassert_type_instance the Vararg, which while I don't think is entirely correct, did give some precision that we're now losing here. I didn't see a great way to get that back without essentially duplicating intersection over the inference lattice, so I though at least this way we'd recover some precision.

return PartialStruct(ti, tt.fields)
end
elseif !(tt <: ti)
return ti
Copy link
Member

Choose a reason for hiding this comment

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

Unrelated?

@@ -220,6 +220,7 @@ widenconst(c::PartialTypeVar) = TypeVar
widenconst(t::PartialStruct) = t.typ
widenconst(t::Type) = t
widenconst(t::TypeVar) = t
widenconst(t::Core.VarargMarker) = t
Copy link
Member

Choose a reason for hiding this comment

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

Why would the typeof a variable be VarargMarker, which isn’t a type?

if isa(N, Int)
return length(p)::Int + N - 1, true
if isdefined(last, :N) && isa(last.N, Int)
return length(p)::Int + last.N - 1, true
Copy link
Member

Choose a reason for hiding this comment

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

Might need a type assert (someone seems to have put effort into them previously, so I assume it mattered for invalidation)

Copy link
Member Author

Choose a reason for hiding this comment

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

Type assert on last.N?

src/builtins.c Outdated Show resolved Hide resolved
src/jltypes.c Outdated Show resolved Hide resolved
@@ -220,6 +220,7 @@ widenconst(c::PartialTypeVar) = TypeVar
widenconst(t::PartialStruct) = t.typ
widenconst(t::Type) = t
widenconst(t::TypeVar) = t
widenconst(t::Core.VarargMarker) = t
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't widenconst not be called on one of these?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ideally yes, but currently inference is pretty liberal about calling both this and rewrap_unionall on non-Types and I just wanted to preserve the status quo for now. I'd argue a similar argument could be made for TypeVar just above.

base/essentials.jl Outdated Show resolved Hide resolved
base/essentials.jl Outdated Show resolved Hide resolved
@@ -627,7 +627,7 @@ function maybe_vararg_tuple_1()
end
@test Type{Tuple{Vararg{Int}}} <: Base.return_types(maybe_vararg_tuple_1, ())[1]
function maybe_vararg_tuple_2()
x = Type[Vararg{Int}][1]
x = [Vararg{Int}][1]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
x = [Vararg{Int}][1]
x = Any[Vararg{Int}][1]

Copy link
Member Author

Choose a reason for hiding this comment

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

This is already tested by the test just above. I figured the most natural translation would be to just have this be typed as typeof(Vararg)..

Keno added a commit that referenced this pull request Jan 10, 2021
This code has been dead since #38136. Clean it up some.
@timholy
Copy link
Member

timholy commented Jan 14, 2021

In the packages I've looked at, where this seems to be causing the most trouble is places like

map(T -> T <: Real, sig.parameters)

Is the absence of support for having Vararg on the left side of <: deliberate? If so, no worries, but if that's an oversight it might make sense to address it.

@Keno
Copy link
Member Author

Keno commented Jan 14, 2021

Is the absence of support for having Vararg on the left side of <: deliberate?

Yes, because the code you posted is probably buggy. You want map(T -> Base.unwrapva(T) <: Real, sig.parameters).

@bkamins
Copy link
Member

bkamins commented Mar 5, 2021

Does this change qualify to be included in NEWS.md section "deprecated" section?

ElOceanografo pushed a commit to ElOceanografo/julia that referenced this pull request May 4, 2021
Currently `Vararg` is a DataType, but is special cased in a
bunch of places to give it special behavior (e.g. in subtyping
and of course in tuple construction). However, unlike all other
DataTypes, it cannot appear as a type parameter, which caused trouble in
PR JuliaLang#38071. Having it be a DataType is a bit of a pun of convenience
in the first place - it's a lot more similar to a tvar (which can
be considered an implementation detail of UnionAll in the same way
Vararg is an implementation detail of Tuple), which has its own
non-type object. This PR does the same to Vararg, and moves it
from being an abstract DataType with special cased behavior to
its own custom type (called `Core.TypeofVararg`). There are a
few small behavior differences, but they are mostly internal.
In particular, we no longer have `Vararg <: Type` and Vararg
objects no longer have the .parameters field. Also, things like
`Vararg{T} where T` are technically illegal now since Vararg is
not a type. However, since a lot of people are using that pattern,
I've brought it back with a deprecation (which is of course off
by default). The only things that's disallowed is `Vararg{N, N} where N`,
but I haven't seen anybody use that.
ElOceanografo pushed a commit to ElOceanografo/julia that referenced this pull request May 4, 2021
This code has been dead since JuliaLang#38136. Clean it up some.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
minor change Marginal behavior change acceptable for a minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Vararg behaves strangely in Core.Typeof
10 participants