Skip to content

Lower const x = ... to new builtin Core.setconst! #58187

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

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

xal-0
Copy link
Member

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

Adds a new builtin, Core.setconst!, with the following signature:

    setconst!(module::Module, name::Symbol, [x])

The lowered Expr(:const, :name, value) form has been removed, saving some
duplicated effort in the interpreter and code generation. Expr(:const, ...) is
now always lowered to the builtin.

julia> Meta.@lower const example = 123
:($(Expr(:thunk, CodeInfo(
1 ─ %1 = 123
│          builtin Core.setconst!(Main, :example, %1)
│        $(Expr(:latestworld))
└──      return %1
))))

The single-argument form of const, having no surface syntax, was previously only
available through eval(Expr(:const, :foo)):

julia> eval(Expr(:const, :example))

julia> GlobalRef(Main, :example).binding
Binding Main.example
   38582:∞ - undefined const binding
   38579:38581 - undefined binding - guard entry

It survived lowering by being special-cased in expand-toplevel-expr--,
resulting in some inconsistencies:

julia> eval(:(begin $(Expr(:const, :example)) end))
ERROR: syntax: malformed expression
Stacktrace:
 [1] top-level scope
   @ none:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
 [3] top-level scope
   @ REPL[1]:1

These are fixed now that const is always lowered.

@xal-0 xal-0 added the compiler:lowering Syntax lowering (compiler front end, 2nd stage) label Apr 21, 2025
@xal-0
Copy link
Member Author

xal-0 commented Apr 22, 2025

Some changes are needed to get Revise working again.

JuliaInterpreter (just running generate_builtins.jl and removing the unused case in step_expr!):

diff --git i/src/builtins.jl w/src/builtins.jl
index 36ae00a..2168e11 100644
--- i/src/builtins.jl
+++ w/src/builtins.jl
@@ -224,6 +224,8 @@ function maybe_evaluate_builtin(frame, call_expr, expand::Bool)
         else
             return Some{Any}(Core.memoryrefswap!(getargs(args, frame)...))
         end
+    elseif f === Core.setconst!
+        return Some{Any}(Core.setconst!(getargs(args, frame)...))
     elseif f === Core.sizeof
         if nargs == 1
             return Some{Any}(Core.sizeof(lookup(frame, args[2])))
diff --git i/src/interpret.jl w/src/interpret.jl
index c552bd4..4744554 100644
--- i/src/interpret.jl
+++ w/src/interpret.jl
@@ -528,7 +528,7 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
                     error("this should have been handled by split_expressions")
                 elseif node.head === :using || node.head === :import || node.head === :export
                     Core.eval(moduleof(frame), node)
-                elseif node.head === :const || node.head === :globaldecl
+                elseif node.head === :globaldecl
                     g = node.args[1]
                     if length(node.args) == 2
                         Core.eval(moduleof(frame), Expr(:block, Expr(node.head, g, lookup(frame, node.args[2])), nothing))

LoweredCodeUtils:

diff --git i/src/codeedges.jl w/src/codeedges.jl
index 647eae8..246e83a 100644
--- i/src/codeedges.jl
+++ w/src/codeedges.jl
@@ -196,7 +196,7 @@ end
 # The depwarn is currently disabled because Revise's tests check for empty warning logs
 function is_assignment_like(@nospecialize stmt)
     # Base.depwarn("is_assignment_like is deprecated, switch to `LoweredCodeUtils.get_lhs_rhs`", :is_assignment_like)
-    return isexpr(stmt, :(=)) || isexpr(stmt, :const, 2)
+    return isexpr(stmt, :(=))
 end
 
 function get_lhs_rhs(@nospecialize stmt)
@@ -206,7 +206,7 @@ function get_lhs_rhs(@nospecialize stmt)
         return Pair{Any,Any}(stmt.args[1], stmt.args[2])
     elseif isexpr(stmt, :call) && length(stmt.args) == 4
         f = stmt.args[1]
-        if is_global_ref_egal(f, :setglobal!, Core.setglobal!)
+        if is_global_ref_egal(f, :setglobal!, Core.setglobal!) || is_global_ref_egal(f, :setconst!, Core.setconst!)
             mod = stmt.args[2]
             mod isa Module || return nothing
             name = stmt.args[3]

;; (= (globalref _ _) _) => setglobal!
;; (const (globalref _ _) _) => setconst!
(cond ((and (globalref? lhs) (eq? op '=))
(emit `(call (top setglobal!) ,(cadr lhs) (inert ,(caddr lhs)) ,rhs)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect this should also be (core setglobal!)

@@ -2753,6 +2753,38 @@ See also [`setpropertyonce!`](@ref Base.setpropertyonce!) and [`setglobal!`](@re
"""
setglobalonce!

"""
setconst!(module::Module, name::Symbol, [x])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bikeshed: I think declare_const might make more sense than calling it "set!"? Later we can add declare_method similarly. They also do not modify anything (they create a new world for possible execution, but do not change anything), so they likely should not have a ! (similar to Base.delete_method)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, and I hadn't really considered how this would fit with possible builtins for the :global/:globaldecl and :method Exprs.

declare_global matches jl_declare_global, but we use noun-verb for jl_method_def.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, in other languages (llvm, C particularly) this would be define, not declare

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also might want to choose the name to be consistent with _eval_import, whether we rename that to declare_import or rename this to eval_global

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to go with declare_const and declare_global for the time being. Ending in ! is just wrong, and we've got jl_declare_* for internal stuff that establishes new bindings. I will defer the naming of functions that manipulate method tables to future bikeshedding.

Adds a new builtin, Core.setconst!, with the following signature:

    setconst!(module::Module, name::Symbol, [x])

The lowered Expr(:const, :name, value) form has been removed, saving some
duplicated effort in the interpreter and code generation.  Expr(:const, ...) is
now always lowered to the builtin.

The single-argument form of const, having no surface syntax, was previously only
available through eval(Expr(:const, :foo)).  It can now also be used by omitting
the value argument to setconst!.
@xal-0 xal-0 force-pushed the setconst-builtin branch from 804c89f to 298a7da Compare April 25, 2025 18:39
@vtjnash
Copy link
Member

vtjnash commented Apr 28, 2025

Could you update Revise so we can get this over the finish line? For a similar example, take a look at timholy/Revise.jl#890

xal-0 and others added 3 commits April 29, 2025 10:21
Co-authored-by: Jameson Nash <vtjnash@gmail.com>
Core.declare_const doesn't mutate state and so shouldn't end with !.  This also
brings it in line with potential future builtins like Core.declare_global.
@c42f
Copy link
Member

c42f commented Aug 1, 2025

For what it's worth, I'm very much in favor of removing special Expr forms from the IR where possible and replacing them with calls to the Julia runtime. A lot of special forms have implicit semantics you can only figure out by reading the code, and I want to remove them where possible :)

What's left to get this over the line?

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.

3 participants