Skip to content

Commit

Permalink
remove implicitly inherited globals (#19324)
Browse files Browse the repository at this point in the history
fix #19324
  • Loading branch information
vtjnash authored and JeffBezanson committed Oct 26, 2017
1 parent a5949a3 commit cc87d82
Show file tree
Hide file tree
Showing 12 changed files with 212 additions and 188 deletions.
179 changes: 89 additions & 90 deletions doc/src/manual/variables-and-scoping.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,34 @@ set of source lines; instead, it will always line up with one of these blocks. T
main types of scopes in Julia, *global scope* and *local scope*, the latter can be nested. The
constructs introducing scope blocks are:

| Scope name | block/construct introducing this kind of scope |
|:-------------------- |:-------------------------------------------------------------------------------------------------------- |
| [Global Scope](@ref) | `module`, `baremodule`, at interactive prompt (REPL) |
| [Local Scope](@ref) | [Soft Local Scope](@ref): `for`, `while`, comprehensions, try-catch-finally, `let` |
| [Local Scope](@ref) | [Hard Local Scope](@ref): functions (either syntax, anonymous & do-blocks), `struct`, `macro` |
# [](@id man-scope-table)

Notably missing from this table are [begin blocks](@ref man-compound-expressions) and [if blocks](@ref man-conditional-evaluation), which do *not*
introduce new scope blocks. All three types of scopes follow somewhat different rules which will
be explained below as well as some extra rules for certain blocks.
* Scope blocks that may nest only in other global scope blocks:

- global scope

+ module, baremodule

+ at interactive prompt (REPL)

- local scope (don't allow nesting)

+ type, immutable, macro

* Scope blocks which may nest anywhere (in global or local scope):

- local scope

+ for, while, try-catch-finally, let

+ functions (either syntax, anonymous & do-blocks)

+ comprehensions, broadcast-fusing

Notably missing from this table are
[begin blocks](@ref man-compound-expressions) and [if blocks](@ref man-conditional-evaluation)
which do *not* introduce new scope blocks.
Both types of scopes follow somewhat different rules which will be explained below.

Julia uses [lexical scoping](https://en.wikipedia.org/wiki/Scope_%28computer_science%29#Lexical_scoping_vs._dynamic_scoping),
meaning that a function's scope does not inherit from its caller's scope, but from the scope in
Expand Down Expand Up @@ -50,7 +69,7 @@ Thus *lexical scope* means that the scope of variables can be inferred from the

## Global Scope

*Each module introduces a new global scope*, separate from the global scope of all other modules;
Each module introduces a new global scope, separate from the global scope of all other modules;
there is no all-encompassing global scope. Modules can introduce variables of other modules into
their scope through the [using or import](@ref modules) statements or through qualified access using the
dot-notation, i.e. each module is a so-called *namespace*. Note that variable bindings can only
Expand Down Expand Up @@ -87,16 +106,20 @@ Note that the interactive prompt (aka REPL) is in the global scope of the module

## Local Scope

A new local scope is introduced by most code-blocks, see above table for a complete list.
A local scope *usually* inherits all the variables from its parent scope, both for reading and
writing. There are two subtypes of local scopes, hard and soft, with slightly different rules
concerning what variables are inherited. Unlike global scopes, local scopes are not namespaces,
A new local scope is introduced by most code blocks (see above
[table](@ref man-scope-table) for a complete list).
A local scope inherits all the variables from a parent local scope,
both for reading and writing.
Additionally, the local scope inherits all globals that are assigned
to in its parent global scope block (if it is surrounded by a global `if` or `begin` scope).
Unlike global scopes, local scopes are not namespaces,
thus variables in an inner scope cannot be retrieved from the parent scope through some sort of
qualified access.

The following rules and examples pertain to both hard and soft local scopes. A newly introduced
variable in a local scope does not back-propagate to its parent scope. For example, here the
`z` is not introduced into the top-level scope:
The following rules and examples pertain to local scopes.
A newly introduced variable in a local scope does not
back-propagate to its parent scope.
For example, here the ``z`` is not introduced into the top-level scope:

```jldoctest
julia> for i = 1:10
Expand All @@ -110,21 +133,21 @@ ERROR: UndefVarError: z not defined
(Note, in this and all following examples it is assumed that their top-level is a global scope
with a clean workspace, for instance a newly started REPL.)

Inside a local scope a variable can be forced to be a local variable using the `local` keyword:
Inside a local scope a variable can be forced to be a new local variable using the `local` keyword:

```jldoctest
julia> x = 0;
julia> for i = 1:10
local x
local x # this is also the default
x = i + 1
end
julia> x
0
```

Inside a local scope a new global variable can be defined using the keyword `global`:
Inside a local scope a global variable can be assigned to by using the keyword `global`:

```jldoctest
julia> for i = 1:10
Expand Down Expand Up @@ -152,37 +175,14 @@ julia> z
The `local` and `global` keywords can also be applied to destructuring assignments, e.g.
`local x, y = 1, 2`. In this case the keyword affects all listed variables.

### Soft Local Scope

> In a soft local scope, all variables are inherited from its parent scope unless a variable is
> specifically marked with the keyword `local`.
Soft local scopes are introduced by for-loops, while-loops, comprehensions, try-catch-finally-blocks,
and let-blocks. There are some extra rules for [Let Blocks](@ref) and for [For Loops and Comprehensions](@ref).

In the following example the `x` and `y` refer always to the same variables as the soft local
scope inherits both read and write variables:

```jldoctest
julia> x, y = 0, 1;
julia> for i = 1:10
x = i + y + 1
end
julia> x
12
```

### Hard Local Scope
Local scopes are introduced by most block keywords,
with notable exceptions of `begin` and `if`.

Hard local scopes are introduced by function definitions (in all their forms), struct type definition blocks,
and macro-definitions.
In a local scope, all variables are inherited from its parent
global scope block unless:

> In a hard local scope, all variables are inherited from its parent scope unless:
>
> * an assignment would result in a modified *global* variable, or
> * a variable is specifically marked with the keyword `local`.
* an assignment would result in a modified *global* variable, or
* a variable is specifically marked with the keyword `local`.

Thus global variables are only inherited for reading but not for writing:

Expand All @@ -203,6 +203,14 @@ julia> x

An explicit `global` is needed to assign to a global variable:

!!! sidebar "Avoiding globals"
Avoiding changing the value of global variables is considered by many
to be a programming best-practice.
One reason for this is that remotely changing the state of global variables in other
modules should be done with care as it makes the local behavior of the program hard to reason about.
This is why the scope blocks that introduce local scope require the ``global``
keyword to declare the intent to modify a global variable.

```jldoctest
julia> x = 1;
Expand All @@ -216,8 +224,7 @@ julia> x
2
```

Note that *nested functions* can behave differently to functions defined in the global scope as
they can modify their parent scope's *local* variables:
Note that *nested functions* can modify their parent scope's *local* variables:

```jldoctest
julia> x, y = 1, 2;
Expand All @@ -234,19 +241,40 @@ julia> function baz()
julia> baz()
22
julia> x, y
julia> x, y # verify that global x and y are unchanged
(1, 2)
```

The distinction between inheriting global and local variables for assignment can lead to some
slight differences between functions defined in local vs. global scopes. Consider the modification
of the last example by moving `bar` to the global scope:
The reason to allow *modifying local* variables of parent scopes in
nested functions is to allow constructing `closures
<https://en.wikipedia.org/wiki/Closure_%28computer_programming%29>`_
which have a private state, for instance the ``state`` variable in the
following example:

```jldoctest
julia> let state = 0
global counter() = (state += 1)
end;
julia> counter()
1
julia> counter()
2
```

See also the closures in the examples in the next two sections.

The distinction between inheriting global scope and nesting local scope
can lead to some slight differences between functions
defined in local vs. global scopes for variable assignments.
Consider the modification of the last example by moving `bar` to the global scope:

```jldoctest
julia> x, y = 1, 2;
julia> function bar()
x = 10 # local
x = 10 # local, no longer a closure variable
return x + y
end;
Expand All @@ -258,11 +286,11 @@ julia> function quz()
julia> quz()
14
julia> x, y
julia> x, y # verify that global x and y are unchanged
(1, 2)
```

Note that above subtlety does not pertain to type and macro definitions as they can only appear
Note that the above nesting rules do not pertain to type and macro definitions as they can only appear
at the global scope. There are special scoping rules concerning the evaluation of default and
keyword function arguments which are described in the [Function section](@ref man-functions).

Expand Down Expand Up @@ -292,9 +320,9 @@ they are actually called. As an example, here is an inefficient, mutually recurs
if positive integers are even or odd:

```jldoctest
julia> even(n) = n == 0 ? true : odd(n-1);
julia> even(n) = (n == 0) ? true : odd(n - 1);
julia> odd(n) = n == 0 ? false : even(n-1);
julia> odd(n) = (n == 0) ? false : even(n - 1);
julia> even(3)
false
Expand All @@ -304,37 +332,8 @@ true
```

Julia provides built-in, efficient functions to test for oddness and evenness called [`iseven`](@ref)
and [`isodd`](@ref) so the above definitions should only be taken as examples.

### Hard vs. Soft Local Scope

Blocks which introduce a soft local scope, such as loops, are generally used to manipulate the
variables in their parent scope. Thus their default is to fully access all variables in their
parent scope.

Conversely, the code inside blocks which introduce a hard local scope (function, type, and macro
definitions) can be executed at any place in a program. Remotely changing the state of global
variables in other modules should be done with care and thus this is an opt-in feature requiring
the `global` keyword.

The reason to allow *modifying local* variables of parent scopes in nested functions is to allow
constructing [closures](https://en.wikipedia.org/wiki/Closure_%28computer_programming%29) which
have a private state, for instance the `state` variable in the following example:

```jldoctest
julia> let state = 0
global counter
counter() = state += 1
end;
julia> counter()
1
julia> counter()
2
```

See also the closures in the examples in the next two sections.
and [`isodd`](@ref) so the above definitions should only be considered to be examples of scope,
not efficient design.

### Let Blocks

Expand Down
32 changes: 19 additions & 13 deletions src/julia-syntax.scm
Original file line number Diff line number Diff line change
Expand Up @@ -1710,9 +1710,9 @@
(define (expand-for while lhs X body)
;; (for (= lhs X) body)
(let* ((coll (make-ssavalue))
(state (gensy))
(outer? (and (pair? lhs) (eq? (car lhs) 'outer)))
(lhs (if outer? (cadr lhs) lhs)))
(state (gensy))
(outer? (and (pair? lhs) (eq? (car lhs) 'outer)))
(lhs (if outer? (cadr lhs) lhs)))
`(block (= ,coll ,(expand-forms X))
(= ,state (call (top start) ,coll))
;; TODO avoid `local declared twice` error from this
Expand Down Expand Up @@ -2275,15 +2275,15 @@
'while
(lambda (e)
`(break-block loop-exit
(_while ,(expand-forms (cadr e))
(break-block loop-cont
(scope-block ,(blockify (expand-forms (caddr e))))))))
(_while ,(expand-forms (cadr e))
(break-block loop-cont
(scope-block ,(blockify (expand-forms (caddr e))))))))

'inner-while
(lambda (e)
`(_while ,(expand-forms (cadr e))
(break-block loop-cont
(scope-block ,(blockify (expand-forms (caddr e)))))))
(break-block loop-cont
(scope-block ,(blockify (expand-forms (caddr e)))))))

'break
(lambda (e)
Expand Down Expand Up @@ -2560,10 +2560,15 @@
(define (find-local-def-decls e) (find-decls 'local-def e))
(define (find-global-decls e) (find-decls 'global e))

(define (implicit-locals e env glob)
(define (implicit-locals e env deprecated-env glob)
;; const decls on non-globals introduce locals
(append! (diff (find-decls 'const e) glob)
(find-assigned-vars e env)))
(filter
(lambda (v)
(if (memq v deprecated-env)
(begin (syntax-deprecation #f (string "implicit assignment to global variable `" v "`") (string "global " v)) #f)
#t))
(find-assigned-vars e env))))

(define (unbound-vars e bound tab)
(cond ((or (eq? e 'true) (eq? e 'false) (eq? e UNUSED)) tab)
Expand Down Expand Up @@ -2613,15 +2618,16 @@
((eq? (car e) 'scope-block)
(let* ((blok (cadr e)) ;; body of scope-block expression
(other-locals (if lam (caddr lam) '())) ;; locals that are explicitly part of containing lambda expression
(iglo (find-decls 'implicit-global blok)) ;; globals defined implicitly outside blok
(iglo (find-decls 'implicit-global blok)) ;; globals possibly defined implicitly outside blok
(glob (diff (find-global-decls blok) iglo)) ;; all globals declared in blok
(vars-def (check-dups (find-local-def-decls blok) '()))
(locals-declared (check-dups (find-local-decls blok) vars-def))
(locals-implicit (implicit-locals
blok
;; being declared global prevents a variable
;; assignment from introducing a local
(append env glob iglo outerglobals locals-declared vars-def)
(append env glob iglo locals-declared vars-def)
outerglobals
(append glob iglo)))
(vars (delete-duplicates (append! locals-declared locals-implicit)))
(all-vars (append vars vars-def))
Expand Down Expand Up @@ -2658,7 +2664,7 @@
(memq var implicitglobals) ;; remove anything only added implicitly in the last scope block
(memq var glob))))) ;; remove anything that's now global
renames)))
(new-oglo (append iglo outerglobals)) ;; list of all outer-globals from outside blok
(new-oglo (append iglo outerglobals)) ;; list of all implicit-globals from outside blok
(body (resolve-scopes- blok new-env new-oglo new-iglo lam new-renames #f))
(real-new-vars (append (diff vars need-rename) renamed))
(real-new-vars-def (append (diff vars-def need-rename-def) renamed-def)))
Expand Down
Loading

0 comments on commit cc87d82

Please sign in to comment.