Skip to content

Commit 2440f3b

Browse files
lukaszsamsonjosevalim
authored andcommitted
Fix incorrect narrowing in mixed type for into
Closes #15950. Closes #15952. Assisted-by: Codex:GPT-6
1 parent 0f39750 commit 2440f3b

2 files changed

Lines changed: 117 additions & 32 deletions

File tree

‎lib/elixir/lib/module/types/expr.ex‎

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -629,18 +629,16 @@ defmodule Module.Types.Expr do
629629
if :bitstring in into_kinds do
630630
intersection = opt_intersection(block_type, bitstring())
631631

632-
# TODO: currently if :into is a binary, then we expect the body
633-
# to return a binary, even if the binary is a gradual type. This
634-
# is ok for now because we only check for bitstring if the type
635-
# is a subset of empty_list() or bitstring(), but we may want to
636-
# relax in the future.
637-
if empty?(intersection) do
638-
error = {:badbitbody, block_type, block, context}
639-
{error_type(), error(__MODULE__, error, meta, stack, context)}
640-
else
632+
# On the binary path the body must be a bitstring. If the collectable
633+
# is compatible with a list, that path may never happen, so we accept.
634+
if compatible?(block_type, bitstring()) or
635+
(:non_empty_list in into_kinds and compatible?(into_type, empty_list())) do
641636
# If into_type is a binary but the block is a bitstring_no_binary(),
642637
# then the result may be a bitstring_no_binary().
643-
{opt_union(intersection, result_type), context}
638+
{opt_union(intersection, opt_union(result_type, binary())), context}
639+
else
640+
error = {:badbitbody, block_type, block, context}
641+
{error_type(), error(__MODULE__, error, meta, stack, context)}
644642
end
645643
else
646644
{result_type, context}
@@ -863,19 +861,23 @@ defmodule Module.Types.Expr do
863861
# We use subtype? instead of compatible because we want to handle
864862
# only bitstring/list, even if a dynamic with something else is given.
865863
if subtype?(type, @into_compile) do
866-
cond do
867-
bitstring_type?(type) ->
868-
kinds = if empty_list_type?(type), do: [:bitstring, :non_empty_list], else: [:bitstring]
869-
# A comprehension may concatenate the block an arbitrary number of times.
870-
# Even if both the initial value and each block are unaligned bitstrings,
871-
# repeated concatenation may eventually produce an aligned binary.
872-
{kinds, opt_union(binary(), type), bitstring(), context}
873-
874-
empty_list_type?(type) ->
864+
# A comprehension may concatenate the block an arbitrary number of times.
865+
# Even if both the initial value and each block are unaligned bitstrings,
866+
# repeated concatenation may eventually produce an aligned binary.
867+
case {bitstring_type?(type), empty_list_type?(type)} do
868+
# The collectable may be a list, which accepts any element, so the
869+
# body is not restricted. The caller checks the binary path.
870+
{true, true} ->
871+
{[:bitstring, :non_empty_list], type, term(), context}
872+
873+
{true, false} ->
874+
{[:bitstring], type, bitstring(), context}
875+
876+
{false, true} ->
875877
{[:non_empty_list], type, maybe_list_hd_or_term(expected), context}
876878

877879
# The type is empty...
878-
true ->
880+
{false, false} ->
879881
{[], type, term(), context}
880882
end
881883
else

‎lib/elixir/test/elixir/module/types/expr_test.exs‎

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3576,6 +3576,22 @@ defmodule Module.Types.ExprTest do
35763576
assert typecheck!([enum, other], for(x <- enum, do: x, into: other)) == dynamic()
35773577
end
35783578

3579+
test ":into bitstrings" do
3580+
assert typecheck!([items], for(_ <- items, into: <<0::4>>, do: <<1::4>>)) == bitstring()
3581+
3582+
assert typecheck!(
3583+
[items],
3584+
(
3585+
bits = for _ <- items, into: <<0::4>>, do: <<1::4>>
3586+
3587+
case bits do
3588+
x when is_binary(x) -> :binary
3589+
_ -> :bits
3590+
end
3591+
)
3592+
) == atom([:binary, :bits])
3593+
end
3594+
35793595
test ":into unions" do
35803596
assert typecheck!(
35813597
[binary],
@@ -3595,22 +3611,18 @@ defmodule Module.Types.ExprTest do
35953611
dynamic(
35963612
opt_union(opt_union(bitstring(), empty_list()), list(bitstring_no_binary()))
35973613
)
3598-
end
3599-
3600-
test ":into bitstrings" do
3601-
assert typecheck!([items], for(_ <- items, into: <<0::4>>, do: <<1::4>>)) == bitstring()
36023614

36033615
assert typecheck!(
3604-
[items],
3616+
[value],
36053617
(
3606-
bits = for _ <- items, into: <<0::4>>, do: <<1::4>>
3607-
3608-
case bits do
3609-
x when is_binary(x) -> :binary
3610-
_ -> :bits
3611-
end
3618+
into = if :rand.uniform() > 0.5, do: [], else: ""
3619+
for(_ <- [1], do: value, into: into)
3620+
)
3621+
) ==
3622+
opt_union(
3623+
dynamic(opt_union(bitstring_no_binary(), non_empty_list(term()))),
3624+
opt_union(binary(), empty_list())
36123625
)
3613-
) == atom([:binary, :bits])
36143626
end
36153627

36163628
test ":into inference" do
@@ -3647,6 +3659,77 @@ defmodule Module.Types.ExprTest do
36473659
# from: types_test.ex:LINE
36483660
<<x>>
36493661
"""
3662+
3663+
# In static mode the body must be a bitstring whenever the target may be a binary
3664+
assert typeerror!(
3665+
[flag],
3666+
(
3667+
into = if flag, do: [], else: ""
3668+
value = if flag, do: :ok, else: "ok"
3669+
for(_ <- [1], do: value, into: into)
3670+
)
3671+
) =~ ~l"""
3672+
expected the body of a for-comprehension with into: binary() (or bitstring()) to be a binary (or bitstring):
3673+
3674+
value
3675+
3676+
but got type:
3677+
3678+
:ok or binary()
3679+
"""
3680+
3681+
assert typeerror!(
3682+
[flag],
3683+
(
3684+
into = if flag, do: [], else: ""
3685+
for(_ <- [1], do: :ok, into: into)
3686+
)
3687+
) =~ ~l"""
3688+
expected the body of a for-comprehension with into: binary() (or bitstring()) to be a binary (or bitstring):
3689+
3690+
:ok
3691+
3692+
but got type:
3693+
3694+
:ok
3695+
"""
3696+
3697+
assert typeerror!(
3698+
[flag],
3699+
(
3700+
value = if flag, do: :ok, else: "ok"
3701+
for(_ <- [1], do: value, into: "")
3702+
)
3703+
) =~ ~l"""
3704+
expected the body of a for-comprehension with into: binary() (or bitstring()) to be a binary (or bitstring):
3705+
3706+
value
3707+
3708+
but got type:
3709+
3710+
:ok or binary()
3711+
"""
3712+
end
3713+
3714+
test ":into dynamic" do
3715+
# In dynamic mode the list path may succeed, so the body is not restricted
3716+
# to bitstrings and the list result keeps the full element type
3717+
assert typedyn!(
3718+
[flag],
3719+
(
3720+
into = if flag, do: [], else: ""
3721+
value = if flag, do: :ok, else: "ok"
3722+
for(_ <- [1], do: value, into: into)
3723+
)
3724+
) == dynamic(opt_union(binary(), list(opt_union(atom([:ok]), binary()))))
3725+
3726+
assert typedyn!(
3727+
[flag],
3728+
(
3729+
into = if flag, do: [], else: ""
3730+
for(_ <- [1], do: :ok, into: into)
3731+
)
3732+
) == dynamic(opt_union(binary(), list(atom([:ok]))))
36503733
end
36513734

36523735
test ":into with non-returning collectable" do

0 commit comments

Comments
 (0)