/
githubmirror
/
julia
Обзор
Документация
Войти
/
githubmirror
/
julia
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
JuliaLowering/test/utils.jl
420 строк
13 KB
Em Chu
[JuliaLowering] Make `SyntaxTree` a standard tree (#62474)
29 июл 2026, 15:34
Не верифицирован
29 июл 2026, 15:34
92cca2d
Код
Авторство
О чём код?
# Shared testing code which should be included before running individual test files. using Test using JuliaLowering using JuliaSyntax const JS = JuliaSyntax const JL = JuliaLowering import FileWatching # The following are for docstrings testing. We need to load the REPL module # here for `Base.@doc` lookup to work at all. Yes this does seem really, # really, REALLY messed up. using Markdown import REPL using .JuliaSyntax: SourceAttrType, sourcetext, SyntaxList using .JuliaLowering: @ast, Bindings, Kind, LoweringError, MacroExpansionError, ScopeLayer, SourceRef, SyntaxTree, children, flattened_provenance, is_leaf, mapchildren, numchildren, showprov, syntax_name, syntax_id function _source_node(src) SyntaxTree(K"TOMBSTONE", nothing, nothing, src, nothing) end macro ast_(tree) quote srcref = _source_node($(QuoteNode(__source__))) @ast _ srcref $tree end end #------------------------------------------------------------------------------- function _format_as_ast_macro(io, ex, indent) k = kind(ex) kind_str = repr(k) if !is_leaf(ex) println(io, indent, "[", kind_str) ind2 = indent*" " for c in children(ex) _format_as_ast_macro(io, c, ind2) end println(io, indent, "]") else val_str = if k == K"Identifier" || k == K"core" || k == K"top" repr(syntax_name(ex)) elseif k == K"BindingId" repr(syntax_id(ex)) else repr(get(ex, :value, nothing)) end println(io, indent, val_str, "::", kind_str) end end function format_as_ast_macro(io::IO, ex) print(io, "@ast_ ") _format_as_ast_macro(io, ex, "") end """ format_as_ast_macro(ex) Format AST `ex` as a Julia source code call to the `@ast_` macro for generating test case comparisons with the `≈` function. """ format_as_ast_macro(ex) = format_as_ast_macro(stdout, ex) #------------------------------------------------------------------------------- # Test tools function uncomment_description(desc) replace(desc, r"^# ?"m=>"") end function comment_description(desc) lines = replace(split(desc, '\n')) do line strip("# " * line) end join(lines, '\n') end function match_ir_test_case(case_str) m = match(r"(^#(?:.|\n)*?)^([^#](?:.|\n)*)"m, strip(case_str)) if isnothing(m) error("Malformatted IR test case:\n$(repr(case_str))") end description = uncomment_description(m[1]) inout = split(m[2], r"#----*") input, output = length(inout) == 2 ? inout : length(inout) == 1 ? (inout[1], "") : error("Too many sections in IR test case") expect_error = startswith(description, "Error") is_broken = startswith(description, "FIXME") method_filter = begin mf = match(r"\[method_filter: *(.*)\]", description) isnothing(mf) ? nothing : strip(mf[1]) end (; expect_error=expect_error, is_broken=is_broken, description=strip(description), method_filter=method_filter, input=strip(input), output=strip(output)) end function read_ir_test_cases(filename) str = read(filename, String) parts = split(str, r"#\*+") if length(parts) == 2 preamble_str = strip(parts[1]) cases_str = parts[2] else preamble_str = "" cases_str = only(parts) end (preamble_str, [match_ir_test_case(s) for s in split(cases_str, r"######*") if strip(s) != ""]) end function setup_ir_test_module(preamble) test_mod = Module(:TestMod) Base.eval(test_mod, :(const JuliaLowering = $JuliaLowering)) Base.eval(test_mod, :(const var"@ast_" = $(var"@ast_"))) JuliaLowering.include_string(test_mod, preamble) test_mod end function format_ir_for_test(mod, case) @assert !case.is_broken ex = parsestmt(SyntaxTree, case.input) try if (kind(ex) == K"macrocall" && kind(ex[1]) == K"Identifier" && syntax_name(ex[1]) == "@ast_") # Total hack, until @ast_ can be implemented in terms of new-style # macros. ex = Base.eval(mod, JuliaLowering.est_to_expr(ex)) end x = JuliaLowering.lower(mod, ex) if case.expect_error error("Expected a lowering error in test case \"$(case.description)\"") end ir = strip(sprint(JuliaLowering.print_ir, x, case.method_filter)) return replace(ir, string(mod)=>"TestMod") catch exc if exc isa InterruptException rethrow() elseif case.expect_error && (exc isa LoweringError) return sprint(io->Base.showerror(io, exc, show_detail=false)) elseif case.expect_error && (exc isa MacroExpansionError) return sprint(io->Base.showerror(io, exc)) else throw("Error in test case \"$(case.description)\"") end end end function test_ir_cases(filename::AbstractString) preamble, cases = read_ir_test_cases(filename) test_mod = setup_ir_test_module(preamble) for case in cases if case.is_broken continue end output = format_ir_for_test(test_mod, case) @testset "$(case.description)" begin if output != case.output # Do additional error dumping, as @test will not format errors in a nice way @error "Test \"$(case.description)\" failed" output=Text(output) ref=Text(case.output) end @test output == case.output end end end """ Update all IR test cases in `filename` when the IR format has changed. When `pattern` is supplied, update only those tests where `occursin(pattern, description)` is true. """ function refresh_ir_test_cases(filename, pattern=nothing) preamble, cases = read_ir_test_cases(filename) test_mod = setup_ir_test_module(preamble) io = IOBuffer() if !isempty(preamble) println(io, preamble, "\n") println(io, "#*******************************************************************************") end for case in cases if !case.is_broken && (isnothing(pattern) || occursin(pattern, case.description)) ir = format_ir_for_test(test_mod, case) if rstrip(ir) != case.output @info "Refreshing test case $(repr(case.description)) in $filename" end else ir = case.output end (case == cases[end] ? print : println)(io, """ ######################################## $(comment_description(case.description)) $(strip(case.input)) #--------------------- $ir """ ) end # Write only at the end to ensure we don't write rubbish if we crash! write(filename, take!(io)) nothing end function refresh_all_ir_test_cases(test_dir=".") foreach(refresh_ir_test_cases, filter(fn->endswith(fn, "ir.jl"), readdir(test_dir, join=true))) end function watch_ir_tests(dir, delay=0.5) dir = abspath(dir) while true (name, event) = FileWatching.watch_folder(dir) if endswith(name, "_ir.jl") && (event.changed || event.renamed) FileWatching.unwatch_folder(dir) sleep(delay) try refresh_ir_test_cases(joinpath(dir, name)) catch @error "Error refreshing test case" exception=current_exceptions() end end end end function lower_str(mod::Module, s::AbstractString) ex = parsestmt(JuliaLowering.SyntaxTree, s) return JuliaLowering.to_lowered_expr(JuliaLowering.lower(mod, ex)) end # See Julia Base tests in "test/docs.jl" function docstrings_equal(d1, d2; debug=true) io1 = IOBuffer() io2 = IOBuffer() show(io1, MIME"text/markdown"(), d1) show(io2, MIME"text/markdown"(), d2) s1 = String(take!(io1)) s2 = String(take!(io2)) if debug && s1 != s2 print(s1) println("--------------------------------------------------------------------------------") print(s2) println("================================================================================") end return s1 == s2 end docstrings_equal(d1::Docs.DocStr, d2) = docstrings_equal(Docs.parsedoc(d1), d2) #------------------------------------------------------------------------------- # Tools for test case reduction function block_reduction_1(is_lowering_error::Function, orig_ex::ST, ex::ST, curr_path = Int[]) where {ST <: SyntaxTree} if !is_leaf(ex) if kind(ex) == K"block" for i in 1:numchildren(ex) trial_ex = delete_block_child(orig_ex, orig_ex, curr_path, i) if is_lowering_error(trial_ex) # @info "Reduced expression" curr_path i return trial_ex end end end for (i,e) in enumerate(children(ex)) push!(curr_path, i) res = block_reduction_1(is_lowering_error, orig_ex, e, curr_path) if !isnothing(res) return res end pop!(curr_path) end end return nothing end # Find children of all `K"block"`s in an expression and try deleting them while # preserving the invariant `is_lowering_error(reduced) == true`. function block_reduction(is_lowering_error, ex) reduced = ex was_reduced = false while true r = block_reduction_1(is_lowering_error, reduced, reduced) if isnothing(r) return (reduced, was_reduced) end reduced = r was_reduced = true end end function delete_block_child(ctx, ex, block_path, child_idx, depth=1) if depth > length(block_path) cs = copy(children(ex)) deleteat!(cs, child_idx) @ast ctx ex [ex cs...] else j = block_path[depth] mapchildren(ctx, ex, j:j) do e delete_block_child(ctx, e, block_path, child_idx, depth+1) end end end function throws_lowering_exc(mod, ex) try debug_lower(mod, ex) return false catch exc if exc isa LoweringError return true else rethrow() end end end # Parse a file and lower the top level expression one child at a time, finding # any top level statement that fails lowering and producing a partially reduced # test case. function reduce_any_failing_toplevel(mod::Module, filename::AbstractString; do_eval::Bool=false) text = read(filename, String) ex0 = parseall(SyntaxTree, text; filename) for ex in children(ex0) try ex_compiled = JuliaLowering.lower(mod, ex) ex_expr = JuliaLowering.to_lowered_expr(ex_compiled) if do_eval Base.eval(mod, ex_expr) end catch exc @error "Failure lowering code" ex if !(exc isa LoweringError) rethrow() end (reduced,was_reduced) = block_reduction(e->throws_lowering_exc(mod,e), ex) if !was_reduced @info "No reduction possible" return ex else @info "Reduced code" reduced return reduced end end end nothing end function expr_structure_eq(e1,e2) @nospecialize typeof(e1) == typeof(e2) || return false e1 isa Expr || return e1 == e2 e1.head === e2.head || return false length(e1.args) == length(e2.args) || return false for (a, b) in Iterators.zip(e1.args, e2.args) expr_structure_eq(a,b) || return false end true end macro newmod(name="newmod_$(string(__source__))", parentmod=__module__, body...) mod_ex = :( module $(Symbol(name)) const JuliaLowering = $(JuliaLowering) const JuliaSyntax = $(JuliaSyntax) const var"@K_str" = JuliaSyntax.var"@K_str" const var"@legacy_quote_to_syntax" = JuliaLowering.var"@legacy_quote_to_syntax" $(body...) end) Expr(:block, :(mod = $(Expr(:escape, :(Core.eval($parentmod, $(QuoteNode(mod_ex))))))), Expr(Symbol("latestworld-if-toplevel")), :mod) end function fl_macroexpand(mod::Module, x::Expr) ccall(:jl_macroexpand, Any, (Any, Any, Cint, Cint, Cint), x, mod, true, false, true) end function fl_lower(mod::Module, x::Expr) Base.fl_lower(x, mod, @__FILE__, @__LINE__, Base.get_world_counter())[1] end function fl_eval(mod::Module, x::Expr) Core.eval(mod, fl_lower(mod, x)) end function jl_macroexpand(mod::Module, x::SyntaxTree; expr_compat_mode=false) JuliaLowering.macroexpand(mod, x; expr_compat_mode) end function jl_lower(mod::Module, st::SyntaxTree; expr_compat_mode=false) JuliaLowering.lower(mod, st; expr_compat_mode) end function jl_eval(mod::Module, st::SyntaxTree; expr_compat_mode=false) JuliaLowering.eval(mod, st; expr_compat_mode) end fl_macroexpand(mod::Module, st::SyntaxTree; kws...) = fl_macroexpand(mod, JuliaLowering.est_to_expr(st); kws...) fl_lower(mod::Module, st::SyntaxTree; kws...) = fl_lower(mod, JuliaLowering.est_to_expr(st); kws...) fl_eval(mod::Module, st::SyntaxTree; kws...) = fl_eval(mod, JuliaLowering.est_to_expr(st); kws...) jl_macroexpand(mod::Module, ex::Expr; kws...) = jl_macroexpand(mod, JuliaLowering.expr_to_est(ex); kws...) jl_lower(mod::Module, ex::Expr; kws...) = jl_lower(mod, JuliaLowering.expr_to_est(ex); kws...) jl_eval(mod::Module, ex::Expr; kws...) = jl_eval(mod, JuliaLowering.expr_to_est(ex); kws...)