From c24c23538d521d25edd6a9a7a66fc5206caab70e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 27 Sep 2026 12:19:40 +0200 Subject: [PATCH] Fix incorrect narrowing in mixed type for into --- lib/elixir/lib/module/types/expr.ex | 19 ++++++---- .../test/elixir/module/types/expr_test.exs | 36 +++++++++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index d6a4da5813..74bebbff6c 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -634,7 +634,10 @@ defmodule Module.Types.Expr do # is ok for now because we only check for bitstring if the type # is a subset of empty_list() or bitstring(), but we may want to # relax in the future. - if empty?(intersection) do + # + # If the collectable may also be a list, the body may be valid + # on that path, so we only error when the list path is impossible. + if empty?(intersection) and :non_empty_list not in into_kinds do error = {:badbitbody, block_type, block, context} {error_type(), error(__MODULE__, error, meta, stack, context)} else @@ -864,12 +867,16 @@ defmodule Module.Types.Expr do # only bitstring/list, even if a dynamic with something else is given. if subtype?(type, @into_compile) do cond do + # A comprehension may concatenate the block an arbitrary number of times. + # Even if both the initial value and each block are unaligned bitstrings, + # repeated concatenation may eventually produce an aligned binary. + bitstring_type?(type) and empty_list_type?(type) -> + # The collectable may be a list, which accepts any element, + # so we cannot restrict the body to bitstrings. + {[:bitstring, :non_empty_list], opt_union(binary(), type), term(), context} + bitstring_type?(type) -> - kinds = if empty_list_type?(type), do: [:bitstring, :non_empty_list], else: [:bitstring] - # A comprehension may concatenate the block an arbitrary number of times. - # Even if both the initial value and each block are unaligned bitstrings, - # repeated concatenation may eventually produce an aligned binary. - {kinds, opt_union(binary(), type), bitstring(), context} + {[:bitstring], opt_union(binary(), type), bitstring(), context} empty_list_type?(type) -> {[:non_empty_list], type, maybe_list_hd_or_term(expected), context} diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 8cb2a6a0be..41d8563915 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -3586,6 +3586,42 @@ defmodule Module.Types.ExprTest do dynamic( opt_union(opt_union(bitstring(), empty_list()), list(bitstring_no_binary())) ) + + # The list path accepts any element, so the body is not restricted to bitstrings + assert typecheck!( + [flag], + ( + into = if flag, do: [], else: "" + value = if flag, do: :ok, else: "ok" + for(_ <- [1], do: value, into: into) + ) + ) == opt_union(binary(), list(opt_union(atom([:ok]), binary()))) + + assert typedyn!( + [flag], + ( + into = if flag, do: [], else: "" + value = if flag, do: :ok, else: "ok" + for(_ <- [1], do: value, into: into) + ) + ) == dynamic(opt_union(binary(), list(opt_union(atom([:ok]), binary())))) + + assert typecheck!( + [flag], + ( + into = if flag, do: [], else: "" + for(_ <- [1], do: :ok, into: into) + ) + ) == opt_union(binary(), list(atom([:ok]))) + + assert typecheck!( + [flag, value], + ( + into = if flag, do: [], else: "" + for(_ <- [1], do: value, into: into) + value + ) + ) == dynamic() end test ":into bitstrings" do