# Julep: Replaceable global bindings
Aka #265 for types. For context see: https://github.com/JuliaLang/julia/issues/40399
## Problem abstract:
It has long been acknowledged a minor wort that once defined, any constant object in Julia cannot be replaced or redefined. There are some exceptions and workarounds to this, but this is generally a nuisance and a hindrance to developer productivity. A fix for the similar issue for functions, often called by its issue number as 265, was merged many years ago. This enabled some powerful new workflows, powered by the Revise.jl package. However, despite some initial efforts to remove the restriction on redefining constants, the work did not address the correctness and behavioral questions we will attempt to define here.
To begin, lets start by considering all of the ways that users might refer to globals in their code:
```julia
module ABetterDeveloper
using AModule: AReplaceableExport as AReplacableImport
const AReplaceableConstant = AReplaceableImport
struct AReplacableStruct{T} end
function AReplaceableFunction(::AReplaceableImport, ::AReplaceableStruct{AReplaceableConstant}, ::typeof(AReplaceableFunction))
return getglobal(@__MODULE__, :AReplaceableStruct){
@__MODULE__.AReplaceableStruct{
AReplaceableStruct{AReplaceableConstant}}}
end
end
```
Which is about 9 different ways to express them.
This differs from methods, which really only had 4 ways to refer to them:
* An expression to call it later `() -> f(...)`
* A statement to call it now `const = f(...)`
* A macro to call it soon `macro @(); f(...); end`
* A generator expression to call it sometime `@generated () -> f(...)`
But these are not that significantly different from each other in the end. We defined an execution time for each, and then saved the result, not the dependencies that led to it (for that see the Juno.jl package instead).
## Proposals
Some proposed ways to implement this include the following ideas. In them, I will make use of the following glossary of terms:
- World: also referred to as Ages or a counter, this is a shorthand for referring to the current state of the program at a given moment in time. In the #265 fix, it could be used to distinguish what methods were available at a past age or time. In this context, it similarly refers to the order in which global constants are defined or changed over the course of program execution. Each change to a constant can be assumed to be numbered sequentially, allowing us to reference any previous point of time of the code execution, or World.
### Symbol hygiene
One option, similar to Mr. Moon’s PR for a replacement macro hygiene, would be to tag every symbol with the scope tag that tracks what world it belongs to. For example, to implement this, the first time a symbol gets redefined, any later time that symbol gets created in source code as a reference to the new type, it gets internally renamed to `Sym#1` then `Sym#2` and so on. This could even hypothetically be implemented purely inside an external package such as Revise, since it is a syntactic transform pass. This causes us to capture the symbol as it first appeared, so we can refine this also to cover some dynamic behaviors by defining a function `current_symbol_for(Module, Symbol, World)` and replacing the Symbol `Expr(:., Module, Symbol)` with this function call that looks up the current renaming of Symbol in Module as of the World in which this new method was defined.
This approach as the downsides of not being particularly reliable, and not being able to pick up new definitions ever, without redefining the functions themselves that use it. Additionally, the symbol itself has a different name, which can be quite unexpected for any code that uses reflection or dynamic lookups. Any sufficient opaque lookup will fallback to getting the oldest definition of the constant, which is rarely what is actually desired. Additionally, the old values may continue to “pollute” the namespace, even though users would likely prefer to be able to also delete or clear constants fully from their current view.
### TLS ages
Another option, likely the one most directly similar to the #265 fix, would be to make references to global symbols get their value at the current World age held in task local storage (TLS). Like with the fix for functions, any result of an expression would not be updated, but any new lookup of a global would reference the current value according to the age stored in TLS. This is possibly the conceptually simplest option. But there is a tricky exception: constructor-like expressions. If no references to the old value appeared anywhere, this option would be very appealing, but we cannot readily rely on that property.
It is not unusual to see an expression of a method both take a symbol as an argument, as then later use it as a value, clearly expecting those to mean the same thing, such as in:
```julia
struct A{T}
x::T
A{T}(x) where {T} = new{T}(x)
end
A(x) = A{typeof(x)}(x)
```
Needless to say, it seems likely a bit disconcerting if the old constructor for `A(x)` only now returns objects of the new type (albeit with the same name).
### Fully static ages
An option on the opposite end of the spectrum would be to make lookup results basically constant as of the time the function was defined originally. This solves the confusing-constructor case. But this leads to another issue: how does `getproperty` (or equivalently `getglobal`) know what world to look in? For example, this is what the GNU compiler attempts to do for C++ with a custom extension to ELF, but the implementation of UNIQUE there is notoriously unreliable (clang refuses to clone this behavior) and is foundationally broken (as in undefined behavior) in the presence of multiple copies of a shared library existing, even with symbol versioning. We would probably do better to learn here from Windows PE/COFF, which does not suffer those flaws.
### Fully dynamic
Fully dynamic lookup is slow, and thus not being considered.
### Hybrid dynamic
A possible hybrid model would be to define the following rules:
- Literal global symbols resolve to their oldest available meaning after the function using them is defined. This permits use of forward declarations, without actually allowing any redefinitions.
- Any other expression (for example `A.b` or `getproperty(A, :b)` or `getglobal(A, :b)`) would resolve to the current TLS world age definition.
- The call `invoke_in_world(world, getglobal, A, :b)` or `invokelatest(getglobal, A, :b)` would get the value as of the given world.
The first rule is to maximize consistency, so that constants used in a function signature tend to have the same value as the same constant used in the body of the function.
The second rule is to maximize inferability, so that constants cannot change once the function world is defined.
The third rule is to provide an escape hatch, so that the user or inference can get any particular value (new or old) that is desired.
Some constants additionally would not be eligible for redefinition (in particular, the contents of the Core module, where the Builtins live).
Additionally, redefinition would be prohibited during precompilation (it can be defined inside an `__init__`, at the expensive of massive latency and re-compilation though). This is really only intended as an interactive feature, and will not serve particular well as a means of monkey-pirate-patching the system.
The second rule means that the system must have the ability to track constants that are used without bringing them into scope. As this is the minority of uses, this would likely be a fairly small list. However, since external tools (e.g. Revise.jl) may wish to re-evaluate many existing functions to provide duplicate definitions for them as well (for the old vs. new world of bindings managed by the dispatch on the new value of the type), we will likely want to provide interfaces that can track an enumerate all global constants used by a particular method, to aid in identifying which methods may be desirable to replace.
### Hybrid DWIM
One possible change to rule 1 above would be to categorize specifically whether a particular symbol appears as a global in both the function definition and in the body of the function. If so, we would apply rule 1 (make it use the same value of the constant as applied to the definition) and otherwise apply rule 2 (making it get the value at the TLS age.) Indeed, we could even take this a step further, and propose that any symbol (not expression) used to reference a global value in a method signature is immediately also captured and preserved (or spliced in) to maintain that same value in the function body. This might be considered a minor break.
### Other options?
What do you think it should do?