Replies: 4 comments 6 replies
-
I haven't thought that deeply about it, but that does seem inconsistent. It seems that to fix this we would need to add backedges from types to other types. Do we stop there? What if the type of a field is given by a function call? We evaluate that function call at the time of type definition, but what if the definition of the function changes? Should we reevaluate the type definition too? That would suggest not only backedges from types definitions to other types definitions, but also from functions to types. I'm not against that, just trying to think through all the full logic here. |
Beta Was this translation helpful? Give feedback.
-
I'm not as certain that this is inconsistent — the distinction is between a definition and execution, no? Ignoring the redefinition part of it, I can also just write: g() = f()
f() = 2 Calling struct Wrapper
ib::InvalidatedBinding # This errors because InvalidatedBinding isn't defined yet
end
struct InvalidatedBinding
x::Float64
end I do suppose that "fixing" this would enable #269 without the need for an |
Beta Was this translation helpful? Give feedback.
-
To add to the library of use / corner cases, here's an example that I expect will be challenging, but arguably realistic: # This function has many methods, some of which depend on `InvalidatedBinding`
bar(a::Int) = InvalidatedBinding()
bar(b::Float64) = ValidBinding()
bar(c::Any) = nothing
# In `foo`, it ends up on the other side of a dynamic dispatch
function foo()
# ...
return bar(poorly_inferred_x)
end
# Does `list = ...` re-evaluate eagerly because `InvalidatedBinding` changed, even though
# the compiler doesn't actually "know" that you depended on it? (but you might have!)
const list = [foo() for el in items] The initializer case adds a lot of new challenges on top of the (already challenging!) type definition case IMO If you only have a static analysis (whether it's a new syntactic one in Revise or one extending the existing machinery in Compiler), you have a really hard time deciding "Did The other obvious challenge here is that initializers can frequently be side-effecting and have implicit dependencies between them. Do we want to consider them safe-for-re-evaluation to begin with? As a user, I'd certainly expect that: const list = InvalidatedBinding[ #= ... =# ] would re-evaluate when |
Beta Was this translation helpful? Give feedback.
-
This was a fundamental design decision. There is an evaluation time difference between the top level definition and the function body, so there is no inconsistency. The core language does not in general keep edges on definition effects. Revise can and should do that of course. |
Beta Was this translation helpful? Give feedback.
-
I'm not sure there's really anything broken here, but I do want to document my unease about one aspect of the binding partitioning work: it seems to change our notion of the meaning of code--in the sense of "source text"--with respect to world age. Specifically, for methods
we declare that
g
's reference tof
takes precedence over the specific definition off
at the timeg
was defined: henceforth,g()
should return 2. Meaning, the source code should reflect the current world even if it was written in an older one.However, when it comes to types, we do the opposite, with meaning being dependent on the time at which the code is written:
This error happens because in reality the definition of
Wrapper
isBut that's not visible in the source text. It's also applicable to anyone who wrote a method
before the change to
InvalidatedBinding
, because the real "meaning" of that method definition isI'm uncertain about whether we can, and would want to, change this behavior. Revise has made significant strides at this in timholy/Revise.jl#894, leveraging the fact that it has a cache of the original expressions for all the types and methods and can just re-evaluate them. But Julia doesn't, so if it can be changed it would have to be by a different mechanism. But maybe it doesn't even make sense to try: if a type changes its definition entirely, one might have to manually update much of the code that uses it anyway, and Revise's attempts to naively migrate to the new definition seem to hold some potential for trouble.
A related perspective is that if we decide the current path makes sense and I just need to finish the changes to Revise, it makes Julia less "complete" without Revise. Prior to binding partitioning, the only thing you needed Revise for was to pick up changes made by your editor: you could always manually re-evaluate the method in the REPL, which would invalidate precompiled code and any later usage of those methods would reflect the new definitions. But if one changes a type definition, you're effectively "deleting" all the methods that are restricted to that type, and would have to manually re-evaluate them all. In that sense, changing a type manually leaves your system in a broken state, and without a tool like Revise to systematically redefine the entire set of dependents, you have a fairly useless system. In the long run (after JuliaLowering), I expect much of Revise's core machinery to be done at the time code is parsed by Julia, and that Revise will become a stdlib, so this may not be as much of a problem as it might otherwise be.
Beta Was this translation helpful? Give feedback.
All reactions