Skip to content

Conversation

@xal-0
Copy link
Member

@xal-0 xal-0 commented Apr 29, 2025

Overview

In the spirit of #58187 and #57965, this PR lowers more surface syntax to calls,
eliminating the lowered :global and :globaldecl operations in favour of a
single Core.declare_global builtin.

Core.declare_global has the signature:

declare_global(module::Module, name::Symbol, strong::Bool=false, [ty::Type])
  • When strong = false, it has the effect of global name at the top level
    (see the description for PARTITION_KIND_DECLARED).
  • With strong = true:
    • No ty provided: if no global exists, creates a strong global with type
      Any. Has no effect if one already exists. This form is generated by
      global assignments with no type declaration.
    • ty provided: always creates a new global with the given type, failing if
      one already exists with a different declared type.

Definition effects

One of the purposes of this change is to remove the definitions effects for
:global and :globaldecl:

julia/src/method.c

Lines 95 to 105 in d46b665

if (e->head == jl_global_sym && binding_effects) {
// execute the side-effects of "global x" decl immediately:
// creates uninitialized mutable binding in module for each global
jl_eval_global_expr(module, e, 1);
return jl_nothing;
}
if (e->head == jl_globaldecl_sym && binding_effects) {
assert(jl_expr_nargs(e) == 1);
jl_declare_global(module, jl_exprarg(e, 0), NULL, 1);
return jl_nothing;
}

The eventual goal is to make all the definition effects for a method explicit
after lowering, simplifying interpreters for lowered IR.

Minor lowering changes

global permitted in more places

Adds a new ephemeral syntax head, unused-only, to wrap expressions whose
result should not be used. It generates the misplaced "global" declaration
error, and is slightly more forgiving than the old restriction. This was
necessary to permit global to be lowered in all contexts. Old:

julia> global example

julia> begin
           global example
       end
ERROR: syntax: misplaced "global" declaration
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1

New:

julia> global example

julia> begin
           global example
       end

global always lowered

This change maintains support for some expressions that cannot be produced by
the parser (similar to Expr(:const, :foo)):

eval(Expr(:global, GlobalRef(Base, :newglobal)))

This used to work by bypassing lowering but is now lowered to the appropriate
declare_global call.

Generated functions

After lowering the body AST returned by a @generated function, the definition
effects are still performed. Instead of relying on a check in
jl_declare_global to fail during this process, GeneratedFunctionStub now
wraps the AST in a new Expr head, Expr(:toplevel_pure, ...), indicating
lowering should not produce toplevel side effects.

Currently, this is used only to omit calls to declare_global for generated
functions, but it could also be used to improve the catch-all error message when
lowering returns a thunk (telling the user if it failed because of a closure,
generator, etc), or even to support some closures by making them opaque.

The error message for declaring a global as a side effect of a @generated
function AST has changed, because it now fails when the assignment to an
undeclared global is performed. Old:

julia> @generated function foo(x)
           :(global bar = x)
       end
foo (generic function with 1 method)

julia> foo(1)
ERROR: new strong globals cannot be created in a generated function. Declare them outside using `global x::Any`.
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1

New:

julia> @generated function foo(x)
           :(global bar = x)
       end
foo (generic function with 1 method)

julia> foo(1)
ERROR: Global Main.bar does not exist and cannot be assigned.
Note: Julia 1.9 and 1.10 inadvertently omitted this error check (#56933).
Hint: Declare it using `global bar` inside `Main` before attempting assignment.
Stacktrace:
 [1] macro expansion
   @ ./REPL[1]:1 [inlined]
 [2] foo(x::Int64)
   @ Main ./REPL[1]:1
 [3] top-level scope
   @ REPL[2]:1

Examples of the new lowering

Toplevel weak global:

julia> Meta.@lower global example
:($(Expr(:thunk, CodeInfo(
1 ─       builtin Core.declare_global(Main, :example, false)
│       $(Expr(:latestworld))
└──     return nothing
))))

Toplevel strong global declaration with type:

julia> Meta.@lower example::Int
:($(Expr(:thunk, CodeInfo(
1 ─ %1 = Main.example
│   %2 = Main.Int
│   %3 =   builtin Core.typeassert(%1, %2)
└──      return %3
))))

Toplevel strong global assignment:

julia> Meta.@lower example = 1
:($(Expr(:thunk, CodeInfo(
1 ─         builtin Core.declare_global(Main, :example, true)
│         $(Expr(:latestworld))
│   %3  =   builtin Core.get_binding_type(Main, :example)
│         #s1 = 1
│   %5  = #s1
│   %6  =   builtin %5 isa %3
└──       goto #3 if not %6
2 ─       goto #4
3 ─ %9  = #s1
└──       #s1 = Base.convert(%3, %9)
4 ┄ %11 = #s1
│           dynamic Base.setglobal!(Main, :example, %11)
└──       return 1
))))

Toplevel strong global assignment with type:

julia> Meta.@lower example::Int = 1
:($(Expr(:thunk, CodeInfo(
1 ─ %1  = Main.Int
│           builtin Core.declare_global(Main, :example, true, %1)
│         $(Expr(:latestworld))
│   %4  =   builtin Core.get_binding_type(Main, :example)
│         #s1 = 1
│   %6  = #s1
│   %7  =   builtin %6 isa %4
└──       goto #3 if not %7
2 ─       goto #4
3 ─ %10 = #s1
└──       #s1 = Base.convert(%4, %10)
4 ┄ %12 = #s1
│           dynamic Base.setglobal!(Main, :example, %12)
└──       return 1
))))

Global assignment inside function (call to declare_global hoisted to top
level):

julia> Meta.@lower function f1(x)
           global example = x
       end
:($(Expr(:thunk, CodeInfo(
1 ─       $(Expr(:method, :(Main.f1)))
│         $(Expr(:latestworld))
│         $(Expr(:latestworld))
│           builtin Core.declare_global(Main, :example, false)
│         $(Expr(:latestworld))
│           builtin Core.declare_global(Main, :example, true)
│         $(Expr(:latestworld))
│   %8  = Main.f1
│   %9  =   dynamic Core.Typeof(%8)
│   %10 =   builtin Core.svec(%9, Core.Any)
│   %11 =   builtin Core.svec()
│   %12 =   builtin Core.svec(%10, %11, $(QuoteNode(:(#= REPL[7]:1 =#))))
│         $(Expr(:method, :(Main.f1), :(%12), CodeInfo(
    @ REPL[7]:2 within `unknown scope`
1 ─ %1  = x
│   %2  =   builtin Core.get_binding_type(Main, :example)
│         @_3 = %1
│   %4  = @_3
│   %5  =   builtin %4 isa %2
└──       goto #3 if not %5
2 ─       goto #4
3 ─ %8  = @_3
└──       @_3 = Base.convert(%2, %8)
4 ┄ %10 = @_3
│           dynamic Base.setglobal!(Main, :example, %10)
└──       return %1
)))
│         $(Expr(:latestworld))
│   %15 = Main.f1
└──       return %15
))))

xal-0 added 7 commits April 28, 2025 10:22
We accept `global` in a few more cases where we previously would reject it,
possibly complicating some macros:

julia> global v1

julia> begin
           global v2
           global v3
       end
ERROR: syntax: misplaced "global" declaration
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1

A new ephemeral form in lowering, `unused-only` (analogous to `toplevel-only`),
can be used to mark expressions that should not be assigned to variables, used
as call arguments, etc.  It is more permissive than global's prior behaviour,
failing only when the value is used in non-tail position.
While converting assignments to globals, do not generate calls to declare_global
if we are in a context where definition effects are not allowed (@generated
function bodies).

Previously, jl_declare_global would fail when performing the definition effects
for a @generated function body:

  julia> @generated function foo(x)
             :(global bar = x)
         end
  foo (generic function with 1 method)

  julia> foo(1)
  ERROR: new strong globals cannot be created in a generated function. Declare
  them outside using `global x::Any`.
  Stacktrace:
   [1] top-level scope
     @ REPL[2]:1

Now, they fail when the assignment is performed and no global was previously
declared:

    julia> @generated function foo(x)
               :(global bar = x)
           end
    foo (generic function with 1 method)

    julia> foo(1)
    ERROR: Global Main.bar does not exist and cannot be assigned.
    Note: Julia 1.9 and 1.10 inadvertently omitted this error check (JuliaLang#56933).
    Hint: Declare it using `global bar` inside `Main` before attempting
    assignment.
    Stacktrace:
     [1] macro expansion
       @ ./REPL[1]:1 [inlined]
     [2] foo(x::Int64)
       @ Main ./REPL[1]:1
     [3] top-level scope
       @ REPL[2]:1
This succeeded by bypassing lowering previously.
@xal-0 xal-0 added the compiler:lowering Syntax lowering (compiler front end, 2nd stage) label Apr 29, 2025
@xal-0
Copy link
Member Author

xal-0 commented Apr 29, 2025

TODOs:

  • Package support
    • Revise
    • JuliaInterpreter
    • LoweredCodeUtils
  • Generate fewer redundant declare_global calls?
    • 1.12 generates a global and two globaldecls for global foo:Int = 1, we generate two declare_global calls.

@c42f
Copy link
Member

c42f commented Sep 5, 2025

I implemented something similar to the unused-local rule over at JuliaLang/JuliaLowering.jl#67.

Specifically, "global decls are not allowed in value position, except in tail position in a top level thunk". Which is a fairly minor but IMO safe relaxation to the current rules. If another decision gets made here I'm happy to adjust things, though.

vtjnash added a commit to vtjnash/LoweredCodeUtils.jl that referenced this pull request Sep 16, 2025
vtjnash added a commit to vtjnash/LoweredCodeUtils.jl that referenced this pull request Sep 17, 2025
Ensures all tests pass before and after
JuliaLang/julia#58279
based on change from `Expr(:globaldecl, GlobalRef(M, S))`
to `Core.declare_global(M, S, true)`, while still ignoring
`Expr(:global, S)` from `struct` expanding to `strong=false`.
timholy pushed a commit to JuliaDebug/LoweredCodeUtils.jl that referenced this pull request Sep 17, 2025
@xal-0 xal-0 marked this pull request as ready for review September 29, 2025 18:24
aviatesk added a commit to JuliaDebug/LoweredCodeUtils.jl that referenced this pull request Sep 29, 2025
* handle new declare_global intrinsic

Ensures all tests pass before and after
JuliaLang/julia#58279
based on change from `Expr(:globaldecl, GlobalRef(M, S))`
to `Core.declare_global(M, S, true)`, while still ignoring
`Expr(:global, S)` from `struct` expanding to `strong=false`.

* Update src/codeedges.jl

---------

Co-authored-by: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com>
@vtjnash vtjnash added the merge me PR is reviewed. Merge when all tests are passing label Sep 29, 2025
@xal-0 xal-0 force-pushed the globaldecl-builtin branch from e05165a to 45729e7 Compare September 30, 2025 15:41
@xal-0 xal-0 removed the request for review from a team September 30, 2025 15:41
@xal-0 xal-0 merged commit 7a8cd6e into JuliaLang:master Oct 1, 2025
7 checks passed
@mlechu mlechu removed the merge me PR is reviewed. Merge when all tests are passing label Oct 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler:lowering Syntax lowering (compiler front end, 2nd stage)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants