/
githubmirror
/
julia
Обзор
Документация
Войти
/
githubmirror
/
julia
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
JuliaLowering/src/syntax_macros.jl
441 строка
14 KB
Em Chu
[JuliaLowering] Make `SyntaxTree` a standard tree (#62474)
29 июл 2026, 15:34
Не верифицирован
29 июл 2026, 15:34
92cca2d
Код
Авторство
О чём код?
# The following are versions of macros from Base which act as "standard syntax # extensions": # # * They emit syntactic forms with special `Kind`s and semantics known to # lowering # * There is no other Julia surface syntax for these `Kind`s. # In order to implement these here without getting into bootstrapping problems, # we just write them as plain old macro-named functions and add the required # __context__ argument ourselves. # # TODO: @inline, @noinline, @inbounds, @simd, @ccall, @assume_effects # # TODO: Eventually move these to proper `macro` definitions and use # `JuliaLowering.include()` or something. Then we'll be in the fun little world # of bootstrapping but it shouldn't be too painful :) # Note that `@ast __context__ __context__.macrocall [K"foo" ...]` is unhygienic, # since `@ast` is meant for internal lowering use (it requires an explicit # provenance argument, and then copies any syntax context from the provenance to # any created syntax). A real user-facing macro to replace it should use the # provenance of the literal K"foo" expression in the file instead, and should # not copy context (this is not hard to implement, but the provenance requires # it and callers to be JL-lowered, which this file currently isn't.) function Base.var"@nospecialize"(__context__::MacroContext, exs::SyntaxTree...) if length(exs) == 0 @ast __context__ __context__.macrocall [K"meta" "nospecialize"::K"Identifier"] elseif length(exs) == 1 && kind(exs[1]) === K"=" eq = exs[1] @ast __context__ __context__.macrocall [K"meta" "nospecialize"::K"Identifier" [K"kw"(eq) children(eq)...]] else @ast __context__ __context__.macrocall [K"meta" "nospecialize"::K"Identifier" exs...] end end # TODO: support all forms that the original supports # function Base.var"@atomic"(__context__::MacroContext, ex) # @jl_assert kind(ex) == K"Identifier" || kind(ex) == K"::" (ex, "Expected identifier or declaration") # @ast __context__ __context__.macrocall [K"atomic" ex] # end # TODO: @label function Base.var"@goto"(__context__::MacroContext, ex) @jl_assert kind(ex) == K"Identifier" ex @ast __context__ ex [K"symbolicgoto" ex] end function Base.var"@locals"(__context__::MacroContext) @ast __context__ __context__.macrocall [K"locals"] end @static if isdefined(Base, Symbol("@__FUNCTION__")) function Base.var"@__FUNCTION__"(__context__::MacroContext) @ast __context__ __context__.macrocall [K"thisfunction"] end end function Base.var"@isdefined"(__context__::MacroContext, ex) @ast __context__ __context__.macrocall [K"isdefined" ex] end function Base.var"@generated"(__context__::MacroContext) @ast __context__ __context__.macrocall [K"generated"] end function Base.var"@generated"(__context__::MacroContext, ex) if !(kind(ex) === K"function" || kind(ex) === K"=" && is_eventually_call(ex[1])) throw(LoweringError(ex, "Expected a function argument to `@generated`")) end @ast __context__ __context__.macrocall [K"function" ex[1] [K"block" [K"if" [K"generated"] ex[2] [K"block" [K"meta" "generated_only"::K"Identifier"] [K"return" nothing::K"Value"] ] ] ] ] end function Base.var"@cfunction"(__context__::MacroContext, callable, return_type, arg_types) if kind(arg_types) != K"tuple" throw(MacroExpansionError(arg_types, "@cfunction argument types must be a literal tuple")) end arg_types_svec = @ast __context__ arg_types [K"call" [K"core" "svec"::K"Identifier"] children(arg_types)... ] if kind(callable) == K"$" fptr = callable[1] typ = Base.CFunction else # Kinda weird semantics here - without `$`, the callable is a top level # expression evaluated within the module where the `@cfunction` is # expanded into. fptr = @ast __context__ callable [K"inert" callable ] typ = Ptr{Cvoid} end @ast __context__ __context__.macrocall [K"cfunction" typ::K"Value" fptr return_type arg_types_svec [K"inert" "ccall"::K"Identifier"] ] end function ccall_macro_parse(ctx, exs) gc_safe=false opts = exs[1:end-1] ex = exs[end] for opt in opts @stm opt begin [K"=" [K"Identifier"] val] -> if syntax_name(opt[1]) != "gc_safe" throw(MacroExpansionError(opt[1], "unknown option name for ccall")) elseif !(kind(val) in KSet"Bool Value") throw(MacroExpansionError(val, "gc_safe must be true or false")) else gc_safe = val.value end _ -> throw(MacroExpansionError(opt, "bad option to ccall")) end end if length(opts) >= 2 throw(MacroExpansionError(opts[2], "too many options provided to @ccall")) end (func, argts, rettype) = @stm ex begin [K"::" [K"call" f as...] r] -> let f_expanded = @stm f begin [K"." lib sym] -> @ast ctx f [K"tuple" sym lib] [K"inert" [K"Identifier"]] -> @ast ctx f [K"tuple" f] [K"Identifier"] -> @ast ctx f [K"tuple" [K"inert" f]] [K"$" x] -> let kx = kind(x) if kx in KSet"tuple String string" || (kx === K"Value" && x.value isa Tuple) || kx == K"inert" && !(kind(x[1]) == K"Value" && x[1].value isa Ptr) throw(MacroExpansionError( f, "interpolated value should be a variable or expression, not a literal name or tuple")) end x end _ -> throw(MacroExpansionError( f, "@ccall function name must be a symbol, a `.` node (e.g. `libc.printf`) or an interpolated function pointer (with `\$`)")) end (f_expanded, as, r) end [K"call" _...] -> throw(MacroExpansionError( ex, "expected a return type annotation `::SomeType`", position=:end)) _ -> throw(MacroExpansionError( ex, "expected call expression with return type")) end # detect varargs varargs = nothing argstart = 1 if length(argts) > 0 && kind(argts[1]) == K"parameters" varargs = children(argts[1]) argstart = 2 end # collect args and types args = SyntaxList() types = SyntaxList() function pusharg!(at) @stm at begin [K"::" a t] -> (push!(args, a); push!(types, t)) _ -> throw(MacroExpansionError( at, "argument needs a type annotation")) end end for e in argts[argstart:end] pusharg!(e) end if !isnothing(varargs) num_required_args = length(args) if num_required_args == 0 throw(MacroExpansionError( argts[1], "C ABI prohibits varargs without one required argument")) end for e in varargs pusharg!(e) end else num_required_args = 0 # Non-vararg call end return func, rettype, types, args, gc_safe, num_required_args end function ccall_macro_lower(ctx, ex, convention, func, rettype, types, args, gc_safe, num_required_args) if convention isa Tuple cconv_tuple = (convention..., gc_safe) else cconv_tuple = (convention, UInt16(0), gc_safe) end return @ast ctx ex [K"call" "ccall"::K"Identifier" func [K"cconv" cconv_tuple::K"Value" num_required_args::K"Value"] rettype [K"tuple" types...] args... ] end function Base.var"@ccall"(ctx::MacroContext) throw(ArgumentError("@ccall needs a function signature with a return type")) end function Base.var"@ccall"(ctx::MacroContext, exs...) ccall_macro_lower(ctx, exs[end], :ccall, ccall_macro_parse(ctx, exs)...) end function Base.GC.var"@preserve"(__context__::MacroContext, exs...) idents = exs[1:end-1] for e in idents if kind(e) != K"Identifier" throw(MacroExpansionError(e, "Preserved variable must be a symbol")) end end @ast __context__ __context__.macrocall [K"gc_preserve" exs[end] exs[1:end-1]...] end function Base.Experimental.var"@opaque"(__context__::MacroContext, ex) @jl_assert kind(ex) == K"->" ex @ast __context__ __context__.macrocall [K"opaque_closure" nothing::K"Value" nothing::K"Value" nothing::K"Value" true::K"Bool" ex ] end # @eval should mostly ignore hygiene against our system's best wishes. Still # attempt to preserve provenance. function _at_eval_code(mc::MacroContext, mod_st::SyntaxTree, ex) sc = mc.macrocall.context::SyntaxContext val = remove_context(@ast mc mc.macrocall ("eval_result"::K"Identifier")) q = _legacy_quote_to_syntax((@ast mc mc.macrocall [K"quote" ex]), 0, true) new_sc = SyntaxContext(base_layer(sc).mod, sc.version) @ast mc mc.macrocall [K"block" [K"local" [K"=" val [K"call" JuliaLowering.eval::K"Value" mod_st [K"call" JuliaSyntax.fill_context::K"Value" q new_sc::K"Value"] ] ] ] [K"unknown_head"(;value="latestworld-if-toplevel")] val ] end function Base.var"@eval"(__context__::MacroContext, ex) sc = __context__.macrocall.context::SyntaxContext mod = @ast __context__ __context__.macrocall base_layer(sc).mod::K"Value" _at_eval_code(__context__, mod, ex) end function Base.var"@eval"(__context__::MacroContext, mod, ex) _at_eval_code(__context__, mod, ex) end #-------------------------------------------------------------------------------- # The following `@islocal` and `@inert` are macros for special syntax known to # lowering which don't exist in Base but arguably should. # # For now we have our own versions function var"@islocal"(__context__::MacroContext, ex) @jl_assert kind(ex) == K"Identifier" ex @ast __context__ __context__.macrocall [K"islocal" ex] end """ A non-interpolating quoted expression. For example, ```julia @inert quote \$x end ``` does not take `x` from the surrounding scope - instead it leaves the interpolation `\$x` intact as part of the expression tree. TODO: What is the correct way for `@inert` to work? ie which of the following should work? ```julia @inert quote body end @inert begin body end @inert x @inert \$x ``` The especially tricky cases involve nested interpolation ... ```julia quote @inert \$x end @inert quote quote \$x end end @inert quote quote \$\$x end end ``` etc. Needs careful thought - we should probably just copy what lisp does with quote+quasiquote 😅 """ function var"@inert"(__context__::MacroContext, ex) @jl_assert kind(ex) == K"quote" ex @ast __context__ __context__.macrocall [K"inert" ex] end # `quote`/`inert` for syntaxtree function var"@syntaxinert"(__context__::MacroContext, st) @ast __context__ __context__.macrocall [K"syntaxinert" st] end function var"@syntaxquote"(__context__::MacroContext, st) @ast __context__ __context__.macrocall [K"syntaxquote" st] end # not particularly good or useful, as @syntaxquote must expand first function var"@syntaxunquote"(__context__::MacroContext, st) @ast __context__ __context__.macrocall [K"syntaxunquote" st] end # If the syntax version allows, convert quote/$ to syntaxquote/syntaxunquote. # This is just a convenient way to create SyntaxTree with full provenance # without dedicated surface syntax, mainly for testing metaprogramming in JL. # It is insufficient in many ways, e.g. not all forms can be expressed (need # surface syntax) function var"@legacy_quote_to_syntax"(__context__::MacroContext, st) @jl_assert kind(st) === K"quote" || kind(st) === K"inert" st if is_flisp_compat(__context__.macrocall) st elseif kind(st) === K"inert" @mknode(st; kind=K"syntaxinert") # parser simplifies quote to inert else _legacy_quote_to_syntax(st, 0, false) end end function _legacy_quote_to_syntax(st::SyntaxTree, depth, force::Bool) k = kind(st) if k === K"quote" && depth == 0 && (force || !is_flisp_compat(st)) @jl_assert numchildren(st) == 1 st @mknode(st; kind=K"syntaxquote", children= mapsyntax(c->_legacy_quote_to_syntax(c, depth+1, force), children(st))) elseif k === K"$" && depth == 1 && (force || !is_flisp_compat(st)) @jl_assert numchildren(st) == 1 (st, "bad multi-syntaxunquote") @mknode(st; kind=K"syntaxunquote") else depth2 = k === K"quote" ? depth + 1 : k === K"$" ? depth - 1 : depth cs = SyntaxList() for c in children(st) # Convert multi-unquote to single unquote if depth2 == 1 && kind(c) === K"$" && numchildren(c) > 1 for c2 in children(c) push!(cs, @ast _ c [K"$" c2]) end else push!(cs, c) end end cs_out = mapsyntax(c->_legacy_quote_to_syntax(c, depth2, force), cs) cs_out == children(st) ? st : @mknode(st; children=cs_out) end end macro legacy_quote_to_syntax(x) esc(x) end """ Retrieve the syntax version of the macrocall """ function var"@syntax_version"(__context__::MacroContext) (__context__.macrocall.context::SyntaxContext).version end macro syntax_version() JL_OLD_SYNTAX_VERSION end """ Set the syntax version of some syntax. This can be used to define macros producing older syntax than the current version. """ function var"@syntax_version"(__context__::MacroContext, ver_st, st) kind(st) === K"macro" || throw(LoweringError( st, "`@syntax_version version macro` only supports macro definitions")) ver = JuliaLowering.eval(syntax_module(ver_st), ver_st) ver isa VersionNumber || throw(LoweringError( ver_st, "version argument should be literal `v\"...\" call`")) _ensure_syntax_version(st, ver) end macro syntax_version(_, x) throw(ArgumentError("@syntax_version can't set version when lowering with flisp")) end function _ensure_syntax_version(st, ver::VersionNumber) st_sc = st.context::SyntaxContext sc = st_sc.version == ver ? st_sc : SyntaxContext(st_sc.layer, st_sc.unexpanded, ver, st_sc.internal) if is_leaf(st) || numchildren(st) == 0 st_sc == sc ? st : @mknode(st; context=sc) else out = mapchildren(c->_ensure_syntax_version(c, ver), st) (st_sc === sc && out === st) ? out : @mknode(st; context=sc) end end