Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Compiler/src/ssair/ir.jl
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ function is_relevant_expr(e::Expr)
:gc_preserve_begin, :gc_preserve_end,
:foreigncall, :isdefined, :copyast,
:throw_undef_if_not,
:cfunction, :method, :pop_exception,
:cfunction, :pop_exception,
:leave,
:new_opaque_closure)
end
Expand Down
2 changes: 1 addition & 1 deletion Compiler/src/ssair/verify.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ if !isdefined(@__MODULE__, Symbol("@verify_error"))
end
end

is_toplevel_expr_head(head::Symbol) = head === :method || head === :thunk
is_toplevel_expr_head(head::Symbol) = head === :thunk
is_value_pos_expr_head(head::Symbol) = head === :static_parameter
function check_op(ir::IRCode, domtree::DomTree, @nospecialize(op), use_bb::Int, use_idx::Int, printed_use_idx::Int, print::Bool, isforeigncall::Bool, arg_idx::Int,
allow_frontend_forms::Bool, @nospecialize(raise_error))
Expand Down
7 changes: 1 addition & 6 deletions Compiler/src/validation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const VALID_EXPR_HEADS = IdDict{Symbol,UnitRange{Int}}(
:static_parameter => 1:1,
:(&) => 1:1,
:(=) => 2:2,
:method => 1:4,
:new => 1:typemax(Int),
:splatnew => 2:2,
:the_exception => 0:0,
Expand Down Expand Up @@ -49,7 +48,6 @@ const SLOTFLAGS_MISMATCH = "length(slotnames) < length(slotflags)"
const SSAVALUETYPES_MISMATCH = "not all SSAValues in AST have a type in ssavaluetypes"
const SSAVALUETYPES_MISMATCH_UNINFERRED = "uninferred CodeInfo ssavaluetypes field does not equal the number of present SSAValues"
const SSAFLAGS_MISMATCH = "not all SSAValues have a corresponding `ssaflags`"
const NON_TOP_LEVEL_METHOD = "encountered `Expr` head `:method` in non-top-level code (i.e. `nargs` > 0)"
const SIGNATURE_NARGS_MISMATCH = "method signature does not match number of method arguments"
const SLOTNAMES_NARGS_MISMATCH = "CodeInfo for method contains fewer slotnames than the number of method arguments"
const INVALID_SIGNATURE_OPAQUE_CLOSURE = "invalid signature of method for opaque closure - `sig` field must always be set to `Tuple`"
Expand Down Expand Up @@ -119,9 +117,6 @@ function validate_code!(errors::Vector{InvalidCodeError}, c::CodeInfo, is_top_le
for x in c.code
if isa(x, Expr)
head = x.head
if !is_top_level
head === :method && push!(errors, InvalidCodeError(NON_TOP_LEVEL_METHOD))
end
narg_bounds = get(VALID_EXPR_HEADS, head, -1:-1)
nargs = length(x.args)
if narg_bounds == -1:-1
Expand All @@ -145,7 +140,7 @@ function validate_code!(errors::Vector{InvalidCodeError}, c::CodeInfo, is_top_le
head === :gc_preserve_end || head === :meta ||
head === :inbounds || head === :foreigncall || head === :cfunction ||
head === :leave || head === :pop_exception ||
head === :method || head === :static_parameter ||
head === :static_parameter ||
head === :new || head === :splatnew || head === :thunk || head === :loopinfo ||
head === :throw_undef_if_not || head === :code_coverage_effect || head === :inline || head === :noinline
validate_val!(x)
Expand Down
18 changes: 18 additions & 0 deletions base/docs/basedocs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2816,6 +2816,24 @@ See also [`const`](@ref).
"""
Core.declare_const

"""
define_method(module::Module, name::Symbol)
define_method(module::Module, fname_or_mt, argdata, code)

Define a method. The 2-argument form declares a new generic function with the given `name`.
The 4-argument form defines a method for a function, where `fname_or_mt` is either the function
or its method table, `argdata` is the signature information, and `code` is the method body.

This function is typically generated by lowering `function` definitions and should not
be called directly in most cases.

!!! compat "Julia 1.14"
This function requires Julia 1.14 or later.

See also [`function`](@ref).
"""
Core.define_method

"""
_import(to::Module, from::Module, asname::Symbol, [sym::Symbol, imported::Bool])

Expand Down
1 change: 1 addition & 0 deletions src/builtin_proto.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ extern "C" {
XX(getfield,"getfield") \
XX(getglobal,"getglobal") \
XX(declare_global,"declare_global") \
XX(define_method,"define_method") \
XX(ifelse,"ifelse") \
XX(intrinsic_call,"intrinsic_call") \
XX(invoke,"invoke") \
Expand Down
26 changes: 26 additions & 0 deletions src/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,32 @@ JL_CALLABLE(jl_f_declare_const)
return nargs > 2 ? args[2] : jl_nothing;
}

// define_method(module::Module, name::Symbol) - declare generic function
// define_method(module::Module, fname_or_mt, argdata, code) - define method
JL_CALLABLE(jl_f_define_method)
{
JL_NARGS(define_method, 2, 4);
JL_TYPECHK(define_method, module, args[0]);
jl_module_t *module = (jl_module_t *)args[0];

// Generic function declaration: define_method(module, name)
if (nargs == 2) {
JL_TYPECHK(define_method, symbol, args[1]);
jl_sym_t *fname = (jl_sym_t*)args[1];
return jl_declare_const_gf(module, fname);
}

// Method definition: define_method(module, fname_or_mt, argdata, code)
jl_value_t *fname = args[1];
jl_methtable_t *mt = NULL;
if (jl_is_mtable(fname))
mt = (jl_methtable_t*)fname;
jl_value_t *atypes = args[2];
jl_value_t *meth = args[3];
jl_method_t *ret = jl_method_def((jl_svec_t*)atypes, mt, (jl_code_info_t*)meth, module);
Copy link
Member

Choose a reason for hiding this comment

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

Does this need a check that the module is "open" or does it already have that internally?

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'll double check, but you may be correct that we were relying on the eval check before.

return (jl_value_t *)ret;
}

// import, using --------------------------------------------------------------

// Import binding `from.sym` as `asname` into `to`:
Expand Down
33 changes: 28 additions & 5 deletions src/julia-syntax.scm
Original file line number Diff line number Diff line change
Expand Up @@ -5094,7 +5094,8 @@ f(x) = yt(x)
(error (string "Global method definition" (linenode-string current-loc)
" needs to be placed at the top level, or use \"eval\".")))
(if (length> e 2)
(let* ((sig (let ((sig (compile (caddr e) break-labels #t #f)))
(let* ((name (cadr e))
(sig (let ((sig (compile (caddr e) break-labels #t #f)))
(if (valid-ir-argument? sig)
sig
(let ((l (make-ssavalue)))
Expand All @@ -5107,12 +5108,34 @@ f(x) = yt(x)
(emit `(= ,l ,(compile lam break-labels #t #f)))
l))))
(let ((val (make-ssavalue)))
(emit `(= ,val (method ,(or (cadr e) '(false)) ,sig ,lam)))
(emit `(= ,val ,(cond ((not name)
`(call (core define_method) (thismodule) (false) ,sig ,lam))
((globalref? name)
`(call (core define_method) ,(cadr name) (inert ,(caddr name)) ,sig ,lam))
((symbol? name)
`(call (core define_method) (thismodule) (inert ,name) ,sig ,lam))
(else
`(call (core define_method) (thismodule) ,name ,sig ,lam)))))
(if (null? (cadr lam)) (emit `(latestworld)))
(if tail (emit-return tail val))
val))
(cond (tail (emit-return tail e))
(value e)
(else (emit e)))))
;; Generic function declaration (short form)
(let ((name (cadr e)))
(if value
(let ((val (make-ssavalue)))
(emit `(= ,val ,(if (globalref? name)
`(call (core define_method) ,(cadr name) (inert ,(caddr name)))
`(call (core define_method) (thismodule) (inert ,name)))))
(if (null? (cadr lam)) (emit `(latestworld)))
(if tail (emit-return tail val))
val)
(begin
(emit ,(if (globalref? name)
`(call (core define_method) ,(cadr name) (inert ,(caddr name)))
`(call (core define_method) (thismodule) (inert ,name))))
(if (null? (cadr lam)) (emit `(latestworld)))
(if tail (emit-return tail '(null)))
'(null))))))
((lambda)
(let ((temp (linearize e)))
(cond (tail (emit-return tail temp))
Expand Down
6 changes: 3 additions & 3 deletions src/toplevel.c
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ static void expr_attributes(jl_value_t *v, jl_array_t *body, int *has_ccall, int
*has_defs = 1;
return;
}
else if (head == jl_method_sym || jl_is_toplevel_only_expr(v)) {
else if (jl_is_toplevel_only_expr(v)) {
*has_defs = 1;
}
else if (head == jl_cfunction_sym) {
Expand Down Expand Up @@ -417,7 +417,7 @@ static void expr_attributes(jl_value_t *v, jl_array_t *body, int *has_ccall, int
*has_ccall = 1;
}
// TODO: rely on latestworld instead of function callee detection here (or add it to jl_is_toplevel_only_expr)
if (called == BUILTIN(_typebody) || called == BUILTIN(declare_const)) {
if (called == BUILTIN(_typebody) || called == BUILTIN(declare_const) || called == BUILTIN(define_method)) {
*has_defs = 1;
}
}
Expand Down Expand Up @@ -489,7 +489,7 @@ int jl_needs_lowering(jl_value_t *e) JL_NOTSAFEPOINT
jl_sym_t *head = ex->head;
if (head == jl_module_sym || head == jl_export_sym || head == jl_public_sym ||
head == jl_thunk_sym || head == jl_toplevel_sym || head == jl_error_sym ||
head == jl_incomplete_sym || head == jl_method_sym) {
head == jl_incomplete_sym) {
return 0;
}
return 1;
Expand Down
40 changes: 40 additions & 0 deletions stdlib/Serialization/src/Serialization.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1454,6 +1454,46 @@ function deserialize_expr(s::AbstractSerializer, len)
resolve_ref_immediately(s, e)
e.head = deserialize(s)::Symbol
e.args = Any[ deserialize(s) for i = 1:len ]

# Rewrite old :method expressions to define_method calls
Copy link
Member

Choose a reason for hiding this comment

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

Seems unlikely we should encounter or support this, since pre-lowered top level code is a pretty buggy concept even in a single session

Copy link
Member Author

Choose a reason for hiding this comment

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

We have a specific test for this case, so I added the compat. We can remove the test if course

Copy link
Member

Choose a reason for hiding this comment

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

Weird, but I guess harmless probably

if e.head === :method
mod = current_module[]
if mod === nothing
# No current module context, keep the :method expression and hope for the best
# This shouldn't happen in practice for deserialization of top-level code
return e
end

if len == 1
# Short form: (:method name) → (call Core.define_method module (inert name))
name = e.args[1]
if name isa GlobalRef
# Extract module and name from GlobalRef
mod_ref = name.mod
sym = name.name
e = Expr(:call, GlobalRef(Core, :define_method), mod_ref, QuoteNode(sym))
else
# Simple symbol - use the current module
e = Expr(:call, GlobalRef(Core, :define_method), mod, QuoteNode(name))
end
elseif len == 3
# Long form: (:method name_or_mt sigtype code) → (call Core.define_method module name_or_mt sigtype code)
name_or_mt = e.args[1]
sigtype = e.args[2]
code = e.args[3]
if name_or_mt isa Symbol
name_or_mt = QuoteNode(name_or_mt)
e = Expr(:call, GlobalRef(Core, :define_method), mod, name_or_mt, sigtype, code)
elseif name_or_mt isa GlobalRef
mod_ref = name_or_mt.mod
sym = name_or_mt.name
e = Expr(:call, GlobalRef(Core, :define_method), mod_ref, QuoteNode(sym), sigtype, code)
else
# name_or_mt is already something else (like false or a method table)
e = Expr(:call, GlobalRef(Core, :define_method), mod, name_or_mt, sigtype, code)
end
end
end
e
end

Expand Down
23 changes: 17 additions & 6 deletions test/syntax.jl
Original file line number Diff line number Diff line change
Expand Up @@ -566,8 +566,10 @@ end
let code = Meta.lower(Main, :(@inline f(p::Int=2) = 3)).args[1].code
local src
for i = length(code):-1:1
if Meta.isexpr(code[i], :method)
src = code[i].args[3]
if Meta.isexpr(code[i], :call) && length(code[i].args) >= 5 &&
code[i].args[1] isa GlobalRef && code[i].args[1].name == :define_method &&
code[i].args[5] isa Core.CodeInfo
src = code[i].args[5]
break
end
end
Expand Down Expand Up @@ -3988,8 +3990,16 @@ end
code = src.args[1].code
for i = length(code):-1:1
expr = code[i]
Meta.isexpr(expr, :method) || continue
@test isa(expr.args[1], Union{GlobalRef, Symbol})
if Meta.isexpr(expr, :call) && length(expr.args) >= 3 &&
expr.args[1] isa GlobalRef && expr.args[1].name == :define_method
# args[3] should be a QuoteNode wrapping a Symbol, or a GlobalRef
name_arg = expr.args[3]
if name_arg isa QuoteNode
@test isa(name_arg.value, Symbol)
else
@test isa(name_arg, GlobalRef)
end
end
end
end

Expand Down Expand Up @@ -4371,10 +4381,11 @@ module DoubleImport
end
@test DoubleImport.Random === Test.Random

# Expr(:method) returns the method
# define_method call returns the method
let ex = @Meta.lower function return_my_method(); 1; end
code = ex.args[1].code
idx = findfirst(ex->Meta.isexpr(ex, :method) && length(ex.args) > 1, code)
idx = findfirst(ex->Meta.isexpr(ex, :call) && length(ex.args) >= 5 &&
ex.args[1] isa GlobalRef && ex.args[1].name == :define_method, code)
code[end] = Core.ReturnNode(Core.SSAValue(idx))
@test isa(Core.eval(@__MODULE__, ex), Method)
end
Expand Down