Skip to content

Commit d955016

Browse files
committed
Conversion of assignments to closure captures
Convert captured variables to `Box`es for both read and write, both inside and outside the closure. Also ensure we emit newvar nodes for variables which might be used uninitialized (currently pessimistic).
1 parent 2049bc0 commit d955016

File tree

5 files changed

+236
-82
lines changed

5 files changed

+236
-82
lines changed

src/ast.jl

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ struct BindingInfo
6363
type::Union{Nothing,SyntaxTree} # Type, for bindings declared like x::T = 10
6464
is_const::Bool # Constant, cannot be reassigned
6565
is_ssa::Bool # Single assignment, defined before use
66+
is_captured::Bool # Variable is captured by some lambda
6667
is_always_defined::Bool # A local that we know has an assignment that dominates all usages (is never undef)
6768
is_internal::Bool # True for internal bindings generated by the compiler
6869
is_ambiguous_local::Bool # Local, but would be global in soft scope (ie, the REPL)
@@ -74,11 +75,12 @@ function BindingInfo(name::AbstractString, kind::Symbol;
7475
type::Union{Nothing,SyntaxTree} = nothing,
7576
is_const::Bool = false,
7677
is_ssa::Bool = false,
78+
is_captured::Bool = false,
7779
is_always_defined::Bool = is_ssa,
7880
is_internal::Bool = false,
7981
is_ambiguous_local::Bool = false,
8082
is_nospecialize::Bool = false)
81-
BindingInfo(name, kind, mod, type, is_const, is_ssa, is_always_defined,
83+
BindingInfo(name, kind, mod, type, is_const, is_ssa, is_captured, is_always_defined,
8284
is_internal, is_ambiguous_local, is_nospecialize)
8385
end
8486

@@ -113,7 +115,7 @@ function _binding_id(ex::SyntaxTree)
113115
end
114116

115117
function update_binding!(bindings::Bindings, x;
116-
type=nothing, is_const=nothing, is_always_defined=nothing)
118+
type=nothing, is_const=nothing, is_always_defined=nothing, is_captured=nothing)
117119
id = _binding_id(x)
118120
b = lookup_binding(bindings, id)
119121
bindings.info[id] = BindingInfo(
@@ -123,6 +125,7 @@ function update_binding!(bindings::Bindings, x;
123125
isnothing(type) ? b.type : type,
124126
isnothing(is_const) ? b.is_const : is_const,
125127
b.is_ssa,
128+
isnothing(is_captured) ? b.captured : is_captured,
126129
isnothing(is_always_defined) ? b.is_always_defined : is_always_defined,
127130
b.is_internal,
128131
b.is_ambiguous_local,
@@ -224,6 +227,8 @@ function makeleaf(ctx, srcref, k::Kind, value; kws...)
224227
makeleaf(graph, srcref, k; id=value, kws...)
225228
elseif k == K"symbolic_label"
226229
makeleaf(graph, srcref, k; name_val=value, kws...)
230+
elseif k == K"TOMBSTONE"
231+
makeleaf(graph, srcref, k; kws...)
227232
else
228233
val = k == K"Integer" ? convert(Int, value) :
229234
k == K"Float" ? convert(Float64, value) :
@@ -342,8 +347,15 @@ end
342347
function _expand_ast_tree(ctx, srcref, tree)
343348
if Meta.isexpr(tree, :(::))
344349
# Leaf node
345-
_match_kind(srcref, tree.args[2]) do kind, srcref, kws
346-
:(makeleaf($ctx, $srcref, $kind, $(esc(tree.args[1])), $(kws...)))
350+
if length(tree.args) == 2
351+
val = esc(tree.args[1])
352+
kindspec = tree.args[2]
353+
else
354+
val = nothing
355+
kindspec = tree.args[1]
356+
end
357+
_match_kind(srcref, kindspec) do kind, srcref, kws
358+
:(makeleaf($ctx, $srcref, $kind, $(val), $(kws...)))
347359
end
348360
elseif Meta.isexpr(tree, :call) && tree.args[1] === :(=>)
349361
# Leaf node with copied attributes

src/closure_conversion.jl

Lines changed: 98 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,49 @@ function add_lambda_local!(ctx::ClosureConversionCtx, id)
3131
init_lambda_binding(ctx.lambda_bindings, id)
3232
end
3333

34+
# Access captured variable from inside a closure
35+
function captured_var_access(ctx, ex)
36+
cinfo = ctx.closure_info
37+
field_sym = cinfo.field_syms[cinfo.field_name_inds[ex.var_id]]
38+
@ast ctx ex [K"call"
39+
"getfield"::K"core"
40+
# FIXME: attributing the self binding to srcref=ex gives misleading printing.
41+
# We should carry provenance with each binding to fix this.
42+
ctx.lambda_bindings.self::K"BindingId"
43+
field_sym
44+
]
45+
end
46+
47+
function get_box_contents(ctx::ClosureConversionCtx, var, box_ex)
48+
undef_var = new_mutable_var(ctx, var, lookup_binding(ctx, var.var_id).name)
49+
@ast ctx var [K"block"
50+
box := box_ex
51+
# Lower in an UndefVar check to a similarly named variable
52+
# (ref #20016) so that closure lowering Box introduction
53+
# doesn't impact the error message and the compiler is expected
54+
# to fold away the extraneous null check
55+
#
56+
# TODO: Ideally the runtime would rely on provenance info for
57+
# this error and we can remove isdefined check.
58+
[K"if" [K"call"
59+
"isdefined"::K"core"
60+
box
61+
"contents"::K"Symbol"
62+
]
63+
::K"TOMBSTONE"
64+
[K"block"
65+
[K"newvar" undef_var]
66+
undef_var
67+
]
68+
]
69+
[K"call"
70+
"getfield"::K"core"
71+
box
72+
"contents"::K"Symbol"
73+
]
74+
]
75+
end
76+
3477
# Convert `ex` to `type` by calling `convert(type, ex)` when necessary.
3578
#
3679
# Used for converting the right hand side of an assignment to a typed local or
@@ -118,37 +161,32 @@ function convert_assignment(ctx, ex)
118161
if binfo.kind == :global
119162
convert_global_assignment(ctx, ex, var, rhs0)
120163
else
121-
closed = false # TODO
122-
captured = false # TODO
123-
@assert binfo.kind == :local
124-
if isnothing(binfo.type) && !closed && !captured
164+
@assert binfo.kind == :local || binfo.kind == :argument
165+
lbinfo = get(ctx.lambda_bindings.bindings, var.var_id, nothing)
166+
self_captured = !isnothing(lbinfo) && lbinfo.is_captured
167+
captured = binfo.is_captured
168+
if isnothing(binfo.type) && !self_captured && !captured
125169
@ast ctx ex [K"=" var rhs0]
126170
else
127-
@assert binfo.kind == :local
128171
# Typed local
129-
tmp_rhs0 = is_simple_atom(ctx, rhs0) ? nothing : ssavar(ctx, rhs0)
130-
rhs1 = isnothing(tmp_rhs0) ? rhs0 : tmp_rhs0
131-
rhs = isnothing(binfo.type) ? rhs1 :
132-
convert_for_type_decl(ctx, ex, rhs1, _convert_closures(ctx, binfo.type), true)
133-
assgn = if closed
134-
@assert false # TODO
135-
elseif captured
136-
@assert false # TODO
137-
else
138-
@ast ctx ex [K"=" var rhs]
139-
end
140-
if isnothing(tmp_rhs0)
141-
@ast ctx ex [K"block"
142-
assgn
143-
rhs0
172+
tmp_rhs0 = ssavar(ctx, rhs0)
173+
rhs = isnothing(binfo.type) ? tmp_rhs0 :
174+
convert_for_type_decl(ctx, ex, tmp_rhs0, _convert_closures(ctx, binfo.type), true)
175+
assignment = if self_captured || captured
176+
@ast ctx ex [K"call"
177+
"setfield!"::K"core"
178+
self_captured ? captured_var_access(ctx, var) : var
179+
"contents"::K"Symbol"
180+
rhs
144181
]
145182
else
146-
@ast ctx ex [K"block"
147-
[K"=" tmp_rhs0 rhs0]
148-
assgn
149-
tmp_rhs0
150-
]
183+
@ast ctx ex [K"=" var rhs]
151184
end
185+
@ast ctx ex [K"block"
186+
[K"=" tmp_rhs0 rhs0]
187+
assignment
188+
tmp_rhs0
189+
]
152190
end
153191
end
154192
end
@@ -237,40 +275,10 @@ function _convert_closures(ctx::ClosureConversionCtx, ex)
237275
if k == K"BindingId"
238276
id = ex.var_id
239277
lbinfo = get(ctx.lambda_bindings.bindings, id, nothing)
240-
if !isnothing(lbinfo) && lbinfo.is_captured
241-
cinfo = ctx.closure_info
242-
field_sym = cinfo.field_syms[cinfo.field_name_inds[id]]
243-
undef_var = new_mutable_var(ctx, ex, lookup_binding(ctx, id).name)
244-
@ast ctx ex [K"block"
245-
box := [K"call"
246-
"getfield"::K"core"
247-
ctx.lambda_bindings.self::K"BindingId"
248-
field_sym
249-
]
250-
# Lower in an UndefVar check to a similarly named variable
251-
# (ref #20016) so that closure lowering Box introduction
252-
# doesn't impact the error message and the compiler is expected
253-
# to fold away the extraneous null check
254-
#
255-
# TODO: Ideally the runtime would rely on provenance info for
256-
# this error and we can remove isdefined check.
257-
[K"if" [K"call"
258-
"isdefined"::K"core"
259-
box
260-
"contents"::K"Symbol"
261-
]
262-
"nothing"::K"core"
263-
[K"block"
264-
[K"newvar" undef_var]
265-
undef_var
266-
]
267-
]
268-
[K"call"
269-
"getfield"::K"core"
270-
box
271-
"contents"::K"Symbol"
272-
]
273-
]
278+
if !isnothing(lbinfo) && lbinfo.is_captured # TODO: && vinfo:asgn cv ??
279+
get_box_contents(ctx, ex, captured_var_access(ctx, ex))
280+
elseif lookup_binding(ctx, id).is_captured # TODO: && vinfo:asgn vi
281+
get_box_contents(ctx, ex, ex)
274282
else
275283
ex
276284
end
@@ -295,6 +303,16 @@ function _convert_closures(ctx::ClosureConversionCtx, ex)
295303
_convert_closures(ctx, ex[2])
296304
]
297305
end
306+
elseif k == K"local"
307+
var = ex[1]
308+
binfo = lookup_binding(ctx, var)
309+
if binfo.is_captured
310+
@ast ctx ex [K"=" var [K"call" "Box"::K"core"]]
311+
elseif !binfo.is_always_defined
312+
@ast ctx ex [K"newvar" var]
313+
else
314+
makeleaf(ctx, ex, K"TOMBSTONE")
315+
end
298316
elseif k == K"::"
299317
_convert_closures(ctx,
300318
@ast ctx ex [K"call"
@@ -322,10 +340,7 @@ function _convert_closures(ctx::ClosureConversionCtx, ex)
322340
ctx.closure_infos[func_name_id] = closure_info
323341
init_closure_args = SyntaxList(ctx)
324342
for id in field_orig_bindings
325-
# FIXME: This isn't actually correct: we need to convert
326-
# all outer references to boxes too!
327-
push!(init_closure_args, _convert_closures(ctx,
328-
@ast ctx ex [K"call" "Box"::K"core" id::K"BindingId"]))
343+
push!(init_closure_args, @ast ctx ex id::K"BindingId")
329344
end
330345
@ast ctx ex [K"block"
331346
[K"=" func_name
@@ -380,18 +395,30 @@ function closure_convert_lambda(ctx, ex)
380395
@assert kind(ex) == K"lambda"
381396
body_stmts = SyntaxList(ctx)
382397
toplevel_stmts = ex.is_toplevel_thunk ? body_stmts : ctx.toplevel_stmts
398+
lambda_bindings = ex.lambda_bindings
383399
ctx2 = ClosureConversionCtx(ctx.graph, ctx.bindings, ctx.mod,
384-
ctx.closure_bindings, ctx.closure_info, ex.lambda_bindings,
400+
ctx.closure_bindings, ctx.closure_info, lambda_bindings,
385401
toplevel_stmts, ctx.closure_infos)
386402
lambda_children = SyntaxList(ctx)
387-
push!(lambda_children, _convert_closures(ctx2, ex[1]))
388-
push!(lambda_children, _convert_closures(ctx2, ex[2]))
403+
args = ex[1]
404+
push!(lambda_children, args)
405+
push!(lambda_children, ex[2])
389406

390-
# Convert body. This is done as a special case to allow inner calls to
391-
# _convert_closures to also add to body_stmts in the case that
392-
# ex.is_toplevel_thunk is true.
393-
in_body_stmts = kind(ex[3]) != K"block" ? ex[3:3] : ex[3][1:end]
394-
for e in in_body_stmts
407+
# Add box initializations for arguments which are captured by an inner lambda
408+
for arg in children(args)
409+
kind(arg) != K"Placeholder" || continue
410+
binfo = lookup_binding(ctx, arg)
411+
if binfo.is_captured # TODO: && binfo.is_assigned
412+
push!(body_stmts, @ast ctx arg [K"="
413+
arg
414+
[K"call" "Box"::K"core" arg]
415+
])
416+
end
417+
end
418+
# Convert body. Note that _convert_closures may call `push!(body_stmts, e)`
419+
# internally for any expressions `e` which need to be moved to top level.
420+
input_body_stmts = kind(ex[3]) != K"block" ? ex[3:3] : ex[3][1:end]
421+
for e in input_body_stmts
395422
push!(body_stmts, _convert_closures(ctx2, e))
396423
end
397424
push!(lambda_children, @ast ctx2 ex[3] [K"block" body_stmts...])

src/scope_analysis.jl

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ function _find_scope_vars!(assignments, locals, destructured_args, globals, used
6464
end
6565

6666
# Find names of all identifiers used in the given expression, grouping them
67-
# into sets by type.
67+
# into sets by type of usage.
6868
#
69-
# NB: This only works propery after desugaring has already processed assignments
69+
# NB: This only works propery after desugaring
7070
function find_scope_vars(ex)
7171
ExT = typeof(ex)
7272
assignments = Dict{NameKey,ExT}()
@@ -422,6 +422,7 @@ function analyze_scope(ctx, ex, scope_type, is_toplevel_global_scope=false,
422422
if !haskey(lambda_bindings.bindings, id)
423423
# Used vars from a scope *outside* the current lambda are captured
424424
init_lambda_binding(lambda_bindings, id, is_captured=true, is_read=true)
425+
update_binding!(ctx, id; is_captured=true)
425426
else
426427
update_lambda_binding!(lambda_bindings, id, is_read=true)
427428
end
@@ -437,6 +438,7 @@ function analyze_scope(ctx, ex, scope_type, is_toplevel_global_scope=false,
437438
if !haskey(lambda_bindings.bindings, id)
438439
# Assigned vars from a scope *outside* the current lambda are captured
439440
init_lambda_binding(lambda_bindings, id, is_captured=true, is_assigned=true)
441+
update_binding!(ctx, id; is_captured=true)
440442
else
441443
update_lambda_binding!(lambda_bindings, id, is_assigned=true)
442444
end
@@ -457,6 +459,17 @@ function analyze_scope(ctx, ex, scope_type, is_toplevel_global_scope=false,
457459
is_hard_scope, var_ids, lambda_bindings)
458460
end
459461

462+
function add_local_decls!(ctx, stmts, srcref, scope)
463+
# Add local decls to start of block so that closure conversion can
464+
# initialize if necessary.
465+
for id in values(scope.var_ids)
466+
binfo = lookup_binding(ctx, id)
467+
if binfo.kind == :local
468+
push!(stmts, @ast ctx srcref [K"local" id::K"BindingId"])
469+
end
470+
end
471+
end
472+
460473
# Do some things which are better done after converting to BindingId.
461474
function maybe_update_bindings!(ctx, ex)
462475
k = kind(ex)
@@ -522,12 +535,20 @@ function _resolve_scopes(ctx, ex::SyntaxTree)
522535
push!(ctx.scope_stack, scope)
523536
arg_bindings = _resolve_scopes(ctx, ex[1])
524537
sparm_bindings = _resolve_scopes(ctx, ex[2])
538+
body_stmts = SyntaxList(ctx)
539+
add_local_decls!(ctx, body_stmts, ex, scope)
525540
body = _resolve_scopes(ctx, ex[3])
541+
if kind(body) == K"block"
542+
append!(body_stmts, children(body))
543+
else
544+
push!(body_stmts, body)
545+
end
526546
ret_var = numchildren(ex) == 4 ? _resolve_scopes(ctx, ex[4]) : nothing
527547
pop!(ctx.scope_stack)
528548

529549
lambda_bindings = scope.lambda_bindings
530550
if !is_toplevel_thunk
551+
# Record all lambdas for the same closure type in one place
531552
func_name = last(ctx.method_def_stack)
532553
if kind(func_name) == K"BindingId"
533554
func_name_id = func_name.var_id
@@ -544,19 +565,21 @@ function _resolve_scopes(ctx, ex::SyntaxTree)
544565
is_toplevel_thunk=is_toplevel_thunk)
545566
arg_bindings
546567
sparm_bindings
547-
body
568+
[K"block"
569+
body_stmts...
570+
]
548571
ret_var
549572
]
550573
elseif k == K"scope_block"
551574
scope = analyze_scope(ctx, ex, ex.scope_type)
552575
push!(ctx.scope_stack, scope)
553-
body = SyntaxList(ctx)
576+
stmts = SyntaxList(ctx)
577+
add_local_decls!(ctx, stmts, ex, scope)
554578
for e in children(ex)
555-
push!(body, _resolve_scopes(ctx, e))
579+
push!(stmts, _resolve_scopes(ctx, e))
556580
end
557-
body
558581
pop!(ctx.scope_stack)
559-
@ast ctx ex [K"block" body...]
582+
@ast ctx ex [K"block" stmts...]
560583
elseif k == K"extension"
561584
etype = extension_type(ex)
562585
if etype == "islocal"

src/utils.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ function print_ir(io::IO, ex, indent="")
7878
println(io, indent, lno, " --- method ", string(e[1]), " ", string(e[2]))
7979
@assert kind(e[3]) == K"lambda" || kind(e[3]) == K"code_info"
8080
print_ir(io, e[3], indent*" ")
81+
elseif kind(e) == K"code_info" && e.is_toplevel_thunk
82+
println(io, indent, lno, " --- thunk")
83+
print_ir(io, e, indent*" ")
8184
else
8285
code = string(e)
8386
println(io, indent, lno, " ", code)

0 commit comments

Comments
 (0)