Skip to content

Commit 87d3421

Browse files
committed
Complete tuple destructuring with complex splatted left hand sides
The flisp implementation has several arguable-bugs here which allow us to observe some assignments before all side effects of the right hand side have occurred. For example (x, y) = (1, undefined) assigns to `x` before throwing the `UndefVarError`. As another example, let f() = (x = 100) (x, y) = (1, f()) x end leaves `x` with the value of 100 in the existing implementation. Both these examples violate the principle that symbolic simpification should not be observable. As a fix, we now assign the right hand sides to temporary variables and do this even for normal identifiers on the right hand side, ensuring the normal left-to-right evaluation order for function arguments on the right hand side.
1 parent a3b3096 commit 87d3421

File tree

5 files changed

+319
-119
lines changed

5 files changed

+319
-119
lines changed

JuliaLowering/src/desugaring.jl

Lines changed: 96 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ function is_effect_free(ex)
6767
k = kind(ex)
6868
# TODO: metas
6969
is_literal(k) || is_identifier_like(ex) || k == K"Symbol" ||
70-
k == K"inert" || k == K"top" || k == K"core"
70+
k == K"inert" || k == K"top" || k == K"core" || k == K"Value"
7171
# flisp also includes `a.b` with simple `a`, but this seems like a bug
7272
# because this calls the user-defined getproperty?
7373
end
@@ -90,96 +90,122 @@ end
9090
# Destructuring
9191

9292
# Convert things like `(x,y,z) = (a,b,c)` to assignments, eliminating the
93-
# tuple. Includes support for slurping/splatting.
93+
# tuple. Includes support for slurping/splatting. This function assumes that
94+
# `_tuple_sides_match` returns true, so the following have already been
95+
# checked:
96+
# * There's max one `...` on the left hand side
97+
# * There's max one `...` on the right hand side, in the last place, or
98+
# matched with an lhs... in the last place. (required so that
99+
# pairwise-matching terms from the right is valid)
100+
# * Neither side has any key=val terms or parameter blocks
94101
#
95-
# If lhss and rhss are the list of terms on each side, this function assumes
96-
# the following have been checked:
97-
# * There's only one `...` on the left hand side
98-
# * Neither side has any key=val terms
99-
# * _tuple_sides_match returns true
102+
# Tuple elimination must act /as if/ the right hand side tuple was first
103+
# constructed followed by destructuring. In particular, any side effects due to
104+
# evaluating the individual terms in the right hand side tuple must happen in
105+
# order.
100106
function tuple_to_assignments(ctx, ex)
101107
lhs = ex[1]
102108
rhs = ex[2]
109+
110+
# Tuple elimination aims to turn assignments between tuples into lists of assignments.
111+
#
112+
# However, there's a complex interplay of side effects due to the
113+
# individual assignments and these can be surprisingly complicated to
114+
# model. For example `(x[i], y) = (f(), g)` can contain the following
115+
# surprises:
116+
# * `tmp = f()` calls `f` which might throw, or modify the bindings for
117+
# `x` or `y`.
118+
# * `x[i] = tmp` is lowered to `setindex!` which might throw or modify the
119+
# bindings for `x` or `y`.
120+
# * `g` might throw an `UndefVarError`
121+
#
122+
# Thus for correctness we introduce temporaries for all right hand sides
123+
# with observable side effects and ensure they're evaluated in order.
124+
n_lhs = numchildren(lhs)
125+
n_rhs = numchildren(rhs)
103126
stmts = SyntaxList(ctx)
104-
end_stmts = SyntaxList(ctx)
105-
elements = SyntaxList(ctx)
106-
assigned = SyntaxList(ctx)
127+
rhs_tmps = SyntaxList(ctx)
128+
for i in 1:n_rhs
129+
rh = rhs[i]
130+
r = if kind(rh) == K"..."
131+
rh[1]
132+
else
133+
rh
134+
end
135+
k = kind(r)
136+
if is_literal(k) || k == K"Symbol" || k == K"inert" || k == K"top" ||
137+
k == K"core" || k == K"Value"
138+
# Effect-free and nothrow right hand sides do not need a temporary
139+
# (we require nothrow because the order of rhs terms is observable
140+
# due to sequencing, thus identifiers are not allowed)
141+
else
142+
# Example rhs which need a temporary
143+
# * `f()` - arbitrary side effects to any binding
144+
# * `z` - might throw UndefVarError
145+
tmp = emit_assign_tmp(stmts, ctx, r)
146+
rh = kind(rh) == K"..." ? @ast(ctx, rh, [K"..." tmp]) : tmp
147+
end
148+
push!(rhs_tmps, rh)
149+
end
107150

108151
il = 0
109152
ir = 0
110-
while il < numchildren(lhs)
153+
while il < n_lhs
111154
il += 1
112155
ir += 1
113156
lh = lhs[il]
114157
if kind(lh) == K"..."
115-
TODO(lhs, "... in tuple lhs")
116-
n_lhs = numchildren(lhs)
117-
n_rhs = numchildren(rhs)
118-
if il == n_lhs
119-
# Simple case: exactly one `...` at end of lhs. Examples:
120-
# (x, ys...) = (a,b,c)
121-
# (ys...) = ()
122-
rhs_tmp = emit_assign_tmp(stmts, ctx,
123-
@ast(ctx, rhs, [K"tuple" rhs[ir:end]...]),
124-
"rhs_tmp"
125-
)
126-
push!(stmts, @ast ctx ex [K"=" lh[1] rhs_tmp])
127-
push!(elements, @ast ctx rhs_tmp [K"..." rhs_tmp])
128-
break
129-
else
130-
# Exactly one lhs `...` occurs in the middle somewhere, with a
131-
# general rhs which has one `...` term or at least as many
132-
# non-`...` terms.
133-
# Examples:
134-
# (x, ys..., z) = (a, b, c, d)
135-
# (x, ys..., z) = (a, bs...)
136-
# (xs..., y) = (a, bs...)
137-
# in this case we pairwise-match arguments from the end
138-
# backward, with rhs splats falling back to the general case.
139-
jl = n_lhs + 1
140-
jr = n_rhs + 1
141-
while jl > il && jr > ir
142-
if kind(lhs[jl-1]) == K"..." || kind(rhs[jr-1]) == K"..."
143-
break
144-
end
145-
jl -= 1
146-
jr -= 1
158+
# Exactly one lhs `...` occurs in the middle somewhere, with a
159+
# general rhs which has at least as many non-`...` terms or one
160+
# `...` term at the end.
161+
# Examples:
162+
# (x, ys..., z) = (a, b, c, d)
163+
# (x, ys..., z) = (a, bs...)
164+
# (xs..., y) = (a, bs...)
165+
# (xs...) = (a, b, c)
166+
# in this case we can pairwise-match arguments from the end
167+
# backward and emit a general tuple assignment for the middle.
168+
jl = n_lhs
169+
jr = n_rhs
170+
while jl > il && jr > ir
171+
if kind(lhs[jl]) == K"..." || kind(rhs_tmps[jr]) == K"..."
172+
break
147173
end
148-
rhs[jr]
174+
jl -= 1
175+
jr -= 1
149176
end
150-
continue
151-
end
152-
rh = rhs[ir] # In other cases `rhs[ir]` must exist
153-
if kind(rh) == K"..."
154-
@assert ir == numchildren(rhs) # _tuple_sides_match ensures this
155-
rh_tmp = emit_assign_tmp(stmts, ctx, rh[1])
156-
push!(end_stmts, @ast ctx ex [K"=" [K"tuple" lhs[il:end]...] rh_tmp])
157-
push!(elements, @ast ctx rh [K"..." rh_tmp])
158-
break
177+
middle = emit_assign_tmp(stmts, ctx,
178+
@ast(ctx, rhs, [K"tuple" rhs_tmps[ir:jr]...]),
179+
"rhs_tmp"
180+
)
181+
if il == jl
182+
# (x, ys...) = (a,b,c)
183+
# (x, ys...) = (a,bs...)
184+
# (ys...) = ()
185+
push!(stmts, @ast ctx ex [K"=" lh[1] middle])
186+
else
187+
# (x, ys..., z) = (a, b, c, d)
188+
# (x, ys..., z) = (a, bs...)
189+
# (xs..., y) = (a, bs...)
190+
push!(stmts, @ast ctx ex [K"=" [K"tuple" lhs[il:jl]...] middle])
191+
end
192+
# Continue with the remainder of the list of non-splat terms
193+
il = jl
194+
ir = jr
159195
else
160-
if is_identifier_like(lh) && is_effect_free(rh) &&
161-
!any(contains_identifier(rhs[j], lh) for j in ir+1:lastindex(rhs))
162-
!any(contains_identifier(a, rh) for a in assigned)
163-
# Overwrite `lh` directly if that won't cause conflicts with
164-
# other symbols
165-
push!(stmts, @ast ctx ex [K"=" lh rh])
166-
push!(assigned, lh)
167-
push!(elements, rh)
196+
rh = rhs_tmps[ir]
197+
if kind(rh) == K"..."
198+
push!(stmts, @ast ctx ex [K"=" [K"tuple" lhs[il:end]...] rh[1]])
199+
break
168200
else
169-
# In other cases we need a temporary and we'll overwrite `lh` at the end.
170-
tmp = ssavar(ctx, rh)
171-
push!(stmts, @ast ctx ex [K"=" tmp rh])
172-
# `push!(assigned, lh)` is not required when we assign `lh` later.
173-
push!(end_stmts, @ast ctx ex [K"=" lh tmp])
174-
push!(elements, tmp)
201+
push!(stmts, @ast ctx ex [K"=" lh rh])
175202
end
176203
end
177204
end
178205

179206
@ast ctx ex [K"block"
180207
stmts...
181-
end_stmts...
182-
[K"removable" [K"tuple" elements...]]
208+
[K"removable" [K"tuple" rhs_tmps...]]
183209
]
184210
end
185211

@@ -354,12 +380,7 @@ function expand_property_destruct(ctx, ex)
354380
@assert kind(params) == K"parameters"
355381
rhs = ex[2]
356382
stmts = SyntaxList(ctx)
357-
rhs1 = if is_ssa(ctx, rhs) || (is_identifier_like(rhs) &&
358-
!any(is_same_identifier_like(l, rhs) for l in children(params)))
359-
rhs
360-
else
361-
emit_assign_tmp(stmts, ctx, expand_forms_2(ctx, rhs))
362-
end
383+
rhs1 = emit_assign_tmp(stmts, ctx, expand_forms_2(ctx, rhs))
363384
for prop in children(params)
364385
propname = kind(prop) == K"Identifier" ? prop :
365386
kind(prop) == K"::" && kind(prop[1]) == K"Identifier" ? prop[1] :

JuliaLowering/src/eval.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ function to_lowered_expr(mod, ex, ssa_offset=0)
293293
k == K"opaque_closure_method" ? :opaque_closure_method :
294294
nothing
295295
if isnothing(head)
296-
TODO(ex, "Unhandled form for kind $k")
296+
throw(LoweringError(ex, "Unhandled form for kind $k"))
297297
end
298298
Expr(head, map(e->to_lowered_expr(mod, e, ssa_offset), children(ex))...)
299299
end

JuliaLowering/src/scope_analysis.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ function _find_scope_vars!(ctx, assignments, locals, destructured_args, globals,
5858
elseif kv == K"BindingId"
5959
binfo = lookup_binding(ctx, v)
6060
if !binfo.is_ssa && binfo.kind != :global
61-
TODO(v, "BindingId as function name")
61+
@assert false "allow local BindingId as function name?"
6262
end
6363
else
6464
@assert false

JuliaLowering/test/destructuring.jl

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,18 +92,61 @@ end
9292
end
9393

9494

95-
@testset "Tuples on both sides" begin
95+
@testset "Tuple elimination with tuples on both sides" begin
96+
97+
# Simple case
98+
@test JuliaLowering.include_string(test_mod, """
99+
let a = 1, b = 2
100+
(x,y) = (a,b)
101+
(x,y)
102+
end
103+
""") == (1, 2)
96104

97105
# lhs variable name in rhs
98106
@test JuliaLowering.include_string(test_mod, """
99-
let
100-
x = 1
101-
y = 2
107+
let x = 1, y = 2
102108
(x,y) = (y,x)
103109
(x,y)
104110
end
105111
""") == (2, 1)
106112

113+
# Slurps and splats
114+
115+
@test JuliaLowering.include_string(test_mod, """
116+
let a = 1, b = 2, c = 3
117+
(x, ys..., z) = (a, b, c)
118+
(x, ys, z)
119+
end
120+
""") == (1, (2,), 3)
121+
122+
@test JuliaLowering.include_string(test_mod, """
123+
let a = 1, b = 2, cs = (3,4)
124+
(x, ys...) = (a, b, cs...)
125+
(x, ys)
126+
end
127+
""") == (1, (2,3,4))
128+
129+
@test JuliaLowering.include_string(test_mod, """
130+
let a = 1, bs = (2,3), c = 4
131+
(x, ys...) = (a, bs..., c)
132+
(x, ys)
133+
end
134+
""") == (1, (2,3,4))
135+
136+
@test JuliaLowering.include_string(test_mod, """
137+
let a = 1, b = 2, cs = (3,4)
138+
(x, ys..., z) = (a, b, cs...)
139+
(x, ys, z)
140+
end
141+
""") == (1, (2,3), 4)
142+
143+
@test JuliaLowering.include_string(test_mod, """
144+
let a = 1
145+
(x, ys...) = (a,)
146+
(x, ys)
147+
end
148+
""") == (1, ())
149+
107150
# dotted rhs in last place
108151
@test JuliaLowering.include_string(test_mod, """
109152
let
@@ -112,6 +155,7 @@ let
112155
(x,y,z)
113156
end
114157
""") == (1, 2, 3)
158+
115159
# in value position
116160
@test JuliaLowering.include_string(test_mod, """
117161
let
@@ -120,6 +164,49 @@ let
120164
end
121165
""") == (1, 2, 3)
122166

167+
# Side effects in the right hand tuple can affect the previous left hand side
168+
# bindings, for example, `x`, below. In this case we need to ensure `f()` is
169+
# called before `x` is assigned the value from the right hand side.
170+
# (the flisp implementation fails this test.)
171+
@test JuliaLowering.include_string(test_mod, """
172+
let
173+
function f()
174+
x=100
175+
2
176+
end
177+
(x,y) = (1,f())
178+
x,y
179+
end
180+
""") == (1,2)
181+
182+
# `x` is not assigned and no side effect from `f()` happens when the right hand
183+
# side throws an UndefVarError
184+
@test JuliaLowering.include_string(test_mod, """
185+
let x=1, y=2, z=3, side_effect=false, a
186+
exc = try
187+
function f()
188+
side_effect=true
189+
end
190+
(x,y,z) = (100, a, f())
191+
catch e
192+
e
193+
end
194+
(x, y, z, side_effect, exc.var)
195+
end
196+
""") == (1, 2, 3, false, :a)
197+
198+
# Require that rhs is evaluated before any assignments, thus `x` is not defined
199+
# here because accessing `a` first throws an UndefVarError
200+
@test JuliaLowering.include_string(test_mod, """
201+
let x, y, a
202+
try
203+
(x, y) = (1, a)
204+
catch
205+
end
206+
@isdefined(x)
207+
end
208+
""") == false
209+
123210
end
124211

125212

0 commit comments

Comments
 (0)