Skip to content

Commit 65368b6

Browse files
committed
Add K"deferred_toplevel_eval" for cfunction name
A few special forms have a kind of "deferred top level evaluation" semantics for some of their children: * `@cfunction` - the function name and types * `ccall` / `foreigncall` - the type arguments * `@generated` - the expression defining the generated function stub For example, in `@ccall f()::Int`, the `Int` means "the symbol `Int` as looked up in global scope in the module", and should fail if `Int` refers to a local variable. Currently all three of these cases are handled through different mechanisms with varying levels of hygiene inconsistency and ability to warn about access to local variables. To fix this problem, introduce the new `K"deferred_toplevel_eval"` form which wraps an expression and preserves it as a piece of AST in the output (rather than producing IR), but still resolves scope and hygiene.
1 parent e84b0cf commit 65368b6

File tree

8 files changed

+30
-25
lines changed

8 files changed

+30
-25
lines changed

src/closure_conversion.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ function _convert_closures(ctx::ClosureConversionCtx, ex)
320320
else
321321
access
322322
end
323-
elseif is_leaf(ex) || k == K"inert"
323+
elseif is_leaf(ex) || k == K"inert" || k == K"deferred_toplevel_eval"
324324
ex
325325
elseif k == K"="
326326
convert_assignment(ctx, ex)

src/eval.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,9 @@ function to_lowered_expr(mod, ex, ssa_offset=0)
302302
# Unpack K"Symbol" QuoteNode as `Expr(:meta)` requires an identifier here.
303303
args[1] = args[1].value
304304
Expr(:meta, args...)
305+
elseif k == K"deferred_toplevel_eval"
306+
@assert numchildren(ex) == 1
307+
QuoteNode(to_lowered_expr(mod, ex[1], ssa_offset))
305308
else
306309
# Allowed forms according to https://docs.julialang.org/en/v1/devdocs/ast/
307310
#

src/kinds.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ function _register_kinds()
5454
# For expr-macro compatibility; gone after expansion
5555
"escape"
5656
"hygienic_scope"
57+
# An expression which will eventually be evaluated at top level in
58+
# the enclosing module. Used for the callable name in @cfunction;
59+
# TODO: Use this for GeneratedFunctionStub and ccall types also?
60+
# may be used for ccall type arguments at some point.
61+
"deferred_toplevel_eval"
5762
# Catch-all for additional syntax extensions without the need to
5863
# extend `Kind`. Known extensions include:
5964
# locals, islocal

src/linear_ir.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
function is_valid_ir_argument(ctx, ex)
55
k = kind(ex)
6-
if is_simple_atom(ctx, ex) || k in KSet"inert top core quote"
6+
if is_simple_atom(ctx, ex) || k in KSet"inert top core quote deferred_toplevel_eval"
77
true
88
elseif k == K"BindingId"
99
binfo = lookup_binding(ctx, ex)
@@ -112,7 +112,7 @@ end
112112
function is_simple_arg(ctx, ex)
113113
k = kind(ex)
114114
return is_simple_atom(ctx, ex) || k == K"BindingId" || k == K"quote" || k == K"inert" ||
115-
k == K"top" || k == K"core" || k == K"globalref"
115+
k == K"top" || k == K"core" || k == K"globalref" || k == K"deferred_toplevel_eval"
116116
end
117117

118118
function is_single_assign_var(ctx::LinearIRContext, ex)
@@ -128,7 +128,7 @@ function is_const_read_arg(ctx, ex)
128128
# Even if we have side effects, we know that singly-assigned
129129
# locals cannot be affected by them so we can inline them anyway.
130130
# TODO from flisp: "We could also allow const globals here"
131-
return k == K"inert" || k == K"top" || k == K"core" ||
131+
return k == K"inert" || k == K"top" || k == K"core" || k == K"deferred_toplevel_eval" ||
132132
is_simple_atom(ctx, ex) || is_single_assign_var(ctx, ex)
133133
end
134134

@@ -593,7 +593,7 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos)
593593
k = kind(ex)
594594
if k == K"BindingId" || is_literal(k) || k == K"quote" || k == K"inert" ||
595595
k == K"top" || k == K"core" || k == K"Value" || k == K"Symbol" ||
596-
k == K"SourceLocation"
596+
k == K"SourceLocation" || k == K"deferred_toplevel_eval"
597597
if in_tail_pos
598598
emit_return(ctx, ex)
599599
elseif needs_value
@@ -909,7 +909,7 @@ function compile(ctx::LinearIRContext, ex, needs_value, in_tail_pos)
909909
end
910910

911911
function _remove_vars_with_isdefined_check!(vars, ex)
912-
if is_leaf(ex) || is_quoted(ex)
912+
if is_leaf(ex) || is_quoted(ex) || kind(ex) == K"deferred_toplevel_eval"
913913
return
914914
elseif kind(ex) == K"isdefined"
915915
delete!(vars, ex[1].var_id)
@@ -1017,7 +1017,7 @@ function _renumber(ctx, ssa_rewrites, slot_rewrites, label_table, ex)
10171017
makeleaf(ctx, ex, K"globalref", binfo.name, mod=binfo.mod)
10181018
end
10191019
end
1020-
elseif k == K"meta"
1020+
elseif k == K"meta" || k == K"deferred_toplevel_eval"
10211021
# Somewhat-hack for Expr(:meta, :generated, gen) which has
10221022
# weird top-level semantics for `gen`, but we still need to translate
10231023
# the binding it contains to a globalref.

src/scope_analysis.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,14 @@ function _resolve_scopes(ctx, ex::SyntaxTree)
488488
end
489489
pop!(ctx.scope_stack)
490490
@ast ctx ex [K"block" stmts...]
491+
elseif k == K"deferred_toplevel_eval"
492+
local_scopes = splice!(ctx.scope_stack, 2:lastindex(ctx.scope_stack))
493+
scope = analyze_scope(ctx, ex, nothing, true)
494+
push!(ctx.scope_stack, scope)
495+
resolved = mapchildren(e->_resolve_scopes(ctx, e), ctx, ex)
496+
pop!(ctx.scope_stack)
497+
append!(ctx.scope_stack, local_scopes)
498+
resolved
491499
elseif k == K"extension"
492500
etype = extension_type(ex)
493501
if etype == "islocal"
@@ -647,7 +655,7 @@ function analyze_variables!(ctx, ex)
647655
update_binding!(ctx, ex, is_captured=true)
648656
end
649657
end
650-
elseif is_leaf(ex) || is_quoted(ex)
658+
elseif is_leaf(ex) || is_quoted(ex) || k == K"deferred_toplevel_eval"
651659
return
652660
elseif k == K"local" || k == K"global"
653661
# Presence of BindingId within local/global is ignored.

src/syntax_macros.jl

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,21 +91,7 @@ function Base.var"@cfunction"(__context__::MacroContext, callable, return_type,
9191
# Kinda weird semantics here - without `$`, the callable is a top level
9292
# expression which will be evaluated by `jl_resolve_globals_in_ir`,
9393
# implicitly within the module where the `@cfunction` is expanded into.
94-
#
95-
# TODO: The existing flisp implementation is arguably broken because it
96-
# ignores macro hygiene when `callable` is the result of a macro
97-
# expansion within a different module. For now we've inherited this
98-
# brokenness.
99-
#
100-
# Ideally we'd fix this by bringing the scoping rules for this
101-
# expression back into lowering. One option may be to wrap the
102-
# expression in a form which pushes it to top level - maybe as a whole
103-
# separate top level thunk like closure lowering - then use the
104-
# K"captured_local" mechanism to interpolate it back in. This scheme
105-
# would make the complicated scope semantics explicit and let them be
106-
# dealt with in the right place in the frontend rather than putting the
107-
# rules into the runtime itself.
108-
fptr = @ast __context__ callable QuoteNode(Expr(callable))::K"Value"
94+
fptr = @ast __context__ callable [K"deferred_toplevel_eval" callable]
10995
typ = Ptr{Cvoid}
11096
end
11197
@ast __context__ __context__.macrocall [K"cfunction"

test/misc.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ cf_int = JuliaLowering.include_string(test_mod, """
4242
""")
4343
@test @ccall($cf_int(2::Int, 3::Int)::Int) == 32
4444
cf_float = JuliaLowering.include_string(test_mod, """
45-
@cfunction(f_ccallable, Float64, (Float64,Float64))
45+
let
46+
f_ccallable = "irrelevant"
47+
@cfunction(f_ccallable, Float64, (Float64,Float64))
48+
end
4649
""")
4750
@test @ccall($cf_float(2::Float64, 3::Float64)::Float64) == 32.0
4851

test/misc_ir.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ JuxtTest.@emit_juxt
323323
# @cfunction expansion with global generic function as function argument
324324
@cfunction(callable, Int, (Int, Float64))
325325
#---------------------
326-
1 (cfunction Ptr{Nothing} :(:callable) TestMod.Int (call core.svec TestMod.Int TestMod.Float64) :ccall)
326+
1 (cfunction Ptr{Nothing} (deferred_toplevel_eval TestMod.callable) TestMod.Int (call core.svec TestMod.Int TestMod.Float64) :ccall)
327327
2 (return %₁)
328328

329329
########################################

0 commit comments

Comments
 (0)