-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Introduction of an inlinable mutable global binding #38588
Comments
This is unacceptable. |
Perhaps you are looking for c() = 0 which already does what you want and even allows changing types. Redefine the function, and Julia will recompile what it needs to (which is of course costly, but always guaranteed to do the Right Thing:tm:). The other standard idiom you can use is a 1-element container, such as const c = Ref(0) # access and modify c[] |
Fine. I stated it as a right that an implementation has. If an implementation does not like it, it can decide to always inline (and maybe document this choice). Most importantly, all the above is open to discussion. It is meant as a feature primarily focused at interactive use in order to reduce the need for restarts. Maybe an argumentation slightly more verbose than "This is unacceptable" would be helpful to clarify why you believe so. |
No, I'm not. One of the primary intended uses would be to redefine structures struct S1 ... end
inline S = S1
# play around with S, defining and calling functions that
# accept and return objects of type S, which currently is S1
struct S2 ... end
inline S = S2
# play around with S, defining and calling functions that
# accept and return objects of type S, which currently is S2 while developing a module, without the need for restarts. Currently, with |
No that's not the problem. The behavior for well defined code must not depend on whether the compiler decide to compile something, or whether some code is run. |
I can give you an example of how redefining structures would look from the user perspective. This can play the role of a motivation for this feature, but I don't want it to hijack the discussion. Anyway, let me introduce this macro macro redefinable(struct_def)
struct_def isa Expr && struct_def.head == :struct || error("struct definition expected")
is_unionall = false
if struct_def.args[2] isa Symbol
name = struct_def.args[2]
real_name = struct_def.args[2] = gensym(name)
elseif struct_def.args[2].head == :curly
is_unionall = true
name = struct_def.args[2].args[1]
real_name = struct_def.args[2].args[1] = gensym(name)
elseif struct_def.args[2].head == :<:
if struct_def.args[2].args[1] isa Symbol
name = struct_def.args[2].args[1]
real_name = struct_def.args[2].args[1] = gensym(name)
elseif struct_def.args[2].args[1].head == :curly
is_unionall = true
name = struct_def.args[2].args[1].args[1]
real_name = struct_def.args[2].args[1].args[1] = gensym(name)
else
error("expected `S <: AbstractType`")
end
else
error("expected `S` or `S <: AbstractType`")
end
if is_unionall
fix_name = :($real_name.body.name.name = $(QuoteNode(name)))
else
fix_name = :($real_name.name.name = $(QuoteNode(name)))
end
esc(quote
$struct_def
$fix_name
$name = $real_name # this should be `const $name = $real_name`
end)
end I know it may look scary, but what it does is quite simple. Let's say that you do abstract type A end
@redefinable struct S end
@redefinable struct S <: A end
@redefinable struct S{T} end
@redefinable struct S{T} <: A end At each step it defines a structure with a "secret" name Base.remove_linenums!(@macroexpand @redefinable struct S{T} <: A end) expands to struct var"##S#262"{T} <: A
end
(var"##S#262").body.name.name = :S
S = var"##S#262" As written in a comment toward the end of the macro, we would really want (Edit: if you don't like the tricky |
I agree, although at least in C there is the concept of "unspecified value", which is a different notion from undefined behavior. The behavior of the program is well defined, simply the value can be any valid value of the suitable type, and may depend on external factors not under the control of the programmer. With the freedom for the compiler not to inline, the global Anyway, I don't think this precludes the examination of this feature. I originally stated it in that more flexible way to leave more choice to the implementation. I now recognize that it was almost indisputably an error. It is better to ask that the compiler always inlines the value. |
And that's exactly what we don't want.
No it's not even about asking the compiler to always inline. The compiler isn't a concept that exist at as far as the user is concerned. There isn't a "compilation" step. The code can run with or without it so one must not make different decision (again for well defined code) to do different things depending on if and when the code is compiled. |
I'll quote and paraphrase myself to answer this.
Let me put it differently.
There is no mentioning of the compiler in the previous formulation. In my original post I spoke about the compiler to explain how it would kind of work internally. The specification of the meaning does not need to refer to the compiler. foo() = c # this is time t_d
# time t_s must be somewhere here in between
foo() # this is time t_c several There are several useful instances where there is a unique choice of the value to inline. And remember that this feature is particularly targeted at interactive use. I don't mind if it is decided to ban it from packages at an earlier stage. It is a mean of working around the current limitations for interactivity. I gave an example where this results in a completely unambiguous program. This program const c = 0
const c = 1 currently is (almost surely) undefined behavior. The linked issue (#38588) is about making it defined behavior (basically with the same meaning of This issue is about retaining the meaning of inline c = 0
inline c = 1
foo() = c
foo() # must return 1 a program with defined behavior. Or, if you prefer a more useful example: # 3 lines coming from an include(...)
struct S1 x::Int end
inline S = S1
foo(s::S) = ... s.x ... S(42)
# work with foo and S
# update the included file
struct S2 y::Float64 end
inline S = S2
foo(s::S) = ... s.y ... S(3.14)
# experiment with the new foo and S The macro I presented above can hide the existence of the names |
That's just a missing feature. If someone were to make a pull-request implementing local
The other option is just redefine the constant and carry on—it usually works just fine. The reason that it is still undefined behavior is that even though it usually works to redefine a constant, is that guaranteeing that nothing bad will happen is very hard and puts quite a burden on the compiler.
That would indeed by useful, but it seems like it would make more sense to directly request that as a feature instead of proposing a new language feature that it's not clear how one would use correctly. Would there be any justified usage of the proposed inline global feature? I can't think of any. Every use case in a final working program should actually be one of const, const Ref, or a function returning a value. Adding a language feature that should not be used is kind of strange. |
You just removed the mentioning of "compilation" but that doesn't fix any problem with this at all. It's just called "resolve" instead of "compile" now. That's not a concept that exist and must never have any user visible effect for well defined code. |
@FedericoStra I'm following julia development from the sidelines, so hopefully somebody more knowledgeable will correct me if I'm wrong, but the broader point is that the language is separate from the compiler, and that the language should be designed not for the compiler we have now but for the compiler we wish to have. While the julia language itself is quite stable and looks like it should be around for a good number of years, some of the compiler limitations can hopefully be lifted in future releases. It seems that the features that your proposal is meant to work around (struct redefinition, const-type global variables), which a lot of people want, are implementable in the current state of the language, it's "just" a question of somebody actually doing it. So the answer to your point "It's not Revise's fault, it's a shortcoming of the language" is no, it's "just" a shortcoming of the compiler. This is putting a lot of weight onto compiler people and is frustrating because in the short term we're missing important features, but it's a principle that seems to have served julia well up to now. It's tempting to introduce language features to address compiler problems, but it's hurtful in the long-term. |
"Resolves" means "evaluates to", not "compiles". Regarding the text in bold, that's just a plain wrong opinion. As I already said, at least in languages like C, there is the concept of unspecified value, which is different from undefined behavior. What it means is that any instance can evaluate to a value on which the language specification imposes no conditions apart from being valid for the relative type. The behavior is not undefined, because in particular the instance must evaluate to something valid. The semantic of You can even witness unspecified values in Julia. Quoting from the docs:
julia> struct HasPlain
n::Int
HasPlain() = new()
end
julia> HasPlain()
HasPlain(438103441441) I interpret that "undefined" at the end to mean actually the same thing as "unspecified value" from the C standard, and not that the previous program exhibits undefined behavior. There is in fact no mentioning in the Julia docs that accessing @StefanKarpinski I get your points, and from a "language purity" perspective I agree with you at 95%. I would just like to comment on this
Since redefining I would even go as far as saying that this @antoine-levitt Again, I agree at a 95% confidence level.
Maybe I'm misunderstanding what you mean, but if In order to have an interpreter/compiler that gives us the possibility to redefine |
The fundamental problems here are that 1) structs can't be redefined and 2) non-const globals are slow. If you relax 1) (which plausibly can be done without any other change to the language) and you add global-scope type assertions, you can make |
Now I agree 99% that this would be satisfactory. But please notice that by relaxing 1) you now allow Also, if I understand correctly, global-scope type assertions have a stricter semantics than c::Int = 0
foo() = c
foo()
c = 1
foo() # must be 1 versus inline c = -1
inline c = 0
foo() = c
foo()
inline c = 1
foo() # can be 0 or 1 Moreover, implementing global-scope type assertions may require complicate machinery. On the other hand, the semantics of |
Of course, but that's what matters anyway. It's the step during "compilation" that does what you want to do. Nothing else in the compilation matters here.
Which isn't always a good argument.
Well, first of all, by definition/as it stands, it is undefined behavior.
This is wrong. It IS undefined behavior. |
I think redefining structs unconditionally is a pretty good deal, as it would allow you to redefine any struct, not those explicitly marked redefinable. It would be analogous to function redefinition in that sense (note I have no idea if it's even feasible to implement...) : you can already redefine Your proposed inline feels to me like a misfeature explicitly introducing hidden state and compiler-dependent behavior, which is a magnet for subtle bugs. In a hypothetical future with redefinable structs and global-scope type assertions, it also doesn't feel too useful (although it would certainly be very useful in the short term). If your code really depends on inlining of a global non-const variable for performance, it's probably not a very good design (and it's explicitly discouraged style in julia) |
I keep referring to the C standard because, despite being considered a huge mess by the masses, at least it strives to give precise definitions of some concepts. In particular, "unspecified value" != "undefined behavior".
Are you 100% sure that struct HasPlain
n::Int
HasPlain() = new()
end
HasPlain().n
print("hello") is really meant to be undefined behavior by the language spec, and not an unspecified valid value (again, in the sense of the C standard)? If it really is undefined behavior, then it means that executing it may lead to a crash before printing I'm not saying that I know the answer. I can only read the docs that get published online, and they are not clear. It may very well be that this really is the intention in the minds of the developers of the language, but for sure it is not what is communicated through the documentation. |
Yes. Because this is mapped to undefined behavior in the compiler. |
Well, sure, it sounds cool, but quite dangerous too, arguably more than redefining
If it (rightfully) feels too dangerous, it can be disallowed in "final products" and enabled only in interactive sessions and modules under development. After all, this is its primary goal.
I'm not sure that global variables with type assertions will be able to be inlined, because you always want the newest value, hence, they will probably not have the same performance characteristics of
Again, I'm not advocating abuse of inline c = 42
inline S = struct ... end
f() = ... c ...
g(::S) = ... At the REPL, you keep reloading it with |
Where does the language specification say so? Also, accessing an uninitialized field currently isn't mapped to instant UB: struct HasPlain
n::Int
HasPlain() = new()
end
foo() = HasPlain().n
@code_llvm foo() shows define i64 @julia_foo_352() {
top:
ret i64 undef
} Producing or using in certain ways a value of
[plenty of examples omitted]
Some usages of
The Julia docs only say:
It doesn't say "reading the contents of an uninitialized field of plain data type is undefined behavior". Furthermore, accessing an uninitialized field which is not of plain data type is surely not undefined behavior:
julia> z.data
ERROR: UndefRefError: access to undefined reference Throwing an exception is most definitely not undefined behavior! You can catch the error and recover struct S
x
S() = new()
end
try
S().x
catch
print("nevermind")
end The docs only say that the contents is undefined. And indeed we have @code_llvm HasPlain() define [1 x i64] @julia_HasPlain_364() {
top:
ret [1 x i64] undef
} Your are interpreting the docs as saying that it must be UB, but it could only be because you are not aware of the difference between If the docs mean that it should be undefined behavior (which they are currently not saying clearly), then define [1 x i64] @julia_HasPlain_364() {
top:
ret [1 x i64] poison
} Here I don't want to debate anymore with you about your interpretations of what undefined behavior is. I want to know from a reputable source if the specification of the Julia language says anywhere that HasPlain().n
print("hello") exhibits undefined behavior and could crash without printing anything. I can open a separate issue specific for the question, because this discussion is getting sidetracked. |
No I'm not interpreting any docs, it is you that is reading the doc. I am merely telling you what are the intended behavior of the compiler and the runtime. Yes |
That may be the case currently, but I don't think any of us really want it to be that way --- we want Changing a constant is a different situation. Any situation where (1) the compiler is allowed to assume the value of something, but (2) that value might change, can lead to unsoundness, i.e. the compiler makes assumptions that are incorrect, which then must be undefined behavior (or a compiler bug). The value of the variable can have unlimited downstream effects, e.g. causing the return type of a function to change, so there's not really any such thing as "just" observing the value of the variable. The only way out of this would be to track dependencies on constant values the same way we do for method definitions, so we can recompile when the value changes. So far we've felt that would be a waste of effort, since redefining constants is not something a program should do anyway. However, maybe the use case of changing structs with Revise makes it worthwhile. |
Introduction of an inlinable mutable global binding
The main purpose of the
const
qualifier seems to be to allow the compiler to infer the type of global bindings and possibly inline their value when they are referenced inside functions.For instance, in this code
the inferred return type is
Tuple{Int64,Any}
, andc
is inlined and replaced byCore.Compiler.Const(0, false)
, whereasv
cannot be.Despite the name, the
const
qualifier doesn't seem too much concerned about const-correctness (preventing subtle bugs caused by inadvertently modifying a variable), since theconst
qualifier is not applicable to local variables to make the binding immutable, consequently limiting the usefulness of the qualifier in this regard. If const-correctness were a major concern, then why not allowconst
to be used in local scopes too?In summary, as it stands right now,
const
seems to be more about performance than correctness.Qualifying a global variables with
const
, however, has the (undesirable?) consequence that reassigning to the variable is probably undefined behavior (#38584), and the semantics of this does not seem likely to change.Especially during an interactive session, a user might feel the need to redefine a global
const
, because maybe it was mistakenly defined, or the user is experimenting and wants to compare different definitions. With the current semantics, however, the only option is to restart the session, with the obvious inconveniences of losing everything else currently in the session, paying the price of the overhead to load againusing ...
statements, etc. This hinders very much the interactive aspect of the language.The natural question to raise is whether it is appropriate to introduce a qualifier that retains the performance characteristics of
const
, whilst allowing reassignment without invoking undefined behavior.Proposal
I therefore propose the introduction of the following qualifier:
inline
.The precise form of this feature is of course subject to modification; here I'm using a new keyword
inline
just to exemplify the concept.The meaning of
is similar to
const c = value
, but with some crucial differences.c
is not allowed to change. If this is the first definition ofg
, then the type ofc
will be forever restricted to be the type ofvalue
. Ifc
was already defined withoutinline
, it is an error; ifvalue
is not of the frozen type thatc
already has, it is an error.c
, it is allowed to inline the valuevalue
instead and infer the frozen type as indicated in point 1. In particular, it is legal to reassign to the variablec
(if the right hand side has the correct type, of course), and the consequence is that whenever the compiler decides to inlinec
from this moment onward it uses this new value, unless overridden again.More specifically, let's say that a method
foo()
is defined at a certain timet_d
and called at a later timet_c
. At an intermediate timet_s ∈ [t_d, t_c]
the method is specialized and compiled. If the method references a global variableinline c
, then the compileris free tomust inline the value ofc
at the timet_s
. The exact time at which this happens (or whether it happens at all) can be implementation specific, or even unspecified; but the important difference relative toconst
is that reassignment is not undefined behavior.The compiler has the right not to inline the global binding. In particular, if it does so, the function will hence witness any future changes of the global variable. If the value is inlined instead, any future changes of the variable will be ignored and the function will always use the frozen value that the variable had at the time of specialization.(Edit: this paragraph is very questionable, and it is probably better to require that the compiler actually inlines the value. I recognize that my original phrasing was a mistake.)Comparisons
The proposed feature is extremely similar to how numba treats global variables:
The result can be either
0
or1
and depends on whether in the commented line we callfoo
(forcing it to compile at a time whenc == 0
) or not (forcing it to compile at a time whenc == 1
).Remarks
The proposed feature has the same performance characteristics of
const
, because the type can be inferred and the value can be inlined in the same manner.At the same time, the proposed feature allows for more flexible interactive use, because the behavior of the following program would be well defined
whereas the analogous program with
const
instead ofinline
requires a restart after line 3.The described semantics of the proposed features seems to be extremely close to the actual current implementation specific behaviour of
const
, hence it is plausible to imagine that implementinginline
would not require the addition of new intricate machinery to the internals of Julia.The text was updated successfully, but these errors were encountered: