/
githubmirror
/
julia
Обзор
Документация
Войти
/
githubmirror
/
julia
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
JuliaLowering/test/macros.jl
1 899 строк
70 KB
Em Chu
[JuliaLowering] Make `SyntaxTree` a standard tree (#62474)
29 июл 2026, 15:34
Не верифицирован
29 июл 2026, 15:34
92cca2d
Код
Авторство
О чём код?
test_mod = @newmod(macro_test) @eval test_mod import JuliaLowering Base.eval(test_mod, :(const var"@ast" = $(JuliaLowering.var"@ast"))) # Set up identity macros for use in this file # - `old_e`, escaping its whole output, should do nothing to an expression # - `new_m`, introducing no new syntax, should behave exactly as `old_e` does # - `old_*` should behave the same across JL and flisp # - `old_h` should not be specified too hard here (buggy renaming pass) fl_eval(test_mod, :(macro old_e(x); esc(x); end)) fl_eval(test_mod, :(macro old_h(x); x; end)) JuliaLowering.include_string(test_mod, "macro new_m(x); x; end") fl_eval(test_mod, :(global mvar = "global mvar")) @testset "syntax versioning sanity-check" begin @test JuliaLowering.include_string( test_mod, "JuliaLowering.@syntax_version") == JuliaSyntax.JL_NEW_SYNTAX_VERSION @test JuliaLowering.include_string( test_mod, "JuliaLowering.@syntax_version"; expr_compat_mode=false) == JuliaSyntax.JL_NEW_SYNTAX_VERSION @test JuliaLowering.include_string( test_mod, "JuliaLowering.@syntax_version"; expr_compat_mode=true) == JuliaSyntax.JL_OLD_SYNTAX_VERSION # TODO: test the version of returned syntax @test JuliaLowering.include_string(@newmod(), """ JuliaLowering.@syntax_version JuliaSyntax.JL_NEW_SYNTAX_VERSION macro m(); end """; expr_compat_mode=false) isa Function @test JuliaLowering.include_string(@newmod(), """ JuliaLowering.@syntax_version JuliaSyntax.JL_NEW_SYNTAX_VERSION macro m(); end """; expr_compat_mode=true) isa Function @test JuliaLowering.include_string(@newmod(), """ JuliaLowering.@syntax_version JuliaSyntax.JL_OLD_SYNTAX_VERSION macro m(); end """; expr_compat_mode=false) isa Function @test JuliaLowering.include_string(@newmod(), """ JuliaLowering.@syntax_version JuliaSyntax.JL_OLD_SYNTAX_VERSION macro m(); end """; expr_compat_mode=true) isa Function end # Basic checks that arbitrary nesting of transparent macros (no new syntax in new # macros, escaped/unhygienic in old macros) doesn't introduce opaque layers @testset "basic transparent macros: old macros" for run in [ (x::String)->Base.include_string( test_mod, "#=FLISP SANITY-CHECK=# "*x), (x::String)->JuliaLowering.include_string( test_mod, "#=JL COMPAT=# "*x; expr_compat_mode=true), (x::String)->JuliaLowering.include_string( test_mod, "#=JL=# "*x; expr_compat_mode=false)] @test run("@old_e let mvar = 0; mvar; end") == 0 @test run("@old_e let @old_e(mvar = 0); mvar; end") == 0 @test run("@old_e let @old_e(@old_e(mvar = 0)); mvar; end") == 0 @test run("@old_e let @old_e(mvar) = 0; mvar; end") == 0 @test run("@old_e let @old_e(@old_e(mvar)) = 0; mvar; end") == 0 @test run("@old_e let mvar = 0; @old_e(mvar); end") == 0 @test run("@old_e let mvar = 0; @old_e(@old_e(mvar)); end") == 0 @test run("@old_e let @old_e(@old_e(mvar) = 0); @old_e(mvar); end") == 0 @test run("@old_h let mvar = 0; mvar; end") == 0 @test run("@old_h let @old_e(mvar = 0); mvar; end") == 0 @test run("@old_h let @old_e(@old_e(mvar = 0)); mvar; end") == 0 @test run("@old_h let @old_e(mvar) = 0; mvar; end") == 0 @test run("@old_h let @old_e(@old_e(mvar)) = 0; mvar; end") == 0 @test run("@old_h let mvar = 0; @old_e(mvar); end") == 0 @test run("@old_h let mvar = 0; @old_e(@old_e(mvar)); end") == 0 @test run("@old_h let @old_e(@old_e(mvar) = 0); @old_e(mvar); end") == 0 @test run("@old_h @old_h let mvar = 0; mvar; end") == 0 @test run("@old_h @old_h let @old_e(mvar = 0); mvar; end") == 0 @test run("@old_h @old_h let @old_e(@old_e(mvar = 0)); mvar; end") == 0 @test run("@old_h @old_h let @old_e(mvar) = 0; mvar; end") == 0 @test run("@old_h @old_h let @old_e(@old_e(mvar)) = 0; mvar; end") == 0 @test run("@old_h @old_h let mvar = 0; @old_e(mvar); end") == 0 @test run("@old_h @old_h let mvar = 0; @old_e(@old_e(mvar)); end") == 0 @test run("@old_h @old_h let @old_e(@old_e(mvar) = 0); @old_e(mvar); end") == 0 end @testset "basic transparent macros: new macros only" for expr_compat_mode in [true, false] local run = (x::String)->JuliaLowering.include_string(test_mod, x; expr_compat_mode) @test run("@new_m let mvar = 0; mvar; end") == 0 @test run("@new_m let @new_m(mvar = 0); mvar; end") == 0 @test run("@new_m let @new_m(@new_m(mvar = 0)); mvar; end") == 0 @test run("@new_m let @new_m(mvar) = 0; mvar; end") == 0 @test run("@new_m let @new_m(@new_m(mvar)) = 0; mvar; end") == 0 @test run("@new_m let mvar = 0; @new_m(mvar); end") == 0 @test run("@new_m let mvar = 0; @new_m(@new_m(mvar)); end") == 0 @test run("@new_m let @new_m(@new_m(mvar) = 0); @new_m(mvar); end") == 0 end @testset "basic transparent macros: new+old interop" for expr_compat_mode in [true, false], mcall in ["@old_e ", "@new_m ", "@old_e @new_m ", "@new_m @old_e "], old_h in ["", "@old_h "] local run = (x::String)->JuliaLowering.include_string(test_mod, x; expr_compat_mode) @test run(old_h*mcall*"let mvar = 0; mvar; end") == 0 @test run(old_h*"let ("*mcall*"mvar = 0); mvar; end") == 0 @test run(old_h*"let ("*mcall*"mvar) = 0; mvar; end") == 0 @test run(old_h*"let mvar = 0; ("*mcall*"mvar); end") == 0 @testset for mcall2 in ["@old_e ", "@new_m ", "@old_e @new_m ", "@new_m @old_e "] @test run(old_h*mcall*"let ("*mcall2*"mvar) = 0; mvar; end") == 0 @test run(old_h*mcall*"let mvar = 0; ("*mcall2*"mvar); end") == 0 @test run(old_h*"let ("*mcall*"mvar = 0); ("*mcall2*"mvar); end") == 0 end end # More simple checks with no difference between macro module and macrocall module isdefined(test_mod, :x) && Base.delete_binding(test_mod, :x) fl_eval(test_mod, :(macro old_read_x(); :x; end)) fl_eval(test_mod, :(macro old_suggest_x(arg) quote let x = "suggested (old)" $(esc(arg)) end end end)) JuliaLowering.include_string(test_mod, raw""" macro new_read_x(); @legacy_quote_to_syntax :x; end """) JuliaLowering.include_string(test_mod, raw""" macro new_suggest_x(arg) @legacy_quote_to_syntax quote let x = "suggested (new)" $arg end end end """) @testset "basic hygiene: check that name resolution fails where it should (flisp)" for run in [ (x::String)->fl_eval(test_mod,JuliaSyntax.parsestmt(Expr, "#=FLISP SANITY-CHECK=# "*x)), (x::String)->JuliaLowering.include_string( test_mod, "#=JL COMPAT=# "*x; expr_compat_mode=true), (x::String)->JuliaLowering.include_string( test_mod, "#=JL=# "*x; expr_compat_mode=false)] @test_throws UndefVarError run("@old_read_x()") @test_throws UndefVarError run("let x = 0; @old_read_x(); end") @test_throws UndefVarError run("@old_suggest_x(x)") @test_throws UndefVarError run("@old_suggest_x(@old_read_x())") @test run("let x = 1; @old_suggest_x(x); end") == 1 @test run("@old_suggest_x(let x = 1; x; end)") == 1 @test_throws UndefVarError run("@old_suggest_x(let x = 1; @old_read_x(); end)") == 1 end @testset "basic hygiene: check that name resolution fails where it should (new)" for run in [ (x::String)->JuliaLowering.include_string( test_mod, "#=JL COMPAT=# "*x; expr_compat_mode=true), (x::String)->JuliaLowering.include_string( test_mod, "#=JL=# "*x; expr_compat_mode=false)] @test_throws UndefVarError run("@new_read_x()") @test_throws UndefVarError run("let x = 0; @new_read_x(); end") @test_throws UndefVarError run("@new_suggest_x(x)") @test_throws UndefVarError run("@new_suggest_x(@new_read_x())") @test run("let x = 1; @new_suggest_x(x); end") == 1 @test run("@new_suggest_x(let x = 1; x; end)") == 1 @test_throws UndefVarError run("@new_suggest_x(let x = 1; @new_read_x(); end)") == 1 @testset "old/new interop" begin @testset for wrapper in ["", "@old_e ", "@old_h ", "@new_m "] @test_throws UndefVarError run(wrapper*"@old_suggest_x(@new_read_x())") @test_throws UndefVarError run(wrapper*"@new_suggest_x(@old_read_x())") @test run(wrapper*"let x = 1; @old_suggest_x(x); end") == 1 @test run(wrapper*"let x = 1; @new_suggest_x(x); end") == 1 @test run(wrapper*"@old_suggest_x(let x = 1; x; end)") == 1 @test run(wrapper*"@new_suggest_x(let x = 1; x; end)") == 1 end end end @eval test_mod (global test_mod_global = "test_mod_global") @newmod(EvalMod, test_mod) @testset "@eval" for run in [ (x::String)->fl_eval(test_mod,JuliaSyntax.parsestmt(Expr, "#=FLISP SANITY-CHECK=# "*x)), (x::String)->JuliaLowering.include_string( test_mod, "#=JL COMPAT=# "*x; expr_compat_mode=true), (x::String)->JuliaLowering.include_string( test_mod, "#=JL=# "*x; expr_compat_mode=false)] has_syntax = run(raw"@legacy_quote_to_syntax :x") isa SyntaxTree treetype = has_syntax ? SyntaxTree : Expr symtype = has_syntax ? SyntaxTree : Symbol valtype = has_syntax ? SyntaxTree : Any @test run(raw"@eval nothing") == nothing @test run(raw"@eval :sym") == :sym @test run(raw"@eval QuoteNode(:sym)") == QuoteNode(:sym) @test run(raw"@eval Expr(:call, :identity, 1)") == Expr(:call, :identity, 1) @test run(raw"@eval :(identity(1))") == Expr(:call, :identity, 1) # syntax version of the caller should be propagated to JL.eval @test run(raw"@eval @legacy_quote_to_syntax(:sym)") isa symtype @test run(raw"@eval @legacy_quote_to_syntax(:(identity(1)))") isa treetype @test run(raw"@eval @eval @legacy_quote_to_syntax(:(identity(1)))") isa treetype # quoting behaves the same as outside of eval @test run(raw"@eval(:(1 + 2))") == Expr(:call, :+, 1, 2) @test run(raw"@eval(:true)") == true @test run(raw"@eval(:x)") == :x # interpolation @test run(raw"let x = nothing; @eval $x; end") == nothing @test run(raw"let x = :identity; @eval $x; end") == Base.identity @test run(raw"let x = QuoteNode(:sym); @eval $x; end") == :sym @test run(raw"let x = Expr(:call, :identity, 1); @eval $x; end") == 1 @test run(raw"let x = :(identity(1)); @eval $x; end") == 1 @test run(raw"let x = @legacy_quote_to_syntax(:identity); @eval $x; end") == Base.identity @test run(raw"let x = @legacy_quote_to_syntax(:(identity(1))); @eval $x; end") == 1 # interpolate into quote @test run(raw"let test_mod_global = 0xbad @eval (@legacy_quote_to_syntax :($test_mod_global)) end") isa valtype @test run(raw"let test_mod_global = 0xbad @eval @legacy_quote_to_syntax(:(1,$test_mod_global)) end") isa treetype # interpolate into quote, double-unquote (mixes of syntax and expr may not # need to work) @test run(raw"let x = @legacy_quote_to_syntax(:identity) @eval (:($($x))) end") == Base.identity @test run(raw"let x = @legacy_quote_to_syntax(:identity) @eval (@legacy_quote_to_syntax :($($x))) end") isa valtype @test run(raw"let x = @legacy_quote_to_syntax(:identity) @eval @legacy_quote_to_syntax(:(1,$$x)) end") isa treetype @test run(raw"let x = @legacy_quote_to_syntax(:identity) @eval $(@eval (:(1,$$x))) end") == (1, Base.identity) # module eval-ed into @test run(raw"@eval @__MODULE__") == test_mod @test run(raw"@eval @eval @__MODULE__") == test_mod # two-arg eval should not obey typical hygiene: decls go to specified module @test run(raw"@eval EvalMod @__MODULE__") == test_mod.EvalMod run(raw"@eval EvalMod global eval_mod_global = 1"); Core.@latestworld @test test_mod.EvalMod.eval_mod_global == 1 run(raw"@eval EvalMod eval_mod_global_implicit = 1"); Core.@latestworld @test test_mod.EvalMod.eval_mod_global_implicit == 1 # standard hygiene atop two-arg eval fl_eval(test_mod, :(module MacroMod module MacroModInner; end macro m_setglobal(); esc(:(mmglobal0 = 0)); end macro m_eval_inner(x); :(@eval $MacroModInner $x) ; end end)) Core.@latestworld @eval test_mod.EvalMod (const MacroMod2 = $(test_mod.MacroMod)) @eval test_mod.EvalMod (const var"@legacy_quote_to_syntax" = $(test_mod.var"@legacy_quote_to_syntax")) @test run(raw"@eval EvalMod @legacy_quote_to_syntax(:(1 + 1))") isa treetype @test run(raw"@eval @eval EvalMod @legacy_quote_to_syntax(:(1 + 1))") isa treetype @test run(raw"@eval EvalMod MacroMod2.@m_setglobal") == 0 Core.@latestworld @test isdefined(test_mod.EvalMod, :mmglobal0) @test !isdefined(test_mod, :mmglobal0) @test !isdefined(test_mod.MacroMod, :mmglobal0) @test run(raw"@eval EvalMod MacroMod2.@m_eval_inner(global mmglobal1 = 1)") == 1 Core.@latestworld @test isdefined(test_mod.MacroMod.MacroModInner, :mmglobal1) @test !isdefined(test_mod, :mmglobal1) @test !isdefined(test_mod.MacroMod, :mmglobal1) @test !isdefined(test_mod.EvalMod, :mmglobal1) # interpolation into top-level: symbol declared in the new module run(raw"let x = @legacy_quote_to_syntax(:sym) @eval EvalMod module tmp; module inner_eval_mod; global $x = 123; end; end end") isa Module Core.@latestworld @test test_mod.EvalMod.tmp.inner_eval_mod isa Module @test test_mod.EvalMod.tmp.inner_eval_mod.sym == 123 # hygiene run("let eval_result = 0; @eval 1+1; eval_result; end") == 0 @testset "(AI) single-arg @eval does not over-preserve hygiene" for expr_compat_mode in (true, false) root = @newmod(root) JuliaLowering.include_string(root, raw""" module MacB import JuliaLowering.@legacy_quote_to_syntax macro do_eval() @legacy_quote_to_syntax quote @eval (@__MODULE__) end end end module MacA import JuliaLowering.@legacy_quote_to_syntax import ..MacB macro wrap() @legacy_quote_to_syntax quote @eval (@__MODULE__) end end macro via_b() @legacy_quote_to_syntax quote MacB.@do_eval() end end macro wrap_ee() @legacy_quote_to_syntax quote @eval @eval (@__MODULE__) end end macro wrap_two_arg() # two-arg control: explicit target module; the payload's # `@__MODULE__` must still see the *target* module @legacy_quote_to_syntax quote @eval MacB (@__MODULE__) end end macro wrap_arg(ex) # caller-provided payload (caller's hygiene layer) @legacy_quote_to_syntax quote @eval $ex end end macro wrap_fn() # `@eval` captures the module current when the enclosing function # *definition* is expanded, like flisp @legacy_quote_to_syntax quote () -> @eval (@__MODULE__) end end macro mkmod() mod = gensym("EvalMod") @legacy_quote_to_syntax quote @eval module $mod const inside = (@__MODULE__) end end end macro mkmod_payload(ex) mod = gensym("EvalMod2") @legacy_quote_to_syntax quote @eval module $mod $ex end end end end module Sub import ..MacA end """; expr_compat_mode) Core.@latestworld run(str) = JuliaLowering.include_string(root, str; expr_compat_mode) # `@eval` inside another macro's unescaped expansion evaluates in the # caller's module, not the macro's @test run("MacA.@wrap()") === root # ... even when the `@eval`-ing macro is called by another macro's expansion # (flisp: still the dynamic module, not either macro's module) @test run("MacA.@via_b()") === root # `@eval` nested in `@eval` re-expands against the outer target @test run("MacA.@wrap_ee()") === root # two-arg control: explicit module wins; payload `@__MODULE__` follows it @test run("MacA.@wrap_two_arg()") === root.MacB # macro-generated closure: `@eval` binds the definition-time module @test Base.invokelatest(run("MacA.@wrap_fn()")) === root # the same macro evaluated into a different module follows the live module @test JuliaLowering.include_string( root.Sub, "MacA.@wrap()"; expr_compat_mode) === root.Sub # Caller-provided payloads evaluate in the caller's module @test run("MacA.@wrap_arg(arg_marker = (@__MODULE__))") === root if !expr_compat_mode # With SyntaxTree-passed arguments the payload keeps the caller's # hygiene: the global lands in `root` and is visible there. (In # expr_compat_mode the old-style Expr round-trip re-layers the payload # with the macro's hygiene and the assignment becomes a hygienic # toplevel local -- a pre-existing divergence from flisp tracked by # the "hygienic toplevel assignments" TODO in scope_analysis.jl.) @test Base.invokelatest(isdefined, root, :arg_marker) @test Base.invokelatest(getfield, root, :arg_marker) === root end # The SafeTestsets shape: a macro-generated `@eval module $mod ... end` # creates the module under the dynamic (caller) module m = run("MacA.@mkmod()") @test m isa Module @test parentmodule(m) === root @test Base.invokelatest(getfield, m, :inside) === m # ... and user payload interpolated into the module body sees the fresh # module as its dynamic module (a user's own `@eval` inside a # `@safetestset` acts on the anonymous test module) m2 = run("MacA.@mkmod_payload(@eval user_marker = (@__MODULE__))") @test m2 isa Module @test parentmodule(m2) === root @test Base.invokelatest(getfield, m2, :user_marker) === m2 if expr_compat_mode # Escaped expansions (old-style macros only): same dynamic target JuliaLowering.include_string(root, raw""" module MacEsc macro wrap_esc() esc(quote @eval esc_marker = (@__MODULE__) end) end end """; expr_compat_mode) Core.@latestworld @test run("MacEsc.@wrap_esc()") === root @test Base.invokelatest(isdefined, root, :esc_marker) @test Base.invokelatest(getfield, root, :esc_marker) === root end # An old-style (flisp-defined and -lowered) macro whose expansion calls # `@eval` gets the same treatment when invoked under JuliaLowering fl_eval(root, :(module MacFl macro flwrap() quote @eval (@__MODULE__) end end end)) Core.@latestworld @test run("MacFl.@flwrap()") === root end @testset "(AI) const shows up in caller mod" begin Core.eval(test_mod, :(module MacHome2 macro make_const() :( @eval const CMARKER = 42 ) end end)) Core.eval(test_mod, :(MacHome2.@make_const())) @test isdefined(test_mod, :CMARKER) JuliaLowering.eval(test_mod, :(module MacHome2 macro make_const() :( @eval const CMARKER = 42 ) end end); expr_compat_mode=true) JuliaLowering.eval(test_mod, :(MacHome2.@make_const())) @test isdefined(test_mod, :CMARKER) end end @eval test_mod module hscope_mod; global hscope_g = 123; end @eval test_mod module nothing_mod; end @eval test_mod global hscope_g = 234 @testset "hygienic scope should be usable without macros" begin @test JuliaLowering.eval( test_mod, Expr( Symbol("hygienic-scope"), 1, test_mod); expr_compat_mode=true) == 1 @test JuliaLowering.eval( test_mod, Expr( Symbol("hygienic-scope"), :hscope_g, test_mod.hscope_mod); expr_compat_mode=true) == 123 @test JuliaLowering.eval( test_mod, Expr( Symbol("hygienic-scope"), Expr(:escape, :hscope_g), test_mod.nothing_mod); expr_compat_mode=true) == 234 @test JuliaLowering.eval( test_mod, Expr( Symbol("hygienic-scope"), Expr( Symbol("hygienic-scope"), Expr(:escape, Expr(:escape, :hscope_g)), test_mod.nothing_mod), test_mod.nothing_mod); expr_compat_mode=true) == 234 @test JuliaLowering.eval( test_mod, Expr( Symbol("hygienic-scope"), Expr( Symbol("hygienic-scope"), Expr(:escape, :hscope_g), test_mod.nothing_mod), test_mod.hscope_mod); expr_compat_mode=true) == 123 end Base.eval(test_mod, :( test_hscope(x, mod=$test_mod) = Expr(Symbol("hygienic-scope"), x, mod) )) Base.eval(test_mod, :( # +3 new scopes and -4 escapes = normal unhygienic macro macro oldstyle_silly_scopes(x, y) stmt1 = test_hscope(test_hscope(test_hscope(esc(esc(esc(esc(:($x = 123)))))))) stmt2 = esc(test_hscope(esc(test_hscope(esc(test_hscope(esc(:($y = 456)))))))) Expr(:block, stmt1, stmt2) end)) @testset "escape and hygienic-scope forms" for run in [ (x::String)->Base.include_string( test_mod, "#=FLISP SANITY-CHECK=# "*x), (x::String)->JuliaLowering.include_string( test_mod, "#=JL COMPAT=# "*x; expr_compat_mode=true), (x::String)->JuliaLowering.include_string( test_mod, "#=JL=# "*x; expr_compat_mode=false)] @test run(raw""" let (x, y) = (0, 0); @oldstyle_silly_scopes(x, y); (x, y); end """) === (123, 456) @test run(raw"""begin global_x, global_y = 0, 0 @oldstyle_silly_scopes(global_x, global_y) global_x, global_y end""") === (123, 456) end @testset "apply_expansion_layer mutation testing" begin local test_mod = @newmod(apply_expansion_layer) # recursion can't stop at module/toplevel/inert without tweaks, because a # macro can pull random stuff out of it. This also tests calling into macro # expansion from macros, mostly because re-using macros I've already written # is the easiest way to create non-surface-syntax SyntaxTree as of writing. JuliaLowering.include_string(test_mod, raw""" macro undo_inert(x) x2 = JuliaLowering.macroexpand(x) x2[1] end """) @test JuliaLowering.include_string(test_mod, raw""" let foo = 1; @undo_inert(:foo); end """) == 1 @test JuliaLowering.include_string(test_mod, raw""" let foo = 1; @undo_inert(@legacy_quote_to_syntax(:foo)); end """) == 1 JuliaLowering.include_string(test_mod, raw""" macro mk_toplevel(x, y, z) JuliaSyntax.newnode( __context__.macrocall, K"toplevel", JuliaSyntax.SyntaxList(x, y, z)) end macro toplevel_first_child(x) x2 = JuliaLowering.macroexpand(x) x2[1] end """) JuliaLowering.include_string(test_mod, raw""" macro mk_module(x, y, z) @legacy_quote_to_syntax :(module mk_module_mod; $x; $y; $z; end) end macro module_first_child(x) x2 = JuliaLowering.macroexpand(x) x2[end][1] end """) # sanity @test JuliaLowering.include_string(test_mod, """ @mk_toplevel(1, :y, "z") """) == "z" @test JuliaLowering.include_string(test_mod, """ @mk_module(1, :y, "z") """) isa Module @test JuliaLowering.include_string(test_mod, """ let (x, y, z) = (1, :y, "z") @toplevel_first_child(@mk_toplevel(x, y, z)) end """) == 1 @test JuliaLowering.include_string(test_mod, """ let (x, y, z) = (1, :y, "z") @toplevel_first_child(@mk_toplevel(x, y, z)) end """) == 1 # escape should obey quote/unquote JuliaLowering.include_string(test_mod, raw""" macro esc_in_quote(); Expr(:quote, Expr(:escape, :x)); end """; expr_compat_mode=true) @test JuliaLowering.include_string(test_mod, raw""" @esc_in_quote """; expr_compat_mode=true) == Expr(:escape, :x) @test JuliaLowering.include_string(test_mod, raw""" @esc_in_quote """) == Expr(:escape, :x) JuliaLowering.include_string(test_mod, raw""" macro esc_in_unquote(); Expr(:quote, Expr(:$, Expr(:escape, :x))); end """; expr_compat_mode=true) @test JuliaLowering.include_string(test_mod, raw""" let x = 1; @esc_in_unquote(); end """; expr_compat_mode=true) == 1 @test JuliaLowering.include_string(test_mod, raw""" let x = 1; @esc_in_unquote(); end """) == 1 end JuliaLowering.include_string(test_mod, raw""" module M using ..JuliaLowering: JuliaLowering, adopt_scope, @legacy_quote_to_syntax using ..JuliaSyntax # Introspection macro __MODULE__() JuliaLowering.syntax_module(__context__.macrocall) end macro __FILE__() JuliaLowering.filename(__context__.macrocall) end macro __LINE__() JuliaLowering.source_location(__context__.macrocall)[1] end someglobal = "global in module M" # Macro with local variables macro foo(ex) @legacy_quote_to_syntax :(begin x = "`x` from @foo" (x, someglobal, $ex) end) end # Set `a_global` in M macro set_a_global(val) @legacy_quote_to_syntax :(begin global a_global = $val end) end macro set_other_global(ex, val) @legacy_quote_to_syntax :(begin global $ex = $val end) end macro set_global_in_parent(ex) sym_ex = @legacy_quote_to_syntax quote; sym_introduced_from_M; end e1 = adopt_scope(__context__.macrocall, sym_ex[1]) @legacy_quote_to_syntax quote $e1 = $ex nothing end end macro inner() @legacy_quote_to_syntax :(y, z) end macro outer() @legacy_quote_to_syntax :((x, @inner)) end macro recursive(N) Nval = N.value::Int if Nval < 1 return N end @legacy_quote_to_syntax quote x = $N (x, @recursive $(Nval-1)) end end end """) @test JuliaLowering.include_string(test_mod, """ let x = "`x` from outer scope" M.@foo x end """) == ("`x` from @foo", "global in module M", "`x` from outer scope") @test !isdefined(test_mod.M, :x) @test JuliaLowering.include_string(test_mod, """ #line1 (M.@__MODULE__(), M.@__FILE__(), M.@__LINE__()) """, "foo.jl") == (test_mod, "foo.jl", 2) @test !isdefined(test_mod.M, :a_global) @test JuliaLowering.include_string(test_mod, """ begin M.@set_a_global 42 M.a_global end """) == 42 JuliaLowering.include_string(test_mod, """ M.@set_global_in_parent "bent hygiene!" """) @test test_mod.sym_introduced_from_M == "bent hygiene!" JuliaLowering.include_string(test_mod, "M.@set_other_global global_in_test_mod 100") @test !isdefined(test_mod.M, :global_in_test_mod) @test test_mod.global_in_test_mod == 100 @test JuliaLowering.include_string(test_mod, """ M.@recursive 3 """) == (3, (2, (1, 0))) ex = JuliaLowering.parsestmt(JuliaLowering.SyntaxTree, "M.@outer()", filename="foo.jl") expanded = JuliaLowering.macroexpand(test_mod, ex) @test JuliaSyntax.sourcetext.(JuliaLowering.flattened_provenance(expanded[2])) == [ "M.@outer()" "@inner" "(y, z)" ] @testset "expansion special case: macrocall in do expression" for expr_compat_mode in [true, false] @test JuliaLowering.include_string(test_mod, raw""" macro mac_called_in_do_expression(dofunc, arg) @legacy_quote_to_syntax :($dofunc($arg)) end """; expr_compat_mode) isa Function @test JuliaLowering.include_string(test_mod, raw""" @mac_called_in_do_expression(9) do x x * 10 end """; expr_compat_mode) == 90 @test JuliaLowering.include_string(test_mod, raw""" let fp = @cfunction(Cint, (Cint,)) do x x + Cint(1) end ccall(fp isa Ptr ? fp : fp.ptr, Cint, (Cint,), 2) end """; expr_compat_mode) == 3 end @test JuliaLowering.include_string(test_mod, raw""" v"1.14" """) isa VersionNumber @test JuliaLowering.include_string(test_mod, raw""" v"1.14" """;expr_compat_mode=true) isa VersionNumber @test JuliaLowering.include_string(test_mod, raw""" Base.Experimental.@VERSION """) isa NamedTuple @test JuliaLowering.include_string(test_mod, raw""" Base.Experimental.@VERSION """;expr_compat_mode=true) isa NamedTuple # World age support for macro expansion JuliaLowering.include_string(test_mod, raw""" macro world_age_test() 1 end """) world1 = Base.get_world_counter() JuliaLowering.include_string(test_mod, raw""" macro world_age_test() 2 end """) world2 = Base.get_world_counter() call_world_arg_test = JuliaLowering.rebase_layers(JuliaLowering.parsestmt(JuliaLowering.SyntaxTree, "@world_age_test()"), test_mod, JuliaSyntax.JL_NEW_SYNTAX_VERSION) @test JuliaLowering.expand_forms_1(call_world_arg_test, world1, true) ≈ @ast_ 1::K"Value" @test JuliaLowering.expand_forms_1(call_world_arg_test, world2, true) ≈ @ast_ 2::K"Value" JuliaLowering.include_string(test_mod, """ f_throw(x) = throw(x) macro m_throw(x) :(\$(f_throw(x))) end """) let (err, st) = try JuliaLowering.include_string(test_mod, "_never_exist = @m_throw 42") catch e e, stacktrace(catch_backtrace()) end @test err isa JuliaLowering.MacroExpansionError @test !isnothing(err.err) # Check that `catch_backtrace` can capture the stacktrace of the macro functions @test any(sf->sf.func===:f_throw, st) # TODO: store this in DebugInfo @test_broken any(sf->sf.func===Symbol("@m_throw"), st) @test any(sf->sf.func===Symbol("macro expansion"), st) end let err = try JuliaLowering.include_string(test_mod, "_never_exist = @m_not_exist 42") catch e e end @test err isa JuliaLowering.MacroExpansionError @test err.msg == "Macro not found" @test err.err isa UndefVarError end @test JuliaLowering.include_string(test_mod, "@ccall strlen(\"foo\"::Cstring)::Csize_t") == 3 @test JuliaLowering.include_string(test_mod, "@ccall gc_safe=true strlen(\"asdf\"::Cstring)::Csize_t") == 4 @test JuliaLowering.include_string(test_mod, """ begin buf = zeros(UInt8, 20) @ccall sprintf(buf::Ptr{UInt8}, "num:%d str:%s"::Cstring; 42::Cint, "hello"::Cstring)::Cint String(buf) end """) == "num:42 str:hello\0\0\0\0" let (err, st) = try JuliaLowering.include_string(test_mod, "@ccall strlen(\"foo\"::Cstring)") catch e e, stacktrace(catch_backtrace()) end @test err isa JuliaLowering.MacroExpansionError @test err.msg == "expected a return type annotation `::SomeType`" @test isnothing(err.err) # Check that `catch_backtrace` can capture the stacktrace of the macro function @test any(sf->sf.func===:ccall_macro_parse, st) end # Tests for interop between old and new-style macros # Hygiene interop: # call_oldstyle_macro -> oldstyle -> newstyle3 JuliaLowering.include_string(test_mod, raw""" macro call_oldstyle_macro(a) @legacy_quote_to_syntax quote x = "x in call_oldstyle_macro" @oldstyle $a x end end macro newstyle3(a, b, c) @legacy_quote_to_syntax quote x = "x in @newstyle3" ($a, $b, $c, x) end end """) # TODO: Make this macro lowering go via JuliaSyntax rather than the flisp code # (JuliaSyntax needs support for old-style quasiquote processing) Base.eval(test_mod, :( macro oldstyle(a, b) quote x = "x in @oldstyle" @newstyle3 $(esc(a)) $(esc(b)) x end end )) @test JuliaLowering.include_string(test_mod, """ let x = "x in outer scope" @call_oldstyle_macro x end """) == ("x in call_oldstyle_macro", "x in call_oldstyle_macro", "x in @oldstyle", "x in @newstyle3") # # would be ideal, but we can't get hygiene through oldstyle # ("x in outer scope", # "x in call_oldstyle_macro", # "x in @oldstyle", # "x in @newstyle3") # Old style unhygienic escaping with esc() Base.eval(test_mod, :( macro oldstyle_unhygienic() esc(:x) end )) @test JuliaLowering.include_string(test_mod, """ let x = "x in outer scope" @oldstyle_unhygienic end """) == "x in outer scope" # Exceptions in old style macros Base.eval(test_mod, :( macro oldstyle_error() error("Some error in old style macro") end )) @test try JuliaLowering.include_string(test_mod, """ @oldstyle_error """) catch exc sprint(showerror, exc) end == """ MacroExpansionError while expanding @oldstyle_error in module Main.macro_test: @oldstyle_error └─────────────┘ ── Error expanding macro Caused by: Some error in old style macro""" # Old-style macros returning non-Expr values Base.eval(test_mod, :( macro oldstyle_non_Expr() 42 end )) @test JuliaLowering.include_string(test_mod, """ @oldstyle_non_Expr """) === 42 # New-style macros called with the wrong arguments JuliaLowering.include_string(test_mod, raw""" macro method_error_test(a) end """) Base.eval(test_mod, :( macro method_error_test() end )) try JuliaLowering.include_string(test_mod, raw""" @method_error_test x y """) @test false catch exc @test exc isa JuliaLowering.MacroExpansionError mexc = exc.err @test mexc isa MethodError @test mexc.args isa Tuple{JuliaLowering.MacroContext, JuliaLowering.SyntaxTree, JuliaLowering.SyntaxTree} end @testset "calling with old/new macro signatures" begin # Old defined with 1 arg, new with 2 args, both with 3 (but with different values) Base.eval(test_mod, :(macro sig_mismatch(x); x; end)) Base.eval(test_mod, :(macro sig_mismatch(x, y, z); z; end)) JuliaLowering.include_string(test_mod, "macro sig_mismatch(x, y); x; end") JuliaLowering.include_string(test_mod, "macro sig_mismatch(x, y, z); x; end") @test JuliaLowering.include_string(test_mod, "@sig_mismatch(1)") === 1 @test JuliaLowering.include_string(test_mod, "@sig_mismatch(1, 2)") === 1 @test JuliaLowering.include_string(test_mod, "@sig_mismatch(1, 2, 3)") === 1 # 3 if we prioritize old sig err = try JuliaLowering.include_string(test_mod, "@sig_mismatch(1, 2, 3, 4)") === 1 catch exc sprint(showerror, exc, context=:module=>test_mod) end @test startswith(err, """ MacroExpansionError while expanding @sig_mismatch in module Main.macro_test: @sig_mismatch(1, 2, 3, 4) └───────────────────────┘ ── Error expanding macro Caused by: MethodError: no method matching var"@sig_mismatch"(""") end @testset "old macros producing exotic expr heads (or are otherwise complex)" for expr_compat_mode in [true, false] @test JuliaLowering.include_string(test_mod, """ let # example from @preserve docstring x = Ref{Int}(101) p = Base.unsafe_convert(Ptr{Int}, x) GC.@preserve x unsafe_load(p) end"""; expr_compat_mode) === 101 # Expr(:gc_preserve) # JuliaLowering.jl/issues/121 @test JuliaLowering.include_string(test_mod, """ GC.@preserve @static if true @__MODULE__ else end """) isa Module @test JuliaLowering.include_string(test_mod, """ GC.@preserve @static if true v"1.14" else end """; expr_compat_mode) isa VersionNumber # JuliaLowering.jl/issues/144 @test JuliaLowering.include_string(test_mod, """ f_preserve144() = let val = Any[] GC.@preserve val begin; end end f_preserve144() """; expr_compat_mode) == nothing # JuliaLowering.jl/issues/145 @test JuliaLowering.include_string(test_mod, """ f_preserve145() = let debug_buffer = IOBuffer() # inside function to force compilation GC.@preserve debug_buffer 1 end f_preserve145() """; expr_compat_mode) == 1 # only invokelatest produces :isglobal now, so MWE here Base.eval(test_mod, :(macro isglobal(x); esc(Expr(:isglobal, x)); end)) @test JuliaLowering.include_string(test_mod, """ some_global = 1 function isglobal_chk(some_arg) local some_local = 1 (@isglobal(some_undefined), @isglobal(some_global), @isglobal(some_arg), @isglobal(some_local)) end isglobal_chk(1) """; expr_compat_mode) === (true, true, false, false) # with K"Placeholder"s @test JuliaLowering.include_string(test_mod, """ __ = 1 function isglobal_chk(___) local ____ = 1 (@isglobal(_), @isglobal(__), @isglobal(___), @isglobal(____)) end isglobal_chk(1) """; expr_compat_mode) === (false, false, false, false) # @test appears to be the only macro in base to use :inert test_result = JuliaLowering.include_string(test_mod, """ using Test @test identity(123) === 123 """; expr_compat_mode) @test test_result.value === true # @enum produces Expr(:toplevel) JuliaLowering.include_string(test_mod, """ @enum SOME_ENUM X1 X2 X3 """; expr_compat_mode) Core.@latestworld @test test_mod.SOME_ENUM <: Enum @test test_mod.X1 isa Enum # @deprecate also produces Expr(:toplevel), and :public with expression # hygiene different from the contained names. @testset "@deprecate" begin @test JuliaLowering.include_string(test_mod, """ module DeprecateMod d2(x) = x+1 @deprecate d1(x) d2(0) end """; expr_compat_mode) isa Module Core.@latestworld @test isdefined(test_mod.DeprecateMod, :d2) @test isdefined(test_mod.DeprecateMod, :d1) @test Base.isexported(test_mod.DeprecateMod, :d1) @test !Base.isexported(test_mod, :d1) end # @testset produces :tryfinally with secret third arg @eval test_mod :(using Test) @test JuliaLowering.include_string(test_mod, "@test true") isa Test.Pass @testset let jltestset = JuliaLowering.include_string(test_mod, """ @testset begin @test true end """; expr_compat_mode) @test jltestset isa Test.AbstractTestSet @test jltestset.n_passed == 1 end # aliasscope @test jl_eval( test_mod, :(function simple_aliasscope(A, B) Base.Experimental.@aliasscope @inbounds for I in eachindex(A, B) A[I] = Base.Experimental.Const(B)[I] end return 0 end); expr_compat_mode) isa Function @test jl_eval( test_mod, :(let A = [1,2,3], B = [4,5,6] simple_aliasscope(A,B), A, B end); expr_compat_mode) == (0, [4,5,6], [4,5,6]) end @testset "empty meta" begin @test fl_eval(test_mod, Expr(:meta)) == nothing @test fl_eval(test_mod, Expr(:block, Expr(:meta))) == nothing @test fl_eval(test_mod, Expr(:call, Expr(:function, Expr(:call, :func_empty_meta), Expr(:block, Expr(:meta))))) == nothing @test jl_eval(test_mod, Expr(:meta)) == nothing @test jl_eval(test_mod, Expr(:block, Expr(:meta))) == nothing @test jl_eval(test_mod, Expr(:call, Expr(:function, Expr(:call, :func_empty_meta), Expr(:block, Expr(:meta))))) == nothing end @testset "macros producing meta forms" for expr_compat_mode in [true, false] function find_method_ci(thunk) ci = thunk.args[1]::Core.CodeInfo m = findfirst(ci.code) do x x isa Expr && x.head === :call && length(x.args) >= 5 && x.args[1] isa GlobalRef && x.args[1].name === :define_method end ci.code[m].args[5] end jlower_e(s) = JuliaLowering.to_lowered_expr( JuliaLowering.lower( test_mod, JuliaLowering.parsestmt( JuliaLowering.SyntaxTree, s); expr_compat_mode)) prog = "Base.@assume_effects :foldable function foo(); end" ref = Meta.lower(test_mod, Meta.parse(prog)) our = jlower_e(prog) @test find_method_ci(ref).purity === find_method_ci(our).purity prog = "Base.@inline function foo(); end" ref = Meta.lower(test_mod, Meta.parse(prog)) our = jlower_e(prog) @test find_method_ci(ref).inlining === find_method_ci(our).inlining prog = "Base.@noinline function foo(); end" ref = Meta.lower(test_mod, Meta.parse(prog)) our = jlower_e(prog) @test find_method_ci(ref).inlining === find_method_ci(our).inlining prog = "Base.@constprop :none function foo(); end" ref = Meta.lower(test_mod, Meta.parse(prog)) our = jlower_e(prog) @test find_method_ci(ref).constprop === find_method_ci(our).constprop prog = "Base.@nospecializeinfer function foo(); end" ref = Meta.lower(test_mod, Meta.parse(prog)) our = jlower_e(prog) @test find_method_ci(ref).nospecializeinfer === find_method_ci(our).nospecializeinfer prog = "Base.@propagate_inbounds function foo(); end" ref = Meta.lower(test_mod, Meta.parse(prog)) our = jlower_e(prog) @test find_method_ci(ref).propagate_inbounds === find_method_ci(our).propagate_inbounds prog = "Base.@assume_effects :total @inline function foo(); end" ref = Meta.lower(test_mod, Meta.parse(prog)) our = jlower_e(prog) @test find_method_ci(ref).inlining === find_method_ci(our).inlining @test find_method_ci(ref).purity === find_method_ci(our).purity prog = "Base.@assume_effects :consistent Base.@assume_effects :nothrow function foo(); end" ref = Meta.lower(test_mod, Meta.parse(prog)) our = jlower_e(prog) @test find_method_ci(ref).purity === find_method_ci(our).purity prog = "Base.@pure @inline foo(x) = x + 1" ref = Meta.lower(test_mod, Meta.parse(prog)) our = jlower_e(prog) @test find_method_ci(ref).purity === find_method_ci(our).purity @test find_method_ci(ref).inlining === find_method_ci(our).inlining # TODO: no api for option retrieval, just check that it compiles let options_mod = Module() @test fl_eval(options_mod, :(Base.Experimental.@optlevel 1)) == nothing @test jl_eval(options_mod, :(Base.Experimental.@optlevel 1)) == nothing @test fl_eval(options_mod, :(Base.Experimental.@max_methods 1)) == nothing @test jl_eval(options_mod, :(Base.Experimental.@max_methods 1)) == nothing end end # partially robot-generated @testset "meta-like forms not using the `meta` expression" for expr_compat_mode in (true,false) @testset "in value position" begin @test fl_eval(test_mod, Expr(:boundscheck)) isa Bool @test jl_eval(test_mod, Expr(:boundscheck); expr_compat_mode) isa Bool @test fl_eval(test_mod, Expr(:inbounds, true)) === nothing @test fl_eval(test_mod, Expr(:inbounds, false)) === nothing @test fl_eval(test_mod, Expr(:inbounds, :pop)) === nothing @test jl_eval(test_mod, Expr(:inbounds, true); expr_compat_mode) === nothing @test jl_eval(test_mod, Expr(:inbounds, false); expr_compat_mode) === nothing @test jl_eval(test_mod, Expr(:inbounds, :pop); expr_compat_mode) === nothing @testset for inline in (:inline, :noinline) @testset let ex = Expr(:block, Expr(inline, true), Expr(inline, false)) @test fl_eval(test_mod, ex) === nothing @test jl_eval(test_mod, ex; expr_compat_mode) === nothing end @testset let ex = Expr(:function, Expr(:tuple), Expr(:block, Expr(inline, true), Expr(inline, false))) local f f = fl_eval(test_mod, ex) Core.@latestworld @test f() === nothing f = jl_eval(test_mod, ex; expr_compat_mode) Core.@latestworld @test f() === nothing end end end function find_method_ci(thunk) ci = thunk.args[1]::Core.CodeInfo m = findfirst(ci.code) do x x isa Expr && x.head === :call && length(x.args) >= 5 && x.args[1] isa GlobalRef && x.args[1].name === :define_method end ci.code[m].args[5] end jlower_e(s) = JuliaLowering.to_lowered_expr( JuliaLowering.lower( test_mod, JuliaLowering.parsestmt( JuliaLowering.SyntaxTree, s); expr_compat_mode)) our_ssaflags(prog) = find_method_ci(jlower_e(prog)).ssaflags local INBOUNDS = Core.Compiler.IR_FLAG_INBOUNDS local INLINE = Core.Compiler.IR_FLAG_INLINE local NOINLINE = Core.Compiler.IR_FLAG_NOINLINE # `compute_ssaflags` shifts the encoded purity overrides up by NUM_IR_FLAGS. purity_mask(eo::Base.EffectsOverride) = UInt32(Base.encode_effects_override(eo)) << Core.Compiler.NUM_IR_FLAGS # check any IR statement in `prog` has `flags` has_any(prog, flags) = any(f -> (f & flags) == flags, our_ssaflags(prog)) has_none(prog, flags) = all(f -> (f & flags) == 0, our_ssaflags(prog)) @testset "boundscheck" begin JuliaLowering.include_string(test_mod, """ @inline function g_boundscheck(A, i) @boundscheck checkbounds(A, i) return A[i] end """; expr_compat_mode) @test test_mod.g_boundscheck(1:2, 2) == 2 @test_throws BoundsError test_mod.g_boundscheck(1:2, 3) # The boundscheck marker itself does not set IR_FLAG_INBOUNDS — it is # a separate runtime predicate, not an annotation. @test has_none("function f(A,i); @boundscheck checkbounds(A,i); A[i]; end", INBOUNDS) # `Expr(:boundscheck)` should survive lowering as a top-level # statement (it gets rewritten by inlining/codegen, not lowering). let our = find_method_ci(jlower_e( "function f(A,i); @boundscheck checkbounds(A,i); A[i]; end")) @test any(s -> s isa Expr && s.head === :boundscheck, our.code) end end @testset "inbounds" begin JuliaLowering.include_string(test_mod, """ function sum_inbounds(A::AbstractArray) r = zero(eltype(A)) for i in eachindex(A) @inbounds r += A[i] end return r end """; expr_compat_mode) @test test_mod.sum_inbounds([1,2,3]) == 6 @test has_none("function f(A,i); A[i]; end", INBOUNDS) @test has_any("function f(A,i); @inbounds A[i]; end", INBOUNDS) @test has_any(""" function f(A) s = zero(eltype(A)) @inbounds for i in eachindex(A) s += A[i] end s end """, INBOUNDS) let flags = our_ssaflags(""" function f(A, i, j) z = @inbounds A[i] A[j] end """) @test any(f -> (f & INBOUNDS) != 0, flags) # inside @inbounds @test any(f -> (f & INBOUNDS) == 0, flags) # outside end end @testset "inline" begin @test has_any("function f(g,x); @inline g(x); end", INLINE) @test has_none("function f(g,x); g(x); end", INLINE) @test has_none("function f(g,x); @inline g(x); end", NOINLINE) @test has_any("function f(g,x); @inline g(x) + g(x); end", INLINE) # Bare `@inline` inside a function body (1.8+) emits # `Expr(:meta, :inline)`; no statement gets a call-site IR_FLAG_INLINE. JuliaLowering.include_string(test_mod, """ function bare_inline(x) @inline x * 2 end """; expr_compat_mode) @test test_mod.bare_inline(3) == 6 @test has_none("function f(x); @inline; x * 2; end", INLINE) # `@inline` on a definition is handled by the meta-expression path # (covered in "macros producing meta forms"); confirm it still runs # and that no call-site INLINE bit leaks into the body. JuliaLowering.include_string(test_mod, """ @inline f_inline_def(x) = x + 1 """; expr_compat_mode) @test test_mod.f_inline_def(2) == 3 @test has_none("@inline f(x) = x + 1", INLINE) end @testset "noinline" begin # Analogous to `@inline` but pushes IR_FLAG_NOINLINE. @test has_any("function f(g,x); @noinline g(x); end", NOINLINE) @test has_none("function f(g,x); g(x); end", NOINLINE) @test has_none("function f(g,x); @noinline g(x); end", INLINE) @test has_any("function f(g,x); @noinline g(x) + g(x); end", NOINLINE) JuliaLowering.include_string(test_mod, """ function bare_noinline(x) @noinline x * 2 end """; expr_compat_mode) @test test_mod.bare_noinline(3) == 6 @test has_none("function f(x); @noinline; x * 2; end", NOINLINE) JuliaLowering.include_string(test_mod, """ @noinline f_noinline_def(x) = x + 1 """; expr_compat_mode) @test test_mod.f_noinline_def(2) == 3 # Innermost annotation wins when @inline / @noinline nest: the inner # call gets IR_FLAG_INLINE; the outer @noinline still applies to # statements outside the inner region. let flags = our_ssaflags(""" function f(g, x) @noinline let a = @inline g(x) b = g(x) (a, b) end end """) @test any(f -> (f & INLINE) != 0, flags) @test any(f -> (f & NOINLINE) != 0, flags) end end @testset "purity" begin # Sanity: plain function with no purity annotation has no purity bits set. @test has_none("function f(g,x); g(x); end", UInt32(0xFFFF) << Core.Compiler.NUM_IR_FLAGS) # `@assume_effects :foo expr` at a call site expands to # (block (purity ...11 bool args...) (local (= val expr)) (purity) val) # where the trailing zero-arg `(purity)` is the region-end token. @test has_any("function f(g,x); Base.@assume_effects :nothrow g(x); end", purity_mask(Base.EffectsOverride(nothrow=true))) # Multiple atomic settings combine to set both bits at once. @test has_any( "function f(g,x); Base.@assume_effects :consistent :effect_free g(x); end", purity_mask(Base.EffectsOverride(consistent=true, effect_free=true))) # Function form goes through a different path: `(meta (purity args...))` JuliaLowering.include_string(test_mod, """ Base.@assume_effects :total f_assume_def(x) = x """; expr_compat_mode) @test test_mod.f_assume_def(5) == 5 prog_def = "Base.@assume_effects :total function f_assume_total(x); x; end" ref_ci = find_method_ci(Meta.lower(test_mod, Meta.parse(prog_def))) our_ci = find_method_ci(jlower_e(prog_def)) @test ref_ci.purity === our_ci.purity end end @testset "scope layers for normally-inert ASTs" begin # Right hand side of `.` @test JuliaLowering.include_string(test_mod, raw""" let x = @legacy_quote_to_syntax :(hi) @legacy_quote_to_syntax :(A.$x) end """) ≈ @ast_ [K"." "A"::K"Identifier" [K"inert" "hi"::K"Identifier"] ] # module @test JuliaLowering.include_string(test_mod, raw""" let x = @legacy_quote_to_syntax :(AA) @legacy_quote_to_syntax :(module $x end) end """) ≈ @ast_ [K"module" v"1.14.0"::K"Value" true::K"Value" "AA"::K"Identifier" [K"block"] ] # In macro expansion, require that expressions passed in as macro # *arguments* get the lexical scope of the calling context, even for the # `x` in `M.$x` where the right hand side of `.` is normally quoted. @test JuliaLowering.include_string(test_mod, raw""" let x = @legacy_quote_to_syntax :(someglobal) @eval M.$x end """; expr_compat_mode=false) == "global in module M" @test JuliaLowering.include_string(test_mod, raw""" let x = @legacy_quote_to_syntax :(someglobal) @eval M.$x end """; expr_compat_mode=true) == "global in module M" # @eval quoting should embed the value, not the syntax @test JuliaLowering.include_string(test_mod, raw""" let some_local = 101 @eval module AA x = $some_local end end """; expr_compat_mode=false) isa Module @test test_mod.AA.x == 101 @test JuliaLowering.include_string(test_mod, raw""" let some_local = 101 @eval module AA x = $some_local end end """; expr_compat_mode=true) isa Module @test test_mod.AA.x == 101 # "Deferred hygiene" in macros which emit quoted code. OK to break # # The old macro system doesn't handle this - here's the equivalent # implementation # macro make_quoted_code(init, y) # QuoteNode(:(let # x = "inner x" # $(esc(init)) # ($(esc(y)), x) # end)) # end JuliaLowering.include_string(test_mod, raw""" macro make_quoted_code(init, y) q = @legacy_quote_to_syntax :(let x = "inner x" $init ($y, x) end) @ast _ q [K"syntaxinert" q] end """) code = JuliaLowering.include_string(test_mod, """@make_quoted_code(x="outer x", x)""") @test JuliaLowering.eval(test_mod, code) == ("outer x", "inner x") end @testset "toplevel macro hygiene" for run in [JuliaLowering.include_string, Base.include_string] @eval test_mod global mod = $test_mod @eval test_mod module MacroMod global mod = MacroMod macro escaped_toplevel() esc(Expr(:toplevel, :(mod))) end macro inner_escaped_toplevel() Expr(:toplevel, esc(:(mod))) end macro unescaped_toplevel() Expr(:toplevel, :(mod)) end end Core.@latestworld @test run(test_mod, "MacroMod.@escaped_toplevel") === test_mod @test run(test_mod, "MacroMod.@inner_escaped_toplevel") === test_mod @test run(test_mod, "MacroMod.@unescaped_toplevel") === test_mod.MacroMod unrelated = @newmod(unrelated) @eval unrelated const MacroMod = $(test_mod.MacroMod) @eval unrelated global mod = 123 @test run(unrelated, "MacroMod.@escaped_toplevel") == 123 @test run(unrelated, "MacroMod.@inner_escaped_toplevel") == 123 @test run(unrelated, "MacroMod.@unescaped_toplevel") === test_mod.MacroMod end @testset "toplevel macro hygiene: @__MODULE__" for run in [JuliaLowering.include_string, Base.include_string] @eval test_mod module MacroMod macro atmodule_in_toplevel() Expr(:toplevel, :(@__MODULE__)) end macro atmodule_in_module() Expr(:toplevel, Expr(:module, true, esc(:atmod_mod), Expr( :block, :(global global_mod = @__MODULE__)))) end end Core.@latestworld @test run(test_mod, "MacroMod.@atmodule_in_toplevel") === test_mod @test run(test_mod, "MacroMod.@atmodule_in_module") isa Module Core.@latestworld @test isdefined(test_mod, :atmod_mod) @test test_mod.atmod_mod.global_mod == test_mod.atmod_mod end # JuliaLang/JuliaLowering.jl#120 # # `__module__` should be expanded as the lexical module containing the expanded # code, not the module corresponding to the current hygienic scope JuliaLowering.include_string(test_mod, raw""" module Mod1 import ..JuliaLowering.@legacy_quote_to_syntax macro indirect_MODULE() return @legacy_quote_to_syntax :(@__MODULE__()) end end """) code = JuliaLowering.include_string(test_mod, """Mod1.@indirect_MODULE()""") @test JuliaLowering.eval(test_mod, code) === test_mod # !== test_mod.Mod1 # the lowering/eval iterator needs to expand in the correct world age (currently # the only way to hit this from user code is macros producing toplevel) @testset "old macros defining modules" begin # escaped module nested in tmpmod_1 jl_eval(test_mod, :( module MacMod macro makemod(name) Expr(:toplevel, esc(Expr(:module, false, :tmpmod_1, Expr(:block, Expr(:module, false, name, Expr(:block, Expr(:const, Expr(:(=), :c, 1)))))))) end end); expr_compat_mode=true) @testset for expr_compat_mode in [true, false] @test JuliaLowering.include_string( test_mod, "MacMod.@makemod(newmod)") isa Module Core.@latestworld # module name should escape macmod->test_mod @test test_mod.tmpmod_1.newmod isa Module @test !isdefined(test_mod.MacMod, :newmod) @test !isdefined(test_mod.MacMod, :tmpmod_1) # const in mod body should work @test test_mod.tmpmod_1.newmod.c == 1 end # escaped module name jl_eval(test_mod, :( module MacMod macro makemod(name) Expr(:toplevel, Expr(:module, false, esc(name), Expr(:block, # TODO: escape node in outer context # Expr(:const, Expr(:(=), esc(:c), 1)) ))) end end); expr_compat_mode=true) @testset for expr_compat_mode in [true, false] @test JuliaLowering.include_string( test_mod, "MacMod.@makemod(newmod)") isa Module Core.@latestworld # module name should escape macmod->test_mod @test test_mod.newmod isa Module @test !isdefined(test_mod.MacMod, :newmod) # const in mod body should @test_broken test_mod.newmod.c == 1 end end @testset "(AI) old macro attribution survives a nested eval in its body (#32)" begin Base.eval(test_mod, :(module MacDefMod const secret = 99 macro getsecret() __module__.eval(:(nested_eval_side_effect = 1 + 1)) return :(secret) # bare name -> resolves in the defining module end end)) Core.@latestworld # `secret` must resolve in MacDefMod (== mod_for_ast), matching flisp. @test JuliaLowering.include_string(test_mod, "MacDefMod.@getsecret()") == 99 @test test_mod.nested_eval_side_effect == 2 @test fl_eval(test_mod, :(MacDefMod.@getsecret())) == 99 end @testset "macros defining macros" begin @eval test_mod macro make_and_use_macro_toplevel() Expr(:toplevel, esc(:(macro from_toplevel_expansion() :(123) end)), esc(:(@from_toplevel_expansion()))) end @test JuliaLowering.include_string( test_mod, "@make_and_use_macro_toplevel()"; expr_compat_mode=true) === 123 if isdefined(test_mod, Symbol("@from_toplevel_expansion")) Base.delete_binding(test_mod, Symbol("@from_toplevel_expansion")) end @test JuliaLowering.include_string( test_mod, "@make_and_use_macro_toplevel()"; expr_compat_mode=false) === 123 end @testset "SIMD loopinfo" begin @test JuliaLowering.include_string(test_mod, raw""" @eval let n = 10 x = zeros(n) i = 1 while i ≤ n x[i] += 1 i += 1 $(Expr(:loopinfo, Symbol("julia.simdloop"), nothing)) # Mark loop as SIMD loop end sum(x) end """; expr_compat_mode=true) == 10.0 @test JuliaLowering.include_string(test_mod, raw""" @eval let n = 10 x = zeros(n) i = 1 while i ≤ n x[i] += 1 i += 1 $(Expr(:loopinfo, Symbol("julia.simdloop"), Symbol("julia.ivdep"))) # Mark loop as SIMD loop end sum(x) end """; expr_compat_mode=true) == 10.0 JuliaLowering.include_string(test_mod, """ @noinline function inner(x, y) s = zero(eltype(x)) for i in eachindex(x, y) @inbounds s += x[i]*y[i] end return s end """) JuliaLowering.include_string(test_mod, """ @noinline function innersimd(x, y) s = zero(eltype(x)) @simd for i in eachindex(x, y) @inbounds s += x[i] * y[i] end return s end """) @test test_mod.inner([1,2,3], [1,2,3]) == 14 @test test_mod.innersimd([1,2,3], [1,2,3]) == 14 end @testset "@__FUNCTION__ and Expr(:thisfunction)" begin @testset "Basic usage" begin # @__FUNCTION__ in regular functions JuliaLowering.include_string(test_mod, raw""" test_function_basic() = @__FUNCTION__ """; expr_compat_mode=true) @test test_mod.test_function_basic() === test_mod.test_function_basic # Expr(:thisfunction) in regular functions JuliaLowering.include_string(test_mod, raw""" @eval regular_func() = @__FUNCTION__ """; expr_compat_mode=true) @test test_mod.regular_func() === test_mod.regular_func end @testset "Recursion" begin # Factorial with @__FUNCTION__ JuliaLowering.include_string(test_mod, raw""" factorial_function(n) = n <= 1 ? 1 : n * (@__FUNCTION__)(n - 1) """; expr_compat_mode=true) @test test_mod.factorial_function(5) == 120 # Fibonacci with Expr(:thisfunction) JuliaLowering.include_string(test_mod, raw""" struct RecursiveCallableStruct; end (::RecursiveCallableStruct)(n) = n <= 1 ? n : @__FUNCTION__()(n-1) + @__FUNCTION__()(n-2) """; expr_compat_mode=true) @test test_mod.RecursiveCallableStruct()(10) === 55 # Anonymous function recursion @test JuliaLowering.include_string(test_mod, raw""" (n -> n <= 1 ? 1 : n * (@__FUNCTION__)(n - 1))(5) """; expr_compat_mode=true) == 120 end @testset "Closures and nested functions" begin # Prevents boxed closures JuliaLowering.include_string(test_mod, raw""" function make_closure() fib(n) = n <= 1 ? 1 : (@__FUNCTION__)(n - 1) + (@__FUNCTION__)(n - 2) return fib end """; expr_compat_mode=true) Test.@inferred test_mod.make_closure() closure = test_mod.make_closure() @test closure(5) == 8 Test.@inferred closure(5) # Complex closure of closures JuliaLowering.include_string(test_mod, raw""" function f1() function f2() function f3() return @__FUNCTION__ end return (@__FUNCTION__), f3() end return (@__FUNCTION__), f2()... end """; expr_compat_mode=true) Test.@inferred test_mod.f1() @test test_mod.f1()[1] === test_mod.f1 @test test_mod.f1()[2] !== test_mod.f1 @test test_mod.f1()[3] !== test_mod.f1 @test test_mod.f1()[3]() === test_mod.f1()[3] @test test_mod.f1()[2]()[2]() === test_mod.f1()[3] end @testset "Do blocks" begin function test_do_block() result = JuliaLowering.include_string(test_mod, raw""" map([1, 2, 3]) do x return (@__FUNCTION__, x) end """; expr_compat_mode=true) # All should refer to the same do-block function @test all(r -> r[1] === result[1][1], result) # Values should be different @test [r[2] for r in result] == [1, 2, 3] # It should be different than `test_do_block` @test result[1][1] !== test_do_block end test_do_block() end @testset "Keyword arguments" begin # @__FUNCTION__ with kwargs JuliaLowering.include_string(test_mod, raw""" f_thisfunction_kw(; n) = n <= 1 ? 1 : n * (@__FUNCTION__)(; n = n - 1) """; expr_compat_mode=true) @test test_mod.f_thisfunction_kw(n = 5) == 120 # Expr(:thisfunction) with kwargs JuliaLowering.include_string(test_mod, raw""" f_thisfunction_kw2(; n=1) = n <= 1 ? n : n * @__FUNCTION__()(; n=n-1) """; expr_compat_mode=true) result = test_mod.f_thisfunction_kw2(n=5) @test result == 120 end @testset "Callable structs" begin # @__FUNCTION__ in callable structs JuliaLowering.include_string(test_mod, raw""" module A struct CallableStruct{T}; val::T; end (c::CallableStruct)() = @__FUNCTION__ end """; expr_compat_mode=true) JuliaLowering.include_string(test_mod, raw""" using .A: CallableStruct """; expr_compat_mode=true) c = test_mod.CallableStruct(5) @test c() === c # In closures, var"#self#" should refer to the enclosing function, # NOT the enclosing struct instance JuliaLowering.include_string(test_mod, raw""" struct CallableStruct2; end @eval function (obj::CallableStruct2)() function inner_func() @__FUNCTION__ end inner_func end """; expr_compat_mode=true) let cs = test_mod.CallableStruct2() @test cs()() === cs() @test cs()() !== cs end # Accessing values via self-reference JuliaLowering.include_string(test_mod, raw""" struct CallableStruct3 value::Int end (obj::CallableStruct3)() = @__FUNCTION__() (obj::CallableStruct3)(x) = @__FUNCTION__().value + x """; expr_compat_mode=true) let cs = test_mod.CallableStruct3(42) @test cs() === cs @test cs(10) === 52 end # Callable struct with args and kwargs JuliaLowering.include_string(test_mod, raw""" struct CallableStruct4 end @eval function (obj::CallableStruct4)(x, args...; y=2, kws...) return (; func=(@__FUNCTION__), x, args, y, kws) end """; expr_compat_mode=true) c = test_mod.CallableStruct4() @test c(1).func === c @test c(2, 3).args == (3,) @test c(2; y=4).y == 4 @test c(2; y=4, a=5, b=6, c=7).kws[:c] == 7 end @testset "Special cases" begin # Generated functions JuliaLowering.include_string(test_mod, raw""" let @generated foo2() = @__FUNCTION__ foo2() === foo2 end """; expr_compat_mode=true) # Struct constructors let JuliaLowering.include_string(test_mod, raw""" struct Cols{T<:Tuple} cols::T operator Cols(args...; operator=union) = (new{typeof(args)}(args, operator); string(@__FUNCTION__)) end """; expr_compat_mode=true) result = @invokelatest test_mod.Cols(1, 2, 3) @test occursin("Cols", result) end # Should not access arg-map for local variables # TODO: worth the special case? JuliaLowering.include_string(test_mod, raw""" function f_thisfunction_argmap end function (f_thisfunction_argmap::typeof(f_thisfunction_argmap))() f_thisfunction_argmap = 1 @__FUNCTION__ end """; expr_compat_mode=true) @test_broken test_mod.f_thisfunction_argmap() === test_mod.f_thisfunction_argmap end @test JuliaLowering.include_string(test_mod, """ @eval let f=[ ()->$(Expr(:thisfunction)) for i = 1:1 ][1]; f() === f; end """; expr_compat_mode=true) end @testset "macro source LineNumberNode" begin Base.include_string(test_mod, raw""" macro srcfile() string(__source__.file) end """) mac_ex = Expr(:macrocall, Symbol("@srcfile"), LineNumberNode(1, "goodfile")) mac_st = JuliaLowering.expr_to_est(mac_ex, LineNumberNode(1, "badfile")) @test JuliaLowering.eval(test_mod, mac_st) === "goodfile" # tolerate nothing mac_ex = Expr(:macrocall, Symbol("@srcfile"), nothing) mac_st = JuliaLowering.expr_to_est(mac_ex, LineNumberNode(1, "badfile")) @test JuliaLowering.eval(test_mod, mac_st) == "none" end @testset "macro QuoteNode + inert behavior" begin Base.include_string(test_mod, raw""" macro quoted_gr() QuoteNode(GlobalRef(Base, :dontresolveme)) end """) let gr = JuliaLowering.include_string(test_mod, "@quoted_gr") @test gr.mod === Base @test gr.name === :dontresolveme end end @testset "Base macros" begin jl_eval(test_mod, :(function test_invokelatest() @eval invokelatest_target(x, y) = x + y out = @invokelatest(invokelatest_target(1, 2)) Base.delete_binding(@__MODULE__, :invokelatest_target) out end)) # the following test needs to define this to be effective @test_throws UndefVarError JuliaLowering.include_string(test_mod, "invokelatest_target(1,2)") @test JuliaLowering.include_string(test_mod, "test_invokelatest()") === 3 for expr_compat_mode in (false, true), version in (v"1.13", v"1.14") _version = JuliaLowering.include_string(test_mod, "Base.Experimental.@VERSION"; expr_compat_mode, version ) @test _version isa NamedTuple @test _version.syntax == version end end # produces import/using in module that is `@eval`ed. @testset "safetestset" begin macro_mod = @newmod(macro_mod, test_mod) JuliaLowering.include_string(macro_mod, raw""" macro safetestset(testname, expr) quote @eval module $(gensym("safetestset_mod")) using Test @testset $testname $expr end nothing end end """; expr_compat_mode=true) JuliaLowering.include_string(test_mod, """ macro_mod.@safetestset "Tests" begin a = 1; b = 2; c = a + b; @test c == 3 @isdefined(a) == true end """; expr_compat_mode=true) @test !isdefined(test_mod, :a) @test !isdefined(macro_mod, :a) end