JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.71k stars 5.49k forks source link

Behavior of reassignment to a `const` #38584

Open FedericoStra opened 3 years ago

FedericoStra commented 3 years ago

I think that the semantics of const is not specified in the documentation clearly enough. In both Scope of Variables » Constants and Base » Essentials » Keywords » const the word "undefined" does not appear a single time.

What is the behavior of the following code?

const c = 0
const c = 1
print(c)

Is it really undefined behavior?

I see two major answers.

1. It is UB

Then the program above is allowed to do whatever it wants, including printing 0, printing 1, printing 2, printing "hello", not printing anything, crashing, hanging indefinitely, ...

According to the docs:

In some cases changing the value of a const variable gives a warning instead of an error. However, this can produce unpredictable behavior or corrupt the state of your program, and so should be avoided. This feature is intended only for convenience during interactive use.

However, if it is UB, then redefining a const is of no use whatsoever because there is no guarantee that it will do anything meaningful at all. Even during an interactive session, what follows has completely no sense because the whole behavior is undefined.

If this is the case, I think this point should be made more clear in the docs.

2. It is not UB, just "unspecified value"

Another possible interpretation is that when doing const c = some_new_value, from this moment on every reference to the name c can resolve to any value that the "constant" c has ever had, including some_new_value. This mean that the "value" of c may be unpredictable, maybe even unspecified, but the behavior of the program as a whole is not undefined in the strict sense.

Within this interpretation the following code is allowed to compute the surprising result (1, 0), but is not allowed to crash, hang, return 42, etc...

const c = 0
f() = c
f()
c = 1
g() = (c, f())
g() # currently outputs (1, 0)

Question

My personal interpretation from reading the docs is that the intent leans more towards option 2 (unspecified value), but I'm asking here to be sure (and possibly improve the documentation).

What is the correct interpretation of the meaning of const?

yuyichao commented 3 years ago

It's undefined and the compiler is allowed to do anything if you did that. For each specific version (or even a wide range of julia versions) it'll of course have a specific behavior and the compiler will usually no go out of its way to break your code. However, anything can still happen.

FedericoStra commented 3 years ago

May I ask then why not make it an error instead? Having it be UB implies that anything following it (actually, even preceding) is completely meaningless. In this particular instance, this is avoidable and it does not seem useful at all, but rather only dangerous instead.

I hope you realize what are the extreme consequences of having it be UB, but still letting users do it nonetheless. I would like to be sure that there is a clear unanimous consensus among the core team that this is indeed the intended semantics of const. And if it is so then I feel that it should be stated more clearly in the docs.

There are several viable alternatives to having it be UB, so even if it currently is, maybe it is beneficial to discuss whether it could/should be relaxed to a less dramatic "unpredictable behavior" (quotation from the docs).

Just to name a few alternatives, from stronger to weaker guarantees, when we reassign to const c:

  1. const c = some_value could mean that, from the moment of this reassignment onward, any past and future use of the global variable c can resolve to any value that the variable c has ever had up to this point, including some_value, and every access can be resolved with different values. Basically, every time c is resolved, it can take any value that was assigned up to this point. This seems to be the actual behavior of the current implementation, at least.

  2. const c = ... could mean that, from the moment of this reassignment onward, any past and future use of the global variable c can resolve to any value which valid for the type of c. This is akin to "unspecified value" from the C standard:

    3.19.31 unspecified value valid value of the relevant type where this International Standard imposes no requirements on which value is chosen in any instance

  3. const c = ... could mean that, from the moment of this reassignment onward, any past and future use of the global variable c can resolve to any value which is valid for the type of c or even to invalid values. This is akin to "indeterminate value" from the C standard:

    3.19.21 indeterminate value either an unspecified value or a trap representation (an object representation that need not represent a value of the object type)

The quotes of the C standard are taken from this draft, because I don't have 200 $ to buy the standard.

tpapp commented 3 years ago

why not make it an error instead?

That would make sense, especially if it was optional (like --depwarn), perhaps defaulting to error for non-interactive use.

FedericoStra commented 3 years ago

If reassigning to a const is truly meant to be undefined behavior, and this is not subject to change, then I have a proposal (#38588) for a new feature.

StefanKarpinski commented 3 years ago

This is genuine undefined behavior. We allow it because it's useful and usually doesn't cause too many problems. Most of the time all that happens is that old definitions give answers that are inconsistent with the new value, but it's entirely possible that some optimization could cause worse things to happen. It would, however, be a lot of effort and fairly pointless to try to limit how bad things can get when you redefine a constant. The error message is already quite alarming (intentionally). This should only be done while working interactively: do not redefine constants in any final working code; if you redefine a constant and your program does something weird, restart Julia. In general, your final working programs should never emit warnings.

FedericoStra commented 3 years ago

That's completely fine for me. It makes sense and I agree that this is how things should be. I still feel that the documentation is a bit lacking on this point. I'll try to come up with a PR for improving the docs as soon as I find the time to write it properly.

StefanKarpinski commented 3 years ago

Great, thanks. Clarification would certainly be helpful.

FedericoStra commented 3 years ago

I'm sorry to come back without a finished PR, but while working on it I'm still really confused by the meaning of current docs Scope of Variables » Constants because they seem highly contradictory with what is claimed in this issue. Stripping out the examples, they write:

Additionally when one tries to assign a value to a variable that is declared constant the following scenarios are possible:

  1. if a new value has a different type than the type of the constant then an error is thrown: [...]
  2. if a new value has the same type as the constant then a warning is printed: [...]
  3. if an assignment would not result in the change of variable value no message is given: [...]

The last rule applies for immutable objects even if the variable binding would change, e.g.: [...]. However, for mutable objects the warning is printed as expected: [...]

In particular, it appears to me that they are prescribing a very well defined behavior.

Is this section prescribing the meaning of the abstract language or is it merely describing the current implementation-specific behavior? Is scenario 1 guaranteed to throw an error? This would be well defined behavior and the error could be caught. Is scenario 3 guaranteed to be a valid code?

I guess every choice would lead to a valid specification of the language, I just don't know what is your intention because it is not clear from the docs, hence I don't know how to fix them.

To exemplify my doubt, is the following code intended to be undefined behavior or is it guaranteed to print "hi"?

const c = 1
try
    c = "different type" # scenario 1: must throw an error
catch
    print("hi")
end
FedericoStra commented 3 years ago

Maybe the "correct" choice is the following?

  1. the new value has a different type: an error is thrown (well-defined behavior)
  2. same type but different value: undefined behavior (and the current implementation-defined behavior is to print a warning)
  3. same type and same value: no-op (well-defined behavior)
KristofferC commented 3 years ago

That seems reasonable to me at least.

StefanKarpinski commented 3 years ago

Yes, I think only the middle case can cause problems. Would be good for @JeffBezanson to confirm.