Skip to content

Commit 2b487e1

Browse files
committed
Incremental lowering API
Julia's incrementally evaluated top level semantics make it rather tricky to design a lowering interface for top level and module level expressions. Currently these expressions are effectively *interpreted* by eval rather than ever being processed by lowering. However, I'd like a cleaner separation between "low level evaluation" and lowering, such that Core can contain only the low level eval "driver function". I'd like to propose the split as follows: * "Low level" evaluation is about executing a sequence of thunks represented as `CodeInfo` and creating modules for those to be executed inside. * Lowering is about expression processing. In principle, the runtime's view of `eval()` shouldn't know about `Expr` or `SyntaxTree` (or whatever AST we use) - that should be left to the compiler frontend. A useful way to think about the duties of the frontend is to consider the question "What if we wanted to host another language on top of the Julia runtime?". If we can eventually achieve that without ever generating Julia `Expr` then we will have succeeded in separating the frontend. To implement all this I've recast lowering as an incremental iterative API in this change. Thus it's the job of `eval()` to simply evaluate thunks and create new modules as driven by lowering. (Perhaps we'd move this definition of `eval()` over to the Julia runtime before 1.13.) The iteration API is currently oddly bespoke and arguably somewhat non-Julian for two reasons: * Lowering knows when new modules are required, and may request them with `:begin_module`. However `eval()` generates those modules so they need to be passed back into lowering. So we can't just use `Base.iterate()`. (Put a different way, we have a situation which is suited to coroutines but we don't want to use full Julia `Task`s for this.) * We might want to implement this `eval()` in Julia's C runtime code or early in bootstrap. Hence using SimpleVector and Symbol as the return values of `lower_step()` We might consider changing at least the second of these choices, depending on how we end up integrating this into Base.
1 parent d44ce70 commit 2b487e1

File tree

3 files changed

+165
-53
lines changed

3 files changed

+165
-53
lines changed

src/eval.jl

Lines changed: 152 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Non-incremental lowering API for non-toplevel non-module expressions.
2+
# May be removed?
3+
14
function lower(mod::Module, ex0; expr_compat_mode=false, world=Base.get_world_counter())
25
ctx1, ex1 = expand_forms_1( mod, ex0, expr_compat_mode, world)
36
ctx2, ex2 = expand_forms_2( ctx1, ex1)
@@ -12,6 +15,96 @@ function macroexpand(mod::Module, ex; expr_compat_mode=false, world=Base.get_wor
1215
ex1
1316
end
1417

18+
# Incremental lowering API which can manage toplevel and module expressions.
19+
#
20+
# This iteration API is oddly bespoke and arguably somewhat non-Julian for two
21+
# reasons:
22+
#
23+
# * Lowering knows when new modules are required, and may request them with
24+
# `:begin_module`. However `eval()` generates those modules so they need to
25+
# be passed back into lowering. So we can't just use `Base.iterate()`. (Put a
26+
# different way, we have a situation which is suited to coroutines but we
27+
# don't want to use full Julia `Task`s for this.)
28+
# * We might want to implement this `eval()` in Julia's C runtime code or early
29+
# in bootstrap. Hence using SimpleVector and Symbol as the return values of
30+
# `lower_step()`
31+
#
32+
# We might consider changing at least the second of these choices, depending on
33+
# how we end up putting this into Base.
34+
35+
struct LoweringIterator{GraphType}
36+
ctx::MacroExpansionContext{GraphType}
37+
todo::Vector{Tuple{SyntaxTree{GraphType}, Bool, Int}}
38+
end
39+
40+
function lower_init(ex::SyntaxTree, mod::Module, macro_world::UInt; expr_compat_mode::Bool=false)
41+
graph = ensure_macro_attributes(syntax_graph(ex))
42+
ctx = MacroExpansionContext(graph, mod, expr_compat_mode, macro_world)
43+
ex = reparent(ctx, ex)
44+
LoweringIterator{typeof(graph)}(ctx, [(ex, false, 0)])
45+
end
46+
47+
function lower_step(iter, push_mod=nothing)
48+
if !isnothing(push_mod)
49+
push_layer!(iter.ctx, push_mod, false)
50+
end
51+
52+
if isempty(iter.todo)
53+
return Core.svec(:done)
54+
end
55+
56+
ex, is_module_body, child_idx = pop!(iter.todo)
57+
if child_idx > 0
58+
next_child = child_idx + 1
59+
if child_idx <= numchildren(ex)
60+
push!(iter.todo, (ex, is_module_body, next_child))
61+
ex = ex[child_idx]
62+
else
63+
if is_module_body
64+
pop_layer!(iter.ctx)
65+
return Core.svec(:end_module)
66+
else
67+
return lower_step(iter)
68+
end
69+
end
70+
end
71+
72+
k = kind(ex)
73+
if !(k in KSet"toplevel module")
74+
ex = expand_forms_1(iter.ctx, ex)
75+
k = kind(ex)
76+
end
77+
if k == K"toplevel"
78+
push!(iter.todo, (ex, false, 1))
79+
return lower_step(iter)
80+
elseif k == K"module"
81+
name = ex[1]
82+
if kind(name) != K"Identifier"
83+
throw(LoweringError(name, "Expected module name"))
84+
end
85+
newmod_name = Symbol(name.name_val)
86+
body = ex[2]
87+
if kind(body) != K"block"
88+
throw(LoweringError(body, "Expected block in module body"))
89+
end
90+
std_defs = !has_flags(ex, JuliaSyntax.BARE_MODULE_FLAG)
91+
loc = source_location(LineNumberNode, ex)
92+
push!(iter.todo, (body, true, 1))
93+
return Core.svec(:begin_module, newmod_name, std_defs, loc)
94+
else
95+
# Non macro expansion parts of lowering
96+
ctx2, ex2 = expand_forms_2(iter.ctx, ex)
97+
ctx3, ex3 = resolve_scopes(ctx2, ex2)
98+
ctx4, ex4 = convert_closures(ctx3, ex3)
99+
ctx5, ex5 = linearize_ir(ctx4, ex4)
100+
thunk = to_lowered_expr(ex5)
101+
return Core.svec(:thunk, thunk)
102+
end
103+
end
104+
105+
106+
#-------------------------------------------------------------------------------
107+
15108
function codeinfo_has_image_globalref(@nospecialize(e))
16109
if e isa GlobalRef
17110
return 0x00 !== @ccall jl_object_in_image(e.mod::Any)::UInt8
@@ -274,7 +367,7 @@ function _to_lowered_expr(ex::SyntaxTree, stmt_offset::Int)
274367
elseif k == K"code_info"
275368
ir = to_code_info(ex[1], ex.slots)
276369
if ex.is_toplevel_thunk
277-
Expr(:thunk, ir)
370+
Expr(:thunk, ir) # TODO: Maybe nice to just return a CodeInfo here?
278371
else
279372
ir
280373
end
@@ -357,64 +450,74 @@ function _to_lowered_expr(ex::SyntaxTree, stmt_offset::Int)
357450
end
358451

359452
#-------------------------------------------------------------------------------
360-
# Our version of eval takes our own data structures
453+
# Our version of eval - should be upstreamed though?
361454
@fzone "JL: eval" function eval(mod::Module, ex::SyntaxTree;
362-
expr_compat_mode::Bool=false, macro_world=Base.get_world_counter())
363-
graph = ensure_macro_attributes(syntax_graph(ex))
364-
ctx = MacroExpansionContext(graph, mod, expr_compat_mode, macro_world)
365-
_eval(ctx, ex)
455+
macro_world::UInt=Base.get_world_counter(),
456+
opts...)
457+
iter = lower_init(ex, mod, macro_world; opts...)
458+
_eval(mod, iter)
366459
end
367460

368-
function _eval_stmts(ctx, exs)
369-
res = nothing
370-
for ex in exs
371-
res = _eval(ctx, ex)
461+
if VERSION >= v"1.13.0-DEV.1199" # https://github.com/JuliaLang/julia/pull/59604
462+
463+
function _eval(mod, iter)
464+
modules = Module[]
465+
new_mod = nothing
466+
result = nothing
467+
while true
468+
thunk = lower_step(iter, new_mod)::Core.SimpleVector
469+
new_mod = nothing
470+
type = thunk[1]::Symbol
471+
if type == :done
472+
break
473+
elseif type == :begin_module
474+
push!(modules, mod)
475+
mod = @ccall jl_begin_new_module(mod::Any, thunk[2]::Symbol, thunk[3]::Cint,
476+
thunk[4].file::Cstring, thunk[4].line::Cint)::Module
477+
new_mod = mod
478+
elseif type == :end_module
479+
@ccall jl_end_new_module(mod::Module)::Cvoid
480+
result = mod
481+
mod = pop!(modules)
482+
else
483+
@assert type == :thunk
484+
result = Core.eval(mod, thunk[2])
485+
end
372486
end
373-
res
487+
@assert isempty(modules)
488+
return result
374489
end
375490

376-
function _eval_module_body(ctx, mod, ex)
377-
new_layer = ScopeLayer(length(ctx.scope_layers)+1, mod,
378-
current_layer_id(ctx), false)
379-
push!(ctx.scope_layers, new_layer)
380-
push!(ctx.scope_layer_stack, new_layer.id)
381-
stmts = kind(ex[2]) == K"block" ? ex[2][1:end] : ex[2:2]
382-
_eval_stmts(ctx, stmts)
383-
pop!(ctx.scope_layer_stack)
384-
end
385-
386-
function _eval_module(ctx, ex)
387-
# Here we just use `eval()` with an Expr to create a module.
388-
# TODO: Refactor jl_eval_module_expr() in the runtime so that we can avoid
389-
# eval.
390-
std_defs = !has_flags(ex, JuliaSyntax.BARE_MODULE_FLAG)
391-
newmod_name = Symbol(ex[1].name_val)
392-
Core.eval(current_layer(ctx).mod,
393-
Expr(:module, std_defs, newmod_name,
394-
Expr(:block, Expr(:call,
395-
newmod->_eval_module_body(ctx, newmod, ex),
396-
newmod_name))))
397-
end
491+
else
398492

399-
function _eval(ctx, ex::SyntaxTree)
400-
k = kind(ex)
401-
if k == K"toplevel"
402-
_eval_stmts(ctx, children(ex))
403-
elseif k == K"module"
404-
_eval_module(ctx, ex)
405-
else
406-
ex1 = expand_forms_1(ctx, ex)
407-
if kind(ex1) in KSet"toplevel module"
408-
_eval(ctx, ex1)
493+
function _eval(mod, iter, new_mod=nothing)
494+
in_new_mod = !isnothing(new_mod)
495+
result = nothing
496+
while true
497+
thunk = lower_step(iter, new_mod)::Core.SimpleVector
498+
new_mod = nothing
499+
type = thunk[1]::Symbol
500+
if type == :done
501+
@assert !in_new_mod
502+
break
503+
elseif type == :begin_module
504+
name = thunk[2]::Symbol
505+
std_defs = thunk[3]
506+
result = Core.eval(mod,
507+
Expr(:module, std_defs, name,
508+
Expr(:block, thunk[4], Expr(:call, m->_eval(m, iter, m), name)))
509+
)
510+
elseif type == :end_module
511+
@assert in_new_mod
512+
return mod
409513
else
410-
ctx2, ex2 = expand_forms_2(ctx, ex1)
411-
ctx3, ex3 = resolve_scopes(ctx2, ex2)
412-
ctx4, ex4 = convert_closures(ctx3, ex3)
413-
ctx5, ex5 = linearize_ir(ctx4, ex4)
414-
thunk = to_lowered_expr(ex5)
415-
Core.eval(current_layer(ctx).mod, thunk)
514+
@assert type == :thunk
515+
result = Core.eval(mod, thunk[2])
416516
end
417517
end
518+
return result
519+
end
520+
418521
end
419522

420523
"""

src/macro_expansion.jl

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ function MacroExpansionContext(graph::SyntaxGraph, mod::Module, expr_compat_mode
2929
MacroExpansionContext(graph, Bindings(), layers, LayerId[length(layers)], expr_compat_mode, world)
3030
end
3131

32+
function push_layer!(ctx::MacroExpansionContext, mod::Module, is_macro_expansion::Bool)
33+
new_layer = ScopeLayer(length(ctx.scope_layers)+1, mod,
34+
current_layer_id(ctx), is_macro_expansion)
35+
push!(ctx.scope_layers, new_layer)
36+
push!(ctx.scope_layer_stack, new_layer.id)
37+
end
38+
function pop_layer!(ctx::MacroExpansionContext)
39+
pop!(ctx.scope_layer_stack)
40+
end
41+
3242
current_layer(ctx::MacroExpansionContext) = ctx.scope_layers[last(ctx.scope_layer_stack)]
3343
current_layer_id(ctx::MacroExpansionContext) = last(ctx.scope_layer_stack)
3444

@@ -342,10 +352,9 @@ function expand_macro(ctx, ex)
342352
expanded = fix_toplevel_expansion(ctx, expanded, mod_for_ast, macro_loc)
343353
new_layer = ScopeLayer(length(ctx.scope_layers)+1, mod_for_ast,
344354
current_layer_id(ctx), true)
345-
push!(ctx.scope_layers, new_layer)
346-
push!(ctx.scope_layer_stack, new_layer.id)
355+
push_layer!(ctx, mod_for_ast, true)
347356
expanded = expand_forms_1(ctx, expanded)
348-
pop!(ctx.scope_layer_stack)
357+
pop_layer!(ctx)
349358
end
350359
return expanded
351360
end

test/demo.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ end
7474

7575
#-------------------------------------------------------------------------------
7676
# Module containing macros used in the demo.
77-
define_macros = true
77+
define_macros = false
7878
if !define_macros
7979
eval(:(module M end))
8080
else

0 commit comments

Comments
 (0)