diff --git a/src/ast.jl b/src/ast.jl index 26fcddd..10852db 100644 --- a/src/ast.jl +++ b/src/ast.jl @@ -20,7 +20,7 @@ function _makenode(graph::SyntaxGraph, srcref, head, children; attrs...) return SyntaxTree(graph, id) end -function makenode(ctx, srcref, head, children::SyntaxTree...; attrs...) +function makenode(ctx, srcref, head, children::Union{Nothing,SyntaxTree}...; attrs...) _makenode(syntax_graph(ctx), srcref, head, _node_ids(children...); attrs...) end @@ -53,6 +53,13 @@ function makeleaf(ctx, srcref, kind) _makenode(syntax_graph(ctx), srcref, kind, nothing) end +function _match_srcref(ex) + if Meta.isexpr(ex, :macrocall) && ex.args[1] == Symbol("@HERE") + QuoteNode(ex.args[2]) + else + esc(ex) + end +end function _match_kind_ex(defs, srcref, ex) kws = [] @@ -68,12 +75,7 @@ function _match_kind_ex(defs, srcref, ex) end if length(args) == 1 srcref = Symbol("srcref_$(length(defs))") - ref_ex = if Meta.isexpr(args[1], :macrocall) && args[1].args[1] == Symbol("@HERE") - QuoteNode(args[1].args[2]) - else - esc(args[1]) - end - push!(defs, :($srcref = $ref_ex)) + push!(defs, :($srcref = $(_match_srcref(args[1])))) elseif length(args) > 1 error("Unexpected: extra srcref argument in `$ex`?") end @@ -138,7 +140,7 @@ Any `kind` can be replaced with an expression of the form * `kind(attr=val)` - set an additional attribute * `kind(srcref; attr₁=val₁, attr₂=val₂)` - the general form -In any place `srcref` is used, the special form `@HERE` can be used to instead +In any place `srcref` is used, the special form `@HERE()` can be used to instead to indicate that the "primary" location of the source is the location where `@HERE` occurs. @@ -170,7 +172,7 @@ to indicate that the "primary" location of the source is the location where macro ast(ctx, srcref, tree) defs = [] push!(defs, :(ctx = $(esc(ctx)))) - push!(defs, :(srcref = $(esc(srcref)))) + push!(defs, :(srcref = $(_match_srcref(srcref)))) ex = _expand_ast_tree(defs, :ctx, :srcref, tree) quote $(defs...) diff --git a/src/desugaring.jl b/src/desugaring.jl index d5f4694..bc8d517 100644 --- a/src/desugaring.jl +++ b/src/desugaring.jl @@ -18,18 +18,19 @@ end struct DesugaringContext{GraphType} <: AbstractLoweringContext graph::GraphType next_var_id::Ref{VarId} + mod::Module end -function DesugaringContext(ctx) +function DesugaringContext(ctx, mod) graph = syntax_graph(ctx) ensure_attributes!(graph, kind=Kind, syntax_flags=UInt16, green_tree=GreenNode, - source_pos=Int, source=Union{SourceRef,NodeId}, + source_pos=Int, source=SourceAttrType, value=Any, name_val=String, scope_type=Symbol, # :hard or :soft var_id=VarId, lambda_info=LambdaInfo) - DesugaringContext(freeze_attrs(graph), Ref{VarId}(1)) + DesugaringContext(freeze_attrs(graph), Ref{VarId}(1), mod) end #------------------------------------------------------------------------------- @@ -382,6 +383,154 @@ function expand_function_def(ctx, ex) end end +function _append_importpath(ctx, path_spec, path) + prev_was_dot = true + for component in children(path) + k = kind(component) + if k == K"quote" + # Permit quoted path components as in + # import A.(:b).:c + component = component[1] + end + @chk kind(component) in (K"Identifier", K".") + name = component.name_val + is_dot = kind(component) == K"." + if is_dot && !prev_was_dot + throw(LoweringError(component, "invalid import path: `.` in identifier path")) + end + prev_was_dot = is_dot + push!(path_spec, @ast(ctx, component, name::K"String")) + end + path_spec +end + +function expand_import(ctx, ex) + is_using = kind(ex) == K"using" + if kind(ex[1]) == K":" + # import M: x.y as z, w + # (import (: (importpath M) (as (importpath x y) z) (importpath w))) + # => + # (call module_import + # false + # (call core.svec "M") + # (call core.svec 2 "x" "y" "z" 1 "w" "w")) + @chk numchildren(ex[1]) >= 2 + from = ex[1][1] + @chk kind(from) == K"importpath" + from_path = @ast ctx from [K"call" + "svec"::K"core" + _append_importpath(ctx, SyntaxList(ctx), from)... + ] + paths = ex[1][2:end] + else + # import A.B + # (using (importpath A B)) + # (call module_import true nothing (call core.svec 1 "w")) + @chk numchildren(ex) >= 1 + from_path = nothing_(ctx, ex) + paths = children(ex) + end + path_spec = SyntaxList(ctx) + for path in paths + as_name = nothing + if kind(path) == K"as" + @chk numchildren(path) == 2 + as_name = path[2] + @chk kind(as_name) == K"Identifier" + path = path[1] + end + @chk kind(path) == K"importpath" + push!(path_spec, @ast(ctx, path, numchildren(path)::K"Integer")) + _append_importpath(ctx, path_spec, path) + push!(path_spec, isnothing(as_name) ? nothing_(ctx, ex) : + @ast(ctx, as_name, as_name.name_val::K"String")) + end + @ast ctx ex [ + K"call" + module_import ::K"Value" + ctx.mod ::K"Value" + is_using ::K"Value" + from_path + [K"call" + "svec"::K"core" + path_spec... + ] + ] +end + +function expand_module(ctx::DesugaringContext, ex::SyntaxTree) + modname_ex = ex[1] + @chk kind(modname_ex) == K"Identifier" + modname = modname_ex.name_val + + std_defs = if !has_flags(ex, JuliaSyntax.BARE_MODULE_FLAG) + @ast ctx (@HERE) [ + K"block" + [K"using" + [K"importpath" + "Base" ::K"Identifier" + ] + ] + [K"function" + [K"call" + "eval" ::K"Identifier" + "x" ::K"Identifier" + ] + [K"call" + "eval" ::K"core" + modname ::K"Identifier" + "x" ::K"Identifier" + ] + ] + [K"function" + [K"call" + "include" ::K"Identifier" + "x" ::K"Identifier" + ] + [K"call" + "_call_latest" ::K"core" + "include" ::K"top" + modname ::K"Identifier" + "x" ::K"Identifier" + ] + ] + [K"function" + [K"call" + "include" ::K"Identifier" + [K"::" + "mapexpr" ::K"Identifier" + "Function" ::K"top" + ] + "x" ::K"Identifier" + ] + [K"call" + "_call_latest" ::K"core" + "include" ::K"top" + "mapexpr" ::K"Identifier" + modname ::K"Identifier" + "x" ::K"Identifier" + ] + ] + ] + end + + body = ex[2] + @chk kind(body) == K"block" + + @ast ctx ex [ + K"call" + eval_module ::K"Value" + ctx.mod ::K"Value" + modname ::K"String" + [K"inert"(body) + [K"toplevel" + std_defs + children(body)... + ] + ] + ] +end + function expand_forms(ctx::DesugaringContext, ex::SyntaxTree) k = kind(ex) if k == K"call" @@ -417,6 +566,24 @@ function expand_forms(ctx::DesugaringContext, ex::SyntaxTree) "tuple"::K"core" expand_forms(ctx, children(ex))... ] + elseif k == K"module" + # TODO: check-toplevel + expand_module(ctx, ex) + elseif k == K"import" || k == K"using" + # TODO: check-toplevel + expand_import(ctx, ex) + elseif k == K"export" || k == K"public" + TODO(ex) + elseif k == K"toplevel" + # The toplevel form can't be lowered here - it needs to just be quoted + # and passed through to a call to eval. + # TODO: check-toplevel + @ast ctx ex [ + K"call" + eval ::K"Value" + ctx.mod ::K"Value" + [K"inert" ex] + ] elseif !haschildren(ex) ex else @@ -438,8 +605,8 @@ function expand_forms(ctx::DesugaringContext, exs::Union{Tuple,AbstractVector}) res end -function expand_forms(ex::SyntaxTree) - ctx = DesugaringContext(ex) +function expand_forms(mod::Module, ex::SyntaxTree) + ctx = DesugaringContext(ex, mod) res = expand_forms(ctx, reparent(ctx, ex)) ctx, res end diff --git a/src/eval.jl b/src/eval.jl index 8189ce9..07b94cd 100644 --- a/src/eval.jl +++ b/src/eval.jl @@ -1,13 +1,60 @@ function lower(mod, ex) - ctx1, ex1 = expand_forms(ex) - ctx2, ex2 = resolve_scopes!(ctx1, mod, ex1) + ctx1, ex1 = expand_forms(mod, ex) + ctx2, ex2 = resolve_scopes!(ctx1, ex1) ctx3, ex3 = linearize_ir(ctx2, ex2) ex3 end +# CodeInfo constructor. TODO: Should be in Core? +function _CodeInfo(code, + codelocs, + ssavaluetypes, + ssaflags, + method_for_inference_limit_heuristics, + linetable, + slotnames, + slotflags, + slottypes, + rettype, + parent, + edges, + min_world, + max_world, + inferred, + propagate_inbounds, + has_fcall, + nospecializeinfer, + inlining, + constprop, + purity, + inlining_cost) + @eval $(Expr(:new, :(Core.CodeInfo), + convert(Vector{Any}, code), + convert(Vector{Int32}, codelocs), + convert(Any, ssavaluetypes), + convert(Vector{UInt32}, ssaflags), + convert(Any, method_for_inference_limit_heuristics), + convert(Any, linetable), + convert(Vector{Symbol}, slotnames), + convert(Vector{UInt8}, slotflags), + convert(Any, slottypes), + convert(Any, rettype), + convert(Any, parent), + convert(Any, edges), + convert(UInt64, min_world), + convert(UInt64, max_world), + convert(Bool, inferred), + convert(Bool, propagate_inbounds), + convert(Bool, has_fcall), + convert(Bool, nospecializeinfer), + convert(UInt8, inlining), + convert(UInt8, constprop), + convert(UInt16, purity), + convert(UInt16, inlining_cost))) +end + # Convert SyntaxTree to the CodeInfo+Expr data stuctures understood by the # Julia runtime - function to_code_info(ex, mod, funcname, var_info, slot_rewrites) input_code = children(ex) # Convert code to Expr and record low res locations in table @@ -95,7 +142,11 @@ function to_lowered_expr(mod, var_info, ex) elseif k == K"return" Core.ReturnNode(to_lowered_expr(mod, var_info, ex[1])) elseif is_quoted(k) - TODO(ex, "Convert SyntaxTree to Expr") + if k == K"inert" + QuoteNode(ex[1]) + else + TODO(ex, "Convert SyntaxTree to Expr") + end elseif k == K"lambda" funcname = ex.lambda_info.is_toplevel_thunk ? "top-level scope" : @@ -128,6 +179,70 @@ function to_lowered_expr(mod, var_info, ex) end end +#------------------------------------------------------------------------------- +# Runtime support functions called by lowering + +# Construct new bare module including only the "default names" +# +# using Core +# const modname = modval +# public modname +# +# And run statments in the toplevel expression `body` +function eval_module(parentmod, modname, body) + # Here we just use `eval()` with an Expr. + # If we wanted to avoid this we'd need to reproduce a lot of machinery from + # jl_eval_module_expr() + # + # 1. Register / deparent toplevel modules + # 2. Set binding in parent module + # 3. Deal with replacing modules + # * Warn if replacing + # * Root old module being replaced + # 4. Run __init__ + # * Also run __init__ for any children after parent is defined + # mod = @ccall jl_new_module(Symbol(modname)::Symbol, parentmod::Module)::Any + # ... + name = Symbol(modname) + eval(parentmod, :( + baremodule $name + $eval($name, $body) + end + )) +end + +function module_import(into_mod::Module, is_using::Bool, + from_mod::Union{Nothing,Core.SimpleVector}, paths::Core.SimpleVector) + # For now, this function converts our lowered representation back to Expr + # and calls eval() to avoid replicating all of the fiddly logic in + # jl_toplevel_eval_flex. + # FIXME: ccall Julia runtime functions directly? + # * jl_module_using jl_module_use_as + # * import_module jl_module_import_as + path_args = [] + i = 1 + while i < length(paths) + nsyms = paths[i]::Int + n = i + nsyms + path = Expr(:., [Symbol(paths[i+j]::String) for j = 1:nsyms]...) + as_name = paths[i+nsyms+1] + push!(path_args, isnothing(as_name) ? path : + Expr(:as, path, Symbol(as_name))) + i += nsyms + 2 + end + ex = if isnothing(from_mod) + Expr(is_using ? :using : :import, + path_args...) + else + from_path = Expr(:., [Symbol(s::String) for s in from_mod]...) + Expr(is_using ? :using : :import, + Expr(:(:), from_path, path_args...)) + end + eval(into_mod, ex) + nothing +end + + #------------------------------------------------------------------------------- # Our version of eval takes our own data structures function Core.eval(mod::Module, ex::SyntaxTree) diff --git a/src/kinds.jl b/src/kinds.jl index 6b5759f..db6ed8d 100644 --- a/src/kinds.jl +++ b/src/kinds.jl @@ -10,9 +10,6 @@ function _insert_kinds() # A literal Julia value of any kind, as might be inserted by the AST # during macro expansion "Value" - # Quoted symbol. Used to distinguish Symbol literals from AST - # literals of kind K"Identifier" - "Symbol" # TODO: Use `meta` for inbounds and loopinfo etc? "inbounds" "inline" diff --git a/src/linear_ir.jl b/src/linear_ir.jl index 4e9bafb..e2ddabc 100644 --- a/src/linear_ir.jl +++ b/src/linear_ir.jl @@ -32,14 +32,15 @@ struct LinearIRContext{GraphType} <: AbstractLoweringContext graph::GraphType code::SyntaxList{GraphType, Vector{NodeId}} next_var_id::Ref{Int} + is_toplevel_thunk::Bool return_type::Union{Nothing,NodeId} var_info::Dict{VarId,VarInfo} mod::Module end -function LinearIRContext(ctx, return_type) +function LinearIRContext(ctx, is_toplevel_thunk, return_type) LinearIRContext(ctx.graph, SyntaxList(ctx.graph), ctx.next_var_id, - return_type, ctx.var_info, ctx.mod) + is_toplevel_thunk, return_type, ctx.var_info, ctx.mod) end function is_valid_body_ir_argument(ex) @@ -259,15 +260,6 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos) end emit(ctx, ex) nothing - elseif k == K"module" || k == K"toplevel" - # Both these forms can't be lowered here; they need to just be quoted - # and passed through to a call to eval. - # TODO: Is compile() the right place to do this? - # TODO: Restrict to toplevel only - call = makenode(ctx, ex, K"call", - makenode(ctx, ex, K"Value", JuliaLowering.eval), - makenode(ctx, ex, K"Value", ex)) - compile(ctx, call, needs_value, in_tail_pos) elseif k == K"local_def" || k == K"local" nothing else @@ -371,7 +363,7 @@ function compile_lambda(outer_ctx, ex) lambda_info = ex.lambda_info return_type = nothing # FIXME # TODO: Add assignments for reassigned arguments to body using lambda_info.args - ctx = LinearIRContext(outer_ctx, return_type) + ctx = LinearIRContext(outer_ctx, lambda_info.is_toplevel_thunk, return_type) compile_body(ctx, ex[1]) slot_rewrites = Dict{VarId,Int}() _add_slots!(slot_rewrites, ctx.var_info, (arg.var_id for arg in lambda_info.args)) @@ -391,7 +383,7 @@ function linearize_ir(ctx, ex) # TODO: Cleanup needed - `_ctx` is just a dummy context here. But currently # required to call reparent() ... _ctx = LinearIRContext(graph, SyntaxList(graph), ctx.next_var_id, - nothing, ctx.var_info, ctx.mod) + false, nothing, ctx.var_info, ctx.mod) res = compile_lambda(_ctx, reparent(_ctx, ex)) setattr!(graph, res.id, var_info=ctx.var_info) _ctx, res diff --git a/src/scope_analysis.jl b/src/scope_analysis.jl index fc837dd..dae51d0 100644 --- a/src/scope_analysis.jl +++ b/src/scope_analysis.jl @@ -133,11 +133,11 @@ struct ScopeResolutionContext{GraphType} <: AbstractLoweringContext implicit_toplevel_globals::Set{String} end -function ScopeResolutionContext(ctx, mod::Module) +function ScopeResolutionContext(ctx) graph = ensure_attributes(ctx.graph, lambda_locals=Set{VarId}) ScopeResolutionContext(graph, ctx.next_var_id, - mod, + ctx.mod, Dict{String,VarId}(), Vector{ScopeInfo}(), Dict{VarId,VarInfo}(), @@ -361,8 +361,8 @@ function resolve_scopes!(ctx::ScopeResolutionContext, ex) return thunk end -function resolve_scopes!(ctx::DesugaringContext, mod::Module, ex) - ctx2 = ScopeResolutionContext(ctx, mod) +function resolve_scopes!(ctx::DesugaringContext, ex) + ctx2 = ScopeResolutionContext(ctx) res = resolve_scopes!(ctx2, reparent(ctx2, ex)) ctx2, res end diff --git a/src/syntax_graph.jl b/src/syntax_graph.jl index 22b81ae..6f265de 100644 --- a/src/syntax_graph.jl +++ b/src/syntax_graph.jl @@ -81,6 +81,10 @@ function JuliaSyntax.children(graph::SyntaxGraph, id) @view graph.edges[graph.edge_ranges[id]] end +function JuliaSyntax.children(graph::SyntaxGraph, id, r::UnitRange) + @view graph.edges[graph.edge_ranges[id][r]] +end + function JuliaSyntax.child(graph::SyntaxGraph, id::NodeId, i::Integer) graph.edges[graph.edge_ranges[id][i]] end @@ -184,7 +188,7 @@ function Base.getindex(tree::SyntaxTree, i::Integer) end function Base.getindex(tree::SyntaxTree, r::UnitRange) - (child(tree, i) for i in r) + SyntaxList(tree.graph, children(tree.graph, tree.id, r)) end Base.firstindex(tree::SyntaxTree) = 1 @@ -277,8 +281,10 @@ JuliaSyntax.source_location(tree::SyntaxTree) = source_location(sourceref(tree)) JuliaSyntax.first_byte(tree::SyntaxTree) = first_byte(sourceref(tree)) JuliaSyntax.last_byte(tree::SyntaxTree) = last_byte(sourceref(tree)) +const SourceAttrType = Union{SourceRef,LineNumberNode,NodeId} + function SyntaxTree(graph::SyntaxGraph, node::SyntaxNode) - ensure_attributes!(graph, kind=Kind, syntax_flags=UInt16, source=Union{SourceRef,NodeId}, + ensure_attributes!(graph, kind=Kind, syntax_flags=UInt16, source=SourceAttrType, value=Any, name_val=String) id = _convert_nodes(freeze_attrs(graph), node) return SyntaxTree(graph, id) diff --git a/src/utils.jl b/src/utils.jl index 87136cb..81714a8 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,7 +1,7 @@ # Error handling -TODO(msg) = throw(ErrorException("Lowering TODO: $msg")) -TODO(ex, msg) = throw(LoweringError(ex, "Lowering TODO: $msg")) +TODO(msg::AbstractString) = throw(ErrorException("Lowering TODO: $msg")) +TODO(ex::SyntaxTree, msg="") = throw(LoweringError(ex, "Lowering TODO: $msg")) # Errors found during lowering will result in LoweringError being thrown to # indicate the syntax causing the error. @@ -60,53 +60,3 @@ macro chk(ex, cond) _chk_code(ex, cond) end - -#------------------------------------------------------------------------------- -# CodeInfo constructor. TODO: Should be in Core? -function _CodeInfo(code, - codelocs, - ssavaluetypes, - ssaflags, - method_for_inference_limit_heuristics, - linetable, - slotnames, - slotflags, - slottypes, - rettype, - parent, - edges, - min_world, - max_world, - inferred, - propagate_inbounds, - has_fcall, - nospecializeinfer, - inlining, - constprop, - purity, - inlining_cost) - @eval $(Expr(:new, :(Core.CodeInfo), - convert(Vector{Any}, code), - convert(Vector{Int32}, codelocs), - convert(Any, ssavaluetypes), - convert(Vector{UInt32}, ssaflags), - convert(Any, method_for_inference_limit_heuristics), - convert(Any, linetable), - convert(Vector{Symbol}, slotnames), - convert(Vector{UInt8}, slotflags), - convert(Any, slottypes), - convert(Any, rettype), - convert(Any, parent), - convert(Any, edges), - convert(UInt64, min_world), - convert(UInt64, max_world), - convert(Bool, inferred), - convert(Bool, propagate_inbounds), - convert(Bool, has_fcall), - convert(Bool, nospecializeinfer), - convert(UInt8, inlining), - convert(UInt8, constprop), - convert(UInt16, purity), - convert(UInt16, inlining_cost))) -end - diff --git a/test/lowering.jl b/test/demo.jl similarity index 75% rename from test/lowering.jl rename to test/demo.jl index 3b17cee..a482406 100644 --- a/test/lowering.jl +++ b/test/demo.jl @@ -6,7 +6,7 @@ using JuliaLowering using JuliaLowering: SyntaxGraph, SyntaxTree, ensure_attributes!, newnode!, setchildren!, haschildren, children, child, setattr!, sourceref, makenode function wrapscope(ex, scope_type) - makenode(ex, ex, K"block", ex; scope_type=scope_type) + makenode(ex, ex, K"scope_block", ex; scope_type=scope_type) end function softscope_test(ex) @@ -14,6 +14,7 @@ function softscope_test(ex) end #------------------------------------------------------------------------------- +# Demos of the prototype # src = """ # let @@ -57,15 +58,42 @@ end # x + y # """ +src = """ +module A + function f(x)::Int + x + 1 + end + + b = f(2) +end +""" + +src = """ +function f() +end +""" + +src = """ +# import A.B: C.c as d, E.e as f +# import JuliaLowering +using JuliaLowering +""" + +src = """ +module A + z = 1 + 1 +end +""" + ex = parsestmt(SyntaxTree, src, filename="foo.jl") # t = softscope_test(t) @info "Input code" ex in_mod = Main -ctx, ex_desugar = JuliaLowering.expand_forms(ex) +ctx, ex_desugar = JuliaLowering.expand_forms(in_mod, ex) @info "Desugared" ex_desugar -ctx2, ex_scoped = JuliaLowering.resolve_scopes!(ctx, in_mod, ex_desugar) +ctx2, ex_scoped = JuliaLowering.resolve_scopes!(ctx, ex_desugar) @info "Resolved scopes" ex_scoped ctx3, ex_compiled = JuliaLowering.linearize_ir(ctx2, ex_scoped) @@ -73,6 +101,7 @@ ctx3, ex_compiled = JuliaLowering.linearize_ir(ctx2, ex_scoped) ex_expr = JuliaLowering.to_lowered_expr(in_mod, ctx2.var_info, ex_compiled) @info "CodeInfo" ex_expr + x = 100 y = 200 eval_result = Base.eval(in_mod, ex_expr) diff --git a/test/runtests.jl b/test/runtests.jl index 89a2365..9b0627f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,7 @@ using Test using JuliaLowering +using JuliaSyntax using JuliaLowering: @ast @testset "JuliaLowering.jl" begin @@ -42,5 +43,53 @@ JuliaLowering.include_string(test_mod, """ @test test_mod.y == 202 @test JuliaLowering.include_string(test_mod, "x + y") == 303 +# module +A = JuliaLowering.include_string(test_mod, """ +module A + function g() + return "hi" + end +end +""", "module_test") +@test A isa Module +@test A.g() == "hi" +@test A.include isa Function +@test A.Base === Base +@test A.eval(:(x = -1)) == -1 && A.x == -1 + +B = JuliaLowering.include_string(test_mod, """ +baremodule B +end +""", "baremodule_test") +@test B.Core === Core +@test !isdefined(B, :include) +@test !isdefined(B, :Base) + +# using / import +JuliaLowering.include_string(test_mod, """ + using JuliaSyntax + using JuliaLowering: SyntaxTree + using JuliaLowering: SyntaxTree as st + import JuliaLowering: SyntaxTree as st1, SyntaxTree as st2 +""") +@test test_mod.SyntaxTree === JuliaLowering.SyntaxTree +@test test_mod.st === JuliaLowering.SyntaxTree +@test test_mod.st1 === JuliaLowering.SyntaxTree +@test test_mod.st2 === JuliaLowering.SyntaxTree +@test test_mod.parsestmt === JuliaSyntax.parsestmt + +C = JuliaLowering.include_string(test_mod, """ +module C + module D + function f() + "hi" + end + end + module E + using ...C.D: f + end +end +""") +@test C.D.f === C.E.f end