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

implement pure function annotations #13555

Merged
merged 5 commits into from
Oct 13, 2015
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
9 changes: 0 additions & 9 deletions base/abstractarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,6 @@ linearindexing(A::AbstractArray, B::AbstractArray...) = linearindexing(linearind
linearindexing(::LinearFast, ::LinearFast) = LinearFast()
linearindexing(::LinearIndexing, ::LinearIndexing) = LinearSlow()

# The real @inline macro is not available this early in the bootstrap, so this
# internal macro splices the meta Expr directly into the function body.
macro _inline_meta()
Expr(:meta, :inline)
end
macro _noinline_meta()
Expr(:meta, :noinline)
end

## Bounds checking ##
@generated function trailingsize{T,N,n}(A::AbstractArray{T,N}, ::Type{Val{n}})
n > N && return 1
Expand Down
9 changes: 7 additions & 2 deletions base/essentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ end
macro _noinline_meta()
Expr(:meta, :noinline)
end
macro _pure_meta()
Expr(:meta, :pure)
end


# constructors for Core types in boot.jl
Expand Down Expand Up @@ -74,15 +77,17 @@ macro generated(f)
end


@generated function tuple_type_head{T<:Tuple}(::Type{T})
function tuple_type_head{T<:Tuple}(::Type{T})
@_pure_meta
T.parameters[1]
end

isvarargtype(t::ANY) = isa(t,DataType)&&is((t::DataType).name,Vararg.name)
isvatuple(t::DataType) = (n = length(t.parameters); n > 0 && isvarargtype(t.parameters[n]))
unwrapva(t::ANY) = isvarargtype(t) ? t.parameters[1] : t

@generated function tuple_type_tail{T<:Tuple}(::Type{T})
function tuple_type_tail{T<:Tuple}(::Type{T})
@_pure_meta
if isvatuple(T) && length(T.parameters) == 1
return T
end
Expand Down
12 changes: 5 additions & 7 deletions base/expr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,16 @@ macro eval(x)
end

macro inline(ex)
esc(_inline(ex))
esc(isa(ex, Expr) ? pushmeta!(ex, :inline) : ex)
end

_inline(ex::Expr) = pushmeta!(ex, :inline)
_inline(arg) = arg

macro noinline(ex)
esc(_noinline(ex))
esc(isa(ex, Expr) ? pushmeta!(ex, :noinline) : ex)
end

_noinline(ex::Expr) = pushmeta!(ex, :noinline)
_noinline(arg) = arg
macro pure(ex)
esc(isa(ex, Expr) ? pushmeta!(ex, :pure) : ex)
end

## some macro utilities ##

Expand Down
198 changes: 148 additions & 50 deletions base/inference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const MAX_TYPE_DEPTH = 5
const MAX_TUPLETYPE_LEN = 8
const MAX_TUPLE_DEPTH = 4

type NotFound
immutable NotFound
end

const NF = NotFound()
Expand Down Expand Up @@ -534,7 +534,7 @@ function builtin_tfunction(f::ANY, args::ANY, argtype::ANY, vtypes::ObjectIdDict
return tf[3](argtypes...)
end

function isconstantfunc(f::ANY, sv::StaticVarInfo)
function isconstantref(f::ANY, sv::StaticVarInfo)
if isa(f,TopNode)
m = _topmod()
return isconst(m, f.name) && isdefined(m, f.name) && f
Expand All @@ -543,39 +543,46 @@ function isconstantfunc(f::ANY, sv::StaticVarInfo)
M = f.mod; s = f.name
return isdefined(M,s) && isconst(M,s) && f
end
if isa(f,Expr) && is(f.head,:call)
if length(f.args) == 3 && isa(f.args[1], TopNode) &&
is(f.args[1].name,:getfield) && isa(f.args[3],QuoteNode)
s = f.args[3].value
if isa(f.args[2],Module)
M = f.args[2]
else
M = isconstantfunc(f.args[2], sv)
if M === false
return false
end
M = _ieval(M)
if !isa(M,Module)
return false
if isa(f,Expr)
if is(f.head,:call)
if length(f.args) == 3 && isa(f.args[1], TopNode) &&
is(f.args[1].name,:getfield) && isa(f.args[3],QuoteNode)
s = f.args[3].value
if isa(f.args[2],Module)
M = f.args[2]
else
M = isconstantref(f.args[2], sv)
if M === false
return false
end
M = _ieval(M)
if !isa(M,Module)
return false
end
end
return isdefined(M,s) && isconst(M,s) && f
end
return isdefined(M,s) && isconst(M,s) && f
elseif is(f.head,:inert)
return f
end
return false
end

if isa(f,QuoteNode) && (isa(f.value, Function) || isa(f.value, IntrinsicFunction))
return f.value
end
if isa(f,Function) || isa(f,IntrinsicFunction)
if isa(f,QuoteNode)
return f
end
if isa(f,SymbolNode)
f = f.name
end
return isa(f,Symbol) && is_global(sv, f) && _iisconst(f) && f
if isa(f,Symbol)
return is_global(sv, f) && _iisconst(f) && f
end
if isa(f,GenSym) || isa(f,LambdaStaticData)
return false
end
return f
end

const isconstantref = isconstantfunc
const isconstantfunc = isconstantref

const limit_tuple_depth = t->limit_tuple_depth_(t,0)

Expand Down Expand Up @@ -650,25 +657,7 @@ function abstract_call_gf(f, fargs, argtype, e)
end
end
if istopfunction(tm, f, :promote_type) || istopfunction(tm, f, :typejoin)
la = length(argtypes)
c = cell(la)
for i = 1:la
t = argtypes[i]
if isType(t) && !isa(t.parameters[1],TypeVar)
c[i] = t.parameters[1]
else
return Type
end
end
if istopfunction(tm, f, :promote_type)
try
RT = Type{f(c...)}
return RT
catch
end
else
return Type{f(c...)}
end
return Type
end
# don't consider more than N methods. this trades off between
# compiler performance and generated code performance.
Expand Down Expand Up @@ -821,7 +810,109 @@ function abstract_apply(af, fargs, aargtypes::Vector{Any}, vtypes, sv, e)
return abstract_call(af, (), Any[Vararg{Any}], vtypes, sv, ())
end

function isconstantargs(args, argtypes::Vector{Any}, sv::StaticVarInfo)
if isempty(argtypes)
return true
end
if is(args,()) || isa(argtypes[end], Vararg)
return false
end
for i = 1:length(args)
arg = args[i]
t = argtypes[i]
if !isType(t) || has_typevars(t.parameters[1])
if isconstantref(arg, sv) === false
return false
end
end
end
return true
end

function _ieval_args(args, argtypes::Vector{Any}, sv::StaticVarInfo)
c = cell(length(args))
for i = 1:length(argtypes)
t = argtypes[i]
if isType(t) && !has_typevars(t.parameters[1])
c[i] = t.parameters[1]
else
c[i] = _ieval(isconstantref(args[i], sv))
end
end
return c
end

@pure function type_typeof(v::ANY)
if isa(v, Type)
return Type{v}
end
return typeof(v)
end

function pure_eval_call(f, fargs, argtypes, sv, e)
if !isa(f, Function) # TODO: maybe replace with :call?
return false
end
if !isconstantargs(fargs, argtypes, sv)
return false
end

args = _ieval_args(fargs, argtypes, sv)
tm = _topmod()
if isgeneric(f)
atype = Tuple{Any[type_typeof(a) for a in args]...}
meth = _methods(f, atype, 1)
if meth === false || length(meth) != 1
return false
end
meth = meth[1]::SimpleVector
linfo = try
func_for_method(meth[3], atype, meth[2])
catch
NF
end
if linfo === NF
return false
end
if !linfo.pure
typeinf(linfo, meth[1], meth[2], linfo)
if !linfo.pure
return false
end
end
elseif !isdefined(f, :code) || !f.code.pure
return false
end

local v
try
v = f(args...)
Copy link
Contributor

Choose a reason for hiding this comment

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

even if we trust the user that the function is pure, this looks dangerous. pure usually does not imply terminating (or reasonably fast anyway) so we should at least not use that name.

Copy link
Contributor

Choose a reason for hiding this comment

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

I do like that we can get rid of the ugly special cases in inference.jl, I'm just not sure this annotation should be user facing.

Copy link
Member Author

Choose a reason for hiding this comment

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

we trust the user that @generated functions are sane (which is how these particular functions used to be marked). what's wrong with trusting them that @pure is marked correctly? i don't plan on exporting @pure until it is more intelligent about those additional requirements (e.g. post-green-fairy, if ever)

Copy link
Member

Choose a reason for hiding this comment

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

Pure functions, and the associated compiler optimizations, are very useful for writing efficient library functions... it would be sad if this feature were planned to be confined to Base for the foreseeable future.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm fine with trusting the user, I just think the naming at least is a little too innocent. Pure is a factual assertion ("even if it may not look like so, this function can be considered side effect free"), whereas this annotation is an intent declaration ("blindly execute at compile time").
IMO a correct implementation of "pure" would at least terminate the evaluation after some constant number of steps. I am aware that this is basically infeasible today.

Copy link
Member Author

Choose a reason for hiding this comment

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

@carnval i'm not convinced that those require different annotations. it's true that's how it is annotated today, but I think there are a number of heuristics that can be incorporated into inference to improve the off-nominal behavior of this annotation without actually changing its meaning. these include:

  • execute an @pure function only if the return type was not perfectly inferred.
  • execute @pure functions for arguments that subtype Type more than for other argument types
  • implement execution time step limits

again, this is in contrast to the @generated functions and special-case functions that this PR replaces, for which the compiler has no flexibility except to blindly execute at compile time and hope it terminates / works.

but, this PR was intended to only address a couple of particular cases (and provide a slot to store the pure attribute). i'm expecting the gf to provide a more general framework and make better use of this information. in light of that, does this need tweaking now to make it compatible with your plans for that?

@stevengj I agree. However, this can only handle completely trivial expressions like pow(1, 2), and not x = 1; pow(x, 2), and it doesn't propagate the information in such a way that it could hoist this out of a loop. I'm not saying you can't mark any function with Base.@pure, but this just hasn't implement any of the associated compiler optimizations that make it useful for efficient library functions (outside of the type domain), or any of the safety measures that carnval is describing.

Copy link
Member

Choose a reason for hiding this comment

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

I agree that until the associated compiler optimizations are implemented, there is no point in exporting it. But presumably those are orthogonal features that will get implemented eventually — they are well-known and well-understood optimizations that have been implemented to some degree in many other languages.

On the other hand safety checks on pure annotations seem more like an open-ended research project, and I don't think exporting @pure need wait on these.

catch
return false
Copy link
Contributor

Choose a reason for hiding this comment

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

it should be ok to return bottom type here ?

end
if isa(e, Expr) # replace Expr with a constant
stmts = Any[] # check if any arguments aren't effect_free and need to be kept around
for i = 1:length(fargs)
arg = fargs[i]
if !effect_free(arg, sv, false)
push!(stmts, arg)
end
end
if isempty(stmts)
e.head = :inert # eval_annotate will turn this into a QuoteNode
e.args = Any[v]
else
e.head = :call # should get cleaned up by tuple elimination
e.args = Any[top_getfield, Expr(:call, top_tuple, stmts..., v), length(stmts) + 1]
end
end
return type_typeof(v)
end


function abstract_call(f, fargs, argtypes::Vector{Any}, vtypes, sv::StaticVarInfo, e)
t = pure_eval_call(f, fargs, argtypes, sv, e)
t !== false && return t
if is(f,_apply) && length(fargs)>1
af = isconstantfunc(fargs[2], sv)
if !is(af,false)
Expand Down Expand Up @@ -925,7 +1016,8 @@ function abstract_eval_call(e, vtypes, sv::StaticVarInfo)
if !(Function <: ft) && _iisdefined(:call)
call_func = _ieval(:call)
if isa(call_func,Function)
return abstract_call(call_func, e.args, Any[ft,argtypes...], vtypes, sv, e)
unshift!(argtypes, ft)
return abstract_call(call_func, e.args, argtypes, vtypes, sv, e)
end
end
return Any
Expand All @@ -942,7 +1034,8 @@ end

function abstract_eval(e::ANY, vtypes, sv::StaticVarInfo)
if isa(e,QuoteNode)
return typeof((e::QuoteNode).value)
v = (e::QuoteNode).value
return type_typeof(v)
elseif isa(e,TopNode)
return abstract_eval_global(_topmod(), (e::TopNode).name)
elseif isa(e,Symbol)
Expand Down Expand Up @@ -1018,6 +1111,9 @@ function abstract_eval(e::ANY, vtypes, sv::StaticVarInfo)
t = Function
elseif is(e.head,:copyast)
t = abstract_eval(e.args[1], vtypes, sv)
elseif is(e.head,:inert)
v = e.args[1]
return type_typeof(v)
else
t = Any
end
Expand Down Expand Up @@ -1712,6 +1808,9 @@ function typeinf_uncached(linfo::LambdaStaticData, atypes::ANY, sparams::SimpleV
getfield_elim_pass(fulltree.args[3], sv)
end
linfo.inferred = true
body = Expr(:block)
body.args = fulltree.args[3].args::Array{Any,1}
linfo.pure = popmeta!(body, :pure)[1]
fulltree = ccall(:jl_compress_ast, Any, (Any,Any), def, fulltree)
end

Expand Down Expand Up @@ -1778,6 +1877,8 @@ function eval_annotate(e::ANY, vtypes::ANY, sv::StaticVarInfo, decls, clo, undef
return e
#elseif is(head,:gotoifnot) || is(head,:return)
# e.typ = Any
elseif is(head,:inert)
return QuoteNode(e.args[1])
elseif is(head,:(=))
# e.typ = Any
s = e.args[1]
Expand Down Expand Up @@ -1967,10 +2068,7 @@ function exprtype(x::ANY, sv::StaticVarInfo)
return abstract_eval(x::Symbol, emptydict, sv)
elseif isa(x,QuoteNode)
v = (x::QuoteNode).value
if isa(v,Type)
return Type{v}
end
return typeof(v)
return type_typeof(v)
elseif isa(x,Type)
return Type{x}
elseif isa(x,LambdaStaticData)
Expand Down Expand Up @@ -2032,7 +2130,7 @@ function effect_free(e::ANY, sv, allow_volatile::Bool)
allow_volatile && return true
return isconst(e.mod, e.name)
end
if isconstantfunc(e, sv) !== false
if isconstantref(e, sv) !== false
return true
end
if isa(e,Expr)
Expand Down
3 changes: 2 additions & 1 deletion base/iterator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ immutable Zip{I, Z<:AbstractZipIterator} <: AbstractZipIterator
end
zip(a, b, c...) = Zip(a, zip(b, c...))
length(z::Zip) = min(length(z.a), length(z.z))
@generated function tuple_type_cons{S,T<:Tuple}(::Type{S}, ::Type{T})
function tuple_type_cons{S,T<:Tuple}(::Type{S}, ::Type{T})
@_pure_meta
Tuple{S, T.parameters...}
end
eltype{I,Z}(::Type{Zip{I,Z}}) = tuple_type_cons(eltype(I), eltype(Z))
Expand Down
Loading