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

Invalidate methods when binding is typed/const-defined #54733

Closed

Conversation

topolarity
Copy link
Member

@topolarity topolarity commented Jun 7, 2024

This allows for patterns like:

julia> function foo(N)
    x = nothing
    for i = 1:N
        x = bar(i)
    end
    return x
end

julia> foo(1_000_000_000)
ERROR: UndefVarError: `bar` not defined

not to suffer a tremendous performance regression because of the fact that foo was inferred with bar still undefined.

This invalidation is not required for correctness, but for performance reasons once the global is defined we'd like to invalidate the code anyway to get an improved inference result:

julia> bar(x) = 3x
bar (generic function with 1 method)

julia> foo(1_000_000_000) # w/o PR: > 30 seconds, w/ PR: < 1μs

Note: This is essentially an optimized implementation of the non-semantic portion of #54654 for the special case of untyped/undefined globals, and this should mostly be compatible with that PR. Compared to that change, this adds explicit edges so that invalidation can be significantly faster and bit more fine-grained.

TODO:

  • Add lock to jl_binding_edges_t to prevent corruption from concurrent access
  • Add additional tests for global vs. const invalidations and x-module bindings
  • Fix up binding->ty not to be reset on serialization (use the closed/open rules on type set instead)

This allows for patterns like:
```
julia> function foo(N)
    for i = 1:N
        x = bar(i)
    end
end

julia> foo(1_000_000_000)
ERROR: UndefVarError: `bar` not defined
```

not to suffer a tremendous performance regression because of the fact
that `foo` was inferred with `bar` still undefined.

Strictly speaking the original code remains valid, but for performance
reasons once the global is defined we'd like to invalidate the code
anyway to get an improved inference result.

```
julia> bar(x) = 3x
bar (generic function with 1 method)

julia> foo(1_000_000_000) # w/o PR: takes > 30 seconds
```
@topolarity topolarity force-pushed the ct/invalidate-undef-bindings branch from 38d10b6 to 6e90c55 Compare June 7, 2024 20:11
@nsajko nsajko added performance Must go faster types and dispatch Types, subtyping and method dispatch labels Jun 10, 2024
@StefanKarpinski StefanKarpinski requested a review from Keno June 10, 2024 17:45
@Keno
Copy link
Member

Keno commented Jun 10, 2024

I would prefer to do this after #54654 which requires these semantics already and at which point it'll simply become a time/space tradeoff. Which direction to pick for the tradeoff is hard to know without benchmarking.

@vtjnash
Copy link
Member

vtjnash commented Nov 18, 2024

Subsumed by #54654 (and following)

@vtjnash vtjnash closed this Nov 18, 2024
@topolarity
Copy link
Member Author

In the post-#54654 world, this PR is implementing a specific type of backedge to make this invalidation efficient to apply, so it seems like an orthogonal change to me.

@topolarity topolarity reopened this Nov 18, 2024
Keno added a commit that referenced this pull request Feb 4, 2025
This is the final PR in the binding partitions series (modulo bugs and tweaks),
i.e. it closes #54654 and thus closes #40399, which was the original design
sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in particular
allows struct redefinitions. This thus closes timholy/Revise.jl#18 and
also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several related
issues in the bugtracker, so this closes #14055 #44604 #46354 #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus
closes #53958 #54733 - however, this is not activated yet for performance
reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
   slightly differently than the old semantics absent some other
   task going around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
   permissible under the old semantics if the bindings
   happened to be resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no binding",
   plus slightly more exotic corner cases around conflicts

2. Weakly declared bindings: Declared using `global sym` and nothing else

3. Strongly declared bindings: Declared using `global sym::T`, `const sym=val`,
   `import Mod: sym`, `using Mod: sym` or as an implicit strong global declaration
   in `sym=val`, where `sym` is known to be global (either by being at toplevle
   or as `global sym=val` inside a function).

In general, you always allowed to syntactically replace a weaker binding
by a stronger one (although the runtime permits arbitrary binding deletion
now, this is just a syntactic constraint to catch errors).
Second, any implicit binding can be replaced by other implicit
bindings as the result of changing the `using`'ed module.
And lastly, any constants may be replaced by any other constants
(irrespective of type).

We do not currently allow replacing globals, but may consider changing that
in 1.13.

This is mostly how things used to work, as well in the absence of any stray
external binding resolutions. The most prominent difference is probably this one:

```
set_foo!() = global foo = 1
```

In the above terminology, this now always declares a "strongly declared binding",
whereas before it declared a "weakly declared binding" that would become
strongly declared on first write to the global (unless of course somebody
had created a different strongly declared global in the meantime). To see
the difference, this is now disallowed:

```
julia> set_foo!() = global foo = 1
set_foo! (generic function with 1 method)

julia> const foo = 1
ERROR: cannot declare Main.foo constant; it was already declared global
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1
```

Before it would depend on the order of binding resolution (although it
just crashes on current master for some reason - whoops, probably my
fault).

Another major change is the ambiguousness of imports. In:
```
module M1; export x; x=1; end
module M2; export x; x=2; end
using .M1, .M2
```
the binding `Main.x` is now always ambiguous and will throw on access.
Before which binding you get, would depend on resolution order. To choose
one, use an explicit import (which was the behavior you would previously
get if neither binding was resolved before both imports).
Keno added a commit that referenced this pull request Feb 4, 2025
This is the final PR in the binding partitions series (modulo bugs and tweaks),
i.e. it closes #54654 and thus closes #40399, which was the original design
sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in particular
allows struct redefinitions. This thus closes timholy/Revise.jl#18 and
also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several related
issues in the bugtracker, so this closes #14055 #44604 #46354 #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus
closes #53958 #54733 - however, this is not activated yet for performance
reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
   slightly differently than the old semantics absent some other
   task going around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
   permissible under the old semantics if the bindings
   happened to be resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no binding",
   plus slightly more exotic corner cases around conflicts

2. Weakly declared bindings: Declared using `global sym` and nothing else

3. Strongly declared bindings: Declared using `global sym::T`, `const sym=val`,
   `import Mod: sym`, `using Mod: sym` or as an implicit strong global declaration
   in `sym=val`, where `sym` is known to be global (either by being at toplevle
   or as `global sym=val` inside a function).

In general, you always allowed to syntactically replace a weaker binding
by a stronger one (although the runtime permits arbitrary binding deletion
now, this is just a syntactic constraint to catch errors).
Second, any implicit binding can be replaced by other implicit
bindings as the result of changing the `using`'ed module.
And lastly, any constants may be replaced by any other constants
(irrespective of type).

We do not currently allow replacing globals, but may consider changing that
in 1.13.

This is mostly how things used to work, as well in the absence of any stray
external binding resolutions. The most prominent difference is probably this one:

```
set_foo!() = global foo = 1
```

In the above terminology, this now always declares a "strongly declared binding",
whereas before it declared a "weakly declared binding" that would become
strongly declared on first write to the global (unless of course somebody
had created a different strongly declared global in the meantime). To see
the difference, this is now disallowed:

```
julia> set_foo!() = global foo = 1
set_foo! (generic function with 1 method)

julia> const foo = 1
ERROR: cannot declare Main.foo constant; it was already declared global
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1
```

Before it would depend on the order of binding resolution (although it
just crashes on current master for some reason - whoops, probably my
fault).

Another major change is the ambiguousness of imports. In:
```
module M1; export x; x=1; end
module M2; export x; x=2; end
using .M1, .M2
```
the binding `Main.x` is now always ambiguous and will throw on access.
Before which binding you get, would depend on resolution order. To choose
one, use an explicit import (which was the behavior you would previously
get if neither binding was resolved before both imports).
Keno added a commit that referenced this pull request Feb 4, 2025
This is the final PR in the binding partitions series (modulo bugs and tweaks),
i.e. it closes #54654 and thus closes #40399, which was the original design
sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in particular
allows struct redefinitions. This thus closes timholy/Revise.jl#18 and
also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several related
issues in the bugtracker, so this closes #14055 #44604 #46354 #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus
closes #53958 #54733 - however, this is not activated yet for performance
reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
   slightly differently than the old semantics absent some other
   task going around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
   permissible under the old semantics if the bindings
   happened to be resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no binding",
   plus slightly more exotic corner cases around conflicts

2. Weakly declared bindings: Declared using `global sym` and nothing else

3. Strongly declared bindings: Declared using `global sym::T`, `const sym=val`,
   `import Mod: sym`, `using Mod: sym` or as an implicit strong global declaration
   in `sym=val`, where `sym` is known to be global (either by being at toplevle
   or as `global sym=val` inside a function).

In general, you always allowed to syntactically replace a weaker binding
by a stronger one (although the runtime permits arbitrary binding deletion
now, this is just a syntactic constraint to catch errors).
Second, any implicit binding can be replaced by other implicit
bindings as the result of changing the `using`'ed module.
And lastly, any constants may be replaced by any other constants
(irrespective of type).

We do not currently allow replacing globals, but may consider changing that
in 1.13.

This is mostly how things used to work, as well in the absence of any stray
external binding resolutions. The most prominent difference is probably this one:

```
set_foo!() = global foo = 1
```

In the above terminology, this now always declares a "strongly declared binding",
whereas before it declared a "weakly declared binding" that would become
strongly declared on first write to the global (unless of course somebody
had created a different strongly declared global in the meantime). To see
the difference, this is now disallowed:

```
julia> set_foo!() = global foo = 1
set_foo! (generic function with 1 method)

julia> const foo = 1
ERROR: cannot declare Main.foo constant; it was already declared global
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1
```

Before it would depend on the order of binding resolution (although it
just crashes on current master for some reason - whoops, probably my
fault).

Another major change is the ambiguousness of imports. In:
```
module M1; export x; x=1; end
module M2; export x; x=2; end
using .M1, .M2
```
the binding `Main.x` is now always ambiguous and will throw on access.
Before which binding you get, would depend on resolution order. To choose
one, use an explicit import (which was the behavior you would previously
get if neither binding was resolved before both imports).
Keno added a commit that referenced this pull request Feb 4, 2025
This is the final PR in the binding partitions series (modulo bugs and tweaks),
i.e. it closes #54654 and thus closes #40399, which was the original design
sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in particular
allows struct redefinitions. This thus closes timholy/Revise.jl#18 and
also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several related
issues in the bugtracker, so this closes #14055 #44604 #46354 #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus
closes #53958 #54733 - however, this is not activated yet for performance
reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
   slightly differently than the old semantics absent some other
   task going around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
   permissible under the old semantics if the bindings
   happened to be resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no binding",
   plus slightly more exotic corner cases around conflicts

2. Weakly declared bindings: Declared using `global sym` and nothing else

3. Strongly declared bindings: Declared using `global sym::T`, `const sym=val`,
   `import Mod: sym`, `using Mod: sym` or as an implicit strong global declaration
   in `sym=val`, where `sym` is known to be global (either by being at toplevle
   or as `global sym=val` inside a function).

In general, you always allowed to syntactically replace a weaker binding
by a stronger one (although the runtime permits arbitrary binding deletion
now, this is just a syntactic constraint to catch errors).
Second, any implicit binding can be replaced by other implicit
bindings as the result of changing the `using`'ed module.
And lastly, any constants may be replaced by any other constants
(irrespective of type).

We do not currently allow replacing globals, but may consider changing that
in 1.13.

This is mostly how things used to work, as well in the absence of any stray
external binding resolutions. The most prominent difference is probably this one:

```
set_foo!() = global foo = 1
```

In the above terminology, this now always declares a "strongly declared binding",
whereas before it declared a "weakly declared binding" that would become
strongly declared on first write to the global (unless of course somebody
had created a different strongly declared global in the meantime). To see
the difference, this is now disallowed:

```
julia> set_foo!() = global foo = 1
set_foo! (generic function with 1 method)

julia> const foo = 1
ERROR: cannot declare Main.foo constant; it was already declared global
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1
```

Before it would depend on the order of binding resolution (although it
just crashes on current master for some reason - whoops, probably my
fault).

Another major change is the ambiguousness of imports. In:
```
module M1; export x; x=1; end
module M2; export x; x=2; end
using .M1, .M2
```
the binding `Main.x` is now always ambiguous and will throw on access.
Before which binding you get, would depend on resolution order. To choose
one, use an explicit import (which was the behavior you would previously
get if neither binding was resolved before both imports).
Keno added a commit that referenced this pull request Feb 5, 2025
This is the final PR in the binding partitions series (modulo bugs and tweaks),
i.e. it closes #54654 and thus closes #40399, which was the original design
sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in particular
allows struct redefinitions. This thus closes timholy/Revise.jl#18 and
also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several related
issues in the bugtracker, so this closes #14055 #44604 #46354 #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus
closes #53958 #54733 - however, this is not activated yet for performance
reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
   slightly differently than the old semantics absent some other
   task going around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
   permissible under the old semantics if the bindings
   happened to be resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no binding",
   plus slightly more exotic corner cases around conflicts

2. Weakly declared bindings: Declared using `global sym` and nothing else

3. Strongly declared bindings: Declared using `global sym::T`, `const sym=val`,
   `import Mod: sym`, `using Mod: sym` or as an implicit strong global declaration
   in `sym=val`, where `sym` is known to be global (either by being at toplevle
   or as `global sym=val` inside a function).

In general, you always allowed to syntactically replace a weaker binding
by a stronger one (although the runtime permits arbitrary binding deletion
now, this is just a syntactic constraint to catch errors).
Second, any implicit binding can be replaced by other implicit
bindings as the result of changing the `using`'ed module.
And lastly, any constants may be replaced by any other constants
(irrespective of type).

We do not currently allow replacing globals, but may consider changing that
in 1.13.

This is mostly how things used to work, as well in the absence of any stray
external binding resolutions. The most prominent difference is probably this one:

```
set_foo!() = global foo = 1
```

In the above terminology, this now always declares a "strongly declared binding",
whereas before it declared a "weakly declared binding" that would become
strongly declared on first write to the global (unless of course somebody
had created a different strongly declared global in the meantime). To see
the difference, this is now disallowed:

```
julia> set_foo!() = global foo = 1
set_foo! (generic function with 1 method)

julia> const foo = 1
ERROR: cannot declare Main.foo constant; it was already declared global
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1
```

Before it would depend on the order of binding resolution (although it
just crashes on current master for some reason - whoops, probably my
fault).

Another major change is the ambiguousness of imports. In:
```
module M1; export x; x=1; end
module M2; export x; x=2; end
using .M1, .M2
```
the binding `Main.x` is now always ambiguous and will throw on access.
Before which binding you get, would depend on resolution order. To choose
one, use an explicit import (which was the behavior you would previously
get if neither binding was resolved before both imports).
Keno added a commit that referenced this pull request Feb 5, 2025
This is the final PR in the binding partitions series (modulo bugs and tweaks),
i.e. it closes #54654 and thus closes #40399, which was the original design
sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in particular
allows struct redefinitions. This thus closes timholy/Revise.jl#18 and
also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several related
issues in the bugtracker, so this closes #14055 #44604 #46354 #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus
closes #53958 #54733 - however, this is not activated yet for performance
reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
   slightly differently than the old semantics absent some other
   task going around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
   permissible under the old semantics if the bindings
   happened to be resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no binding",
   plus slightly more exotic corner cases around conflicts

2. Weakly declared bindings: Declared using `global sym` and nothing else

3. Strongly declared bindings: Declared using `global sym::T`, `const sym=val`,
   `import Mod: sym`, `using Mod: sym` or as an implicit strong global declaration
   in `sym=val`, where `sym` is known to be global (either by being at toplevle
   or as `global sym=val` inside a function).

In general, you always allowed to syntactically replace a weaker binding
by a stronger one (although the runtime permits arbitrary binding deletion
now, this is just a syntactic constraint to catch errors).
Second, any implicit binding can be replaced by other implicit
bindings as the result of changing the `using`'ed module.
And lastly, any constants may be replaced by any other constants
(irrespective of type).

We do not currently allow replacing globals, but may consider changing that
in 1.13.

This is mostly how things used to work, as well in the absence of any stray
external binding resolutions. The most prominent difference is probably this one:

```
set_foo!() = global foo = 1
```

In the above terminology, this now always declares a "strongly declared binding",
whereas before it declared a "weakly declared binding" that would become
strongly declared on first write to the global (unless of course somebody
had created a different strongly declared global in the meantime). To see
the difference, this is now disallowed:

```
julia> set_foo!() = global foo = 1
set_foo! (generic function with 1 method)

julia> const foo = 1
ERROR: cannot declare Main.foo constant; it was already declared global
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1
```

Before it would depend on the order of binding resolution (although it
just crashes on current master for some reason - whoops, probably my
fault).

Another major change is the ambiguousness of imports. In:
```
module M1; export x; x=1; end
module M2; export x; x=2; end
using .M1, .M2
```
the binding `Main.x` is now always ambiguous and will throw on access.
Before which binding you get, would depend on resolution order. To choose
one, use an explicit import (which was the behavior you would previously
get if neither binding was resolved before both imports).
Keno added a commit that referenced this pull request Feb 5, 2025
This is the final PR in the binding partitions series (modulo bugs and tweaks),
i.e. it closes #54654 and thus closes #40399, which was the original design
sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in particular
allows struct redefinitions. This thus closes timholy/Revise.jl#18 and
also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several related
issues in the bugtracker, so this closes #14055 #44604 #46354 #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus
closes #53958 #54733 - however, this is not activated yet for performance
reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
   slightly differently than the old semantics absent some other
   task going around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
   permissible under the old semantics if the bindings
   happened to be resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no binding",
   plus slightly more exotic corner cases around conflicts

2. Weakly declared bindings: Declared using `global sym` and nothing else

3. Strongly declared bindings: Declared using `global sym::T`, `const sym=val`,
   `import Mod: sym`, `using Mod: sym` or as an implicit strong global declaration
   in `sym=val`, where `sym` is known to be global (either by being at toplevle
   or as `global sym=val` inside a function).

In general, you always allowed to syntactically replace a weaker binding
by a stronger one (although the runtime permits arbitrary binding deletion
now, this is just a syntactic constraint to catch errors).
Second, any implicit binding can be replaced by other implicit
bindings as the result of changing the `using`'ed module.
And lastly, any constants may be replaced by any other constants
(irrespective of type).

We do not currently allow replacing globals, but may consider changing that
in 1.13.

This is mostly how things used to work, as well in the absence of any stray
external binding resolutions. The most prominent difference is probably this one:

```
set_foo!() = global foo = 1
```

In the above terminology, this now always declares a "strongly declared binding",
whereas before it declared a "weakly declared binding" that would become
strongly declared on first write to the global (unless of course somebody
had created a different strongly declared global in the meantime). To see
the difference, this is now disallowed:

```
julia> set_foo!() = global foo = 1
set_foo! (generic function with 1 method)

julia> const foo = 1
ERROR: cannot declare Main.foo constant; it was already declared global
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1
```

Before it would depend on the order of binding resolution (although it
just crashes on current master for some reason - whoops, probably my
fault).

Another major change is the ambiguousness of imports. In:
```
module M1; export x; x=1; end
module M2; export x; x=2; end
using .M1, .M2
```
the binding `Main.x` is now always ambiguous and will throw on access.
Before which binding you get, would depend on resolution order. To choose
one, use an explicit import (which was the behavior you would previously
get if neither binding was resolved before both imports).
Keno added a commit that referenced this pull request Feb 6, 2025
This is the final PR in the binding partitions series (modulo bugs and tweaks),
i.e. it closes #54654 and thus closes #40399, which was the original design
sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in particular
allows struct redefinitions. This thus closes timholy/Revise.jl#18 and
also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several related
issues in the bugtracker, so this closes #14055 #44604 #46354 #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus
closes #53958 #54733 - however, this is not activated yet for performance
reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
   slightly differently than the old semantics absent some other
   task going around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
   permissible under the old semantics if the bindings
   happened to be resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no binding",
   plus slightly more exotic corner cases around conflicts

2. Weakly declared bindings: Declared using `global sym` and nothing else

3. Strongly declared bindings: Declared using `global sym::T`, `const sym=val`,
   `import Mod: sym`, `using Mod: sym` or as an implicit strong global declaration
   in `sym=val`, where `sym` is known to be global (either by being at toplevle
   or as `global sym=val` inside a function).

In general, you always allowed to syntactically replace a weaker binding
by a stronger one (although the runtime permits arbitrary binding deletion
now, this is just a syntactic constraint to catch errors).
Second, any implicit binding can be replaced by other implicit
bindings as the result of changing the `using`'ed module.
And lastly, any constants may be replaced by any other constants
(irrespective of type).

We do not currently allow replacing globals, but may consider changing that
in 1.13.

This is mostly how things used to work, as well in the absence of any stray
external binding resolutions. The most prominent difference is probably this one:

```
set_foo!() = global foo = 1
```

In the above terminology, this now always declares a "strongly declared binding",
whereas before it declared a "weakly declared binding" that would become
strongly declared on first write to the global (unless of course somebody
had created a different strongly declared global in the meantime). To see
the difference, this is now disallowed:

```
julia> set_foo!() = global foo = 1
set_foo! (generic function with 1 method)

julia> const foo = 1
ERROR: cannot declare Main.foo constant; it was already declared global
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1
```

Before it would depend on the order of binding resolution (although it
just crashes on current master for some reason - whoops, probably my
fault).

Another major change is the ambiguousness of imports. In:
```
module M1; export x; x=1; end
module M2; export x; x=2; end
using .M1, .M2
```
the binding `Main.x` is now always ambiguous and will throw on access.
Before which binding you get, would depend on resolution order. To choose
one, use an explicit import (which was the behavior you would previously
get if neither binding was resolved before both imports).
@Keno Keno closed this in 888cf03 Feb 6, 2025
@Keno Keno closed this in #57253 Feb 6, 2025
@topolarity topolarity deleted the ct/invalidate-undef-bindings branch February 6, 2025 06:31
KristofferC pushed a commit that referenced this pull request Feb 6, 2025
This is the final PR in the binding partitions series (modulo bugs and
tweaks), i.e. it closes #54654 and thus closes #40399, which was the
original design sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in
particular allows struct redefinitions. This thus closes
timholy/Revise.jl#18 and also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several
related issues in the bugtracker, so this closes #14055 closes #44604
closes #46354 closes #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus closes
#53958 closes #54733 - however, this is not activated yet for
performance reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
slightly differently than the old semantics absent some other task going
around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
permissible under the old semantics if the bindings happened to be
resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no
binding", plus slightly more exotic corner cases around conflicts

2. Weakly declared bindings: Declared using `global sym` and nothing
else

3. Strongly declared bindings: Declared using `global sym::T`, `const
sym=val`, `import Mod: sym`, `using Mod: sym` or as an implicit strong
global declaration in `sym=val`, where `sym` is known to be global
(either by being at toplevle or as `global sym=val` inside a function).

In general, you always allowed to syntactically replace a weaker binding
by a stronger one (although the runtime permits arbitrary binding
deletion now, this is just a syntactic constraint to catch errors).
Second, any implicit binding can be replaced by other implicit bindings
as the result of changing the `using`'ed module. And lastly, any
constants may be replaced by any other constants (irrespective of type).

We do not currently allow replacing globals, but may consider changing
that in 1.13.

This is mostly how things used to work, as well in the absence of any
stray external binding resolutions. The most prominent difference is
probably this one:

```
set_foo!() = global foo = 1
```

In the above terminology, this now always declares a "strongly declared
binding", whereas before it declared a "weakly declared binding" that
would become strongly declared on first write to the global (unless of
course somebody had created a different strongly declared global in the
meantime). To see the difference, this is now disallowed:

```
julia> set_foo!() = global foo = 1
set_foo! (generic function with 1 method)

julia> const foo = 1
ERROR: cannot declare Main.foo constant; it was already declared global
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1
```

Before it would depend on the order of binding resolution (although it
just crashes on current master for some reason - whoops, probably my
fault).

Another major change is the ambiguousness of imports. In:
```
module M1; export x; x=1; end
module M2; export x; x=2; end
using .M1, .M2
```
the binding `Main.x` is now always ambiguous and will throw on access.
Before which binding you get, would depend on resolution order. To
choose one, use an explicit import (which was the behavior you would
previously get if neither binding was resolved before both imports).

(cherry picked from commit 888cf03)
KristofferC pushed a commit that referenced this pull request Feb 6, 2025
This is the final PR in the binding partitions series (modulo bugs and
tweaks), i.e. it closes #54654 and thus closes #40399, which was the
original design sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in
particular allows struct redefinitions. This thus closes
timholy/Revise.jl#18 and also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several
related issues in the bugtracker, so this closes #14055 closes #44604
closes #46354 closes #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus closes
#53958 closes #54733 - however, this is not activated yet for
performance reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
slightly differently than the old semantics absent some other task going
around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
permissible under the old semantics if the bindings happened to be
resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no
binding", plus slightly more exotic corner cases around conflicts

2. Weakly declared bindings: Declared using `global sym` and nothing
else

3. Strongly declared bindings: Declared using `global sym::T`, `const
sym=val`, `import Mod: sym`, `using Mod: sym` or as an implicit strong
global declaration in `sym=val`, where `sym` is known to be global
(either by being at toplevle or as `global sym=val` inside a function).

In general, you always allowed to syntactically replace a weaker binding
by a stronger one (although the runtime permits arbitrary binding
deletion now, this is just a syntactic constraint to catch errors).
Second, any implicit binding can be replaced by other implicit bindings
as the result of changing the `using`'ed module. And lastly, any
constants may be replaced by any other constants (irrespective of type).

We do not currently allow replacing globals, but may consider changing
that in 1.13.

This is mostly how things used to work, as well in the absence of any
stray external binding resolutions. The most prominent difference is
probably this one:

```
set_foo!() = global foo = 1
```

In the above terminology, this now always declares a "strongly declared
binding", whereas before it declared a "weakly declared binding" that
would become strongly declared on first write to the global (unless of
course somebody had created a different strongly declared global in the
meantime). To see the difference, this is now disallowed:

```
julia> set_foo!() = global foo = 1
set_foo! (generic function with 1 method)

julia> const foo = 1
ERROR: cannot declare Main.foo constant; it was already declared global
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1
```

Before it would depend on the order of binding resolution (although it
just crashes on current master for some reason - whoops, probably my
fault).

Another major change is the ambiguousness of imports. In:
```
module M1; export x; x=1; end
module M2; export x; x=2; end
using .M1, .M2
```
the binding `Main.x` is now always ambiguous and will throw on access.
Before which binding you get, would depend on resolution order. To
choose one, use an explicit import (which was the behavior you would
previously get if neither binding was resolved before both imports).

(cherry picked from commit 888cf03)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance Must go faster types and dispatch Types, subtyping and method dispatch
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants