A :uniq for within a :uniq for

Given that the expression

for _ <- 1..3, uniq: true do
  [nil]
end

evaluates to [[nil]], shouldn’t the “equivalent” expression

for _ <- 1..3, uniq: true do
  for _ <- 1..3, uniq: true do
    nil
  end
end

also evaluate to [[nil]]?

Instead I get the following exception:

** (MatchError) no match of right hand side value: {[nil], %{nil: true}}

The following iex session also illustrates this question:

Interactive Elixir (1.13.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> for _ <- 1..3 do
...(1)>   nil
...(1)> end
[nil, nil, nil]
iex(2)> for _ <- 1..3, uniq: true do
...(2)>   nil
...(2)> end
[nil]
iex(3)> for _ <- 1..3 do
...(3)>   [nil]
...(3)> end
[[nil], [nil], [nil]]
iex(4)> for _ <- 1..3, uniq: true do
...(4)>   [nil]
...(4)> end
[[nil]]
iex(5)> for _ <- 1..3 do
...(5)>   for _ <- 1..3, uniq: true do
...(5)>     nil
...(5)>   end
...(5)> end
[[nil], [nil], [nil]]
iex(6)> for _ <- 1..3, uniq: true do
...(6)>   for _ <- 1..3, uniq: true do
...(6)>     nil
...(6)>   end
...(6)> end
** (MatchError) no match of right hand side value: {[nil], %{nil: true}}
    (stdlib 3.17) erl_eval.erl:450: :erl_eval.expr/5
    (stdlib 3.17) erl_eval.erl:123: :erl_eval.exprs/5
    (elixir 1.13.1) lib/enum.ex:4136: Enum.reduce_range/5
iex(6)>

Thank you in advance

3 Likes

Looks like a bug to me.
It happens when two nested :uniq are given.

for x <- 1..3, uniq: true  do
  for y <- 4..6, uniq: true do
    {x, y}
  end
end

I disassembled the generated BEAM code and there’s something odd. Here’s the code:

{:module, Foo, bytecode, _} = defmodule Foo do
  def foo do
    for _ <- 1..3, uniq: true do
      for _ <- 1..3, uniq: true do
        nil
      end
    end
  end
end

:beam_disasm.file(bytecode) |> IO.inspect(limit: :infinity)

Trimming out the unimportant bits (module_info etc), gives this output (annotations after # far to the right):

   {:function, :foo, 0, 9,
    [
      {:line, 1},
      {:label, 8},
      {:func_info, {:atom, Foo}, {:atom, :foo}, 0},
      {:label, 9},
      {:allocate_heap, 0, {:alloc, [words: 0, floats: 0, funs: 1]}, 0},
      {:make_fun3, {Foo, :"-foo/0-fun-1-", 2}, 0, 61617801, {:x, 2},
       {:list, []}},
      {:move, {:literal, {[], %{}}}, {:x, 1}},
      {:move, {:literal, 1..3}, {:x, 0}},
      {:line, 2},
      {:call_ext, 3, {:extfunc, Enum, :reduce, 3}},
      {:bif, :element, {:f, 0}, [integer: 1, x: 0], {:x, 0}},
      {:call_ext_last, 1, {:extfunc, :lists, :reverse, 1}, 0}
    ]},
   {:function, :"-foo/0-fun-1-", 2, 15,                  # reducer called from foo
    [
      {:line, 2},
      {:label, 14},
      {:func_info, {:atom, Foo}, {:atom, :"-foo/0-fun-1-"}, 2},
      {:label, 15},
      {:test, :is_tuple, {:f, 18}, [x: 1]},
      {:test, :test_arity, {:f, 18}, [{:x, 1}, 2]},
      {:allocate_heap, 3, {:alloc, [words: 3, floats: 0, funs: 1]}, 2},
      {:move, {:x, 1}, {:y, 2}},
      {:get_tuple_element, {:x, 1}, 0, {:y, 1}},                        # unpack tuple in x1 - intoAcc in y1
      {:get_tuple_element, {:x, 1}, 1, {:y, 0}},                        #                      uniqAcc in y0
      {:make_fun3, {Foo, :"-foo/0-fun-0-", 5}, 1, 61617801, {:x, 2},
       {:list, [x: 1, y: 0, y: 1]}},                                    # make closure in x2
      {:move, {:literal, {[], %{}}}, {:x, 1}},                          # x1 gets {[], %{}}
      {:move, {:literal, 1..3}, {:x, 0}},                               # x0 gets 1..3
      {:line, 3},
      {:call_ext, 3, {:extfunc, Enum, :reduce, 3}},                     # call reduce
      {:bif, :element, {:f, 0}, [integer: 1, x: 0], {:x, 0}},           # unpack result tuple (?)
      {:call_ext, 1, {:extfunc, :lists, :reverse, 1}},                  # reverse intoAcc
      {:test, :is_map, {:f, 17}, [y: 0]},                               # bail if the result is not a map (?)
      {:get_map_elements, {:f, 16}, {:y, 0}, {:list, [x: 0, x: 1]}},    # %{^x1 => map_value}
      {:test, :is_eq_exact, {:f, 16}, [x: 1, atom: true]},              # return original acc tuple if map_value is true
      {:move, {:y, 2}, {:x, 0}},
      {:deallocate, 3},
      :return,
      {:label, 16},
      {:put_map_assoc, {:f, 0}, {:y, 0}, {:x, 1}, 1,                    # put x1 => true in uniqAcc
       {:list, [x: 0, atom: true]}},
      {:test_heap, 5, 2},
      {:put_list, {:x, 0}, {:y, 1}, {:x, 0}},
      {:put_tuple2, {:x, 0}, {:list, [x: 0, x: 1]}},                    # build new result tuple
      {:deallocate, 3},
      :return,
      {:label, 17},
      {:line, 2},
      {:case_end, {:y, 0}},
      {:label, 18},
      {:badmatch, {:x, 1}}
    ]},
   {:function, :"-foo/0-fun-0-", 5, 20,
    [
      {:line, 3},
      {:label, 19},
      {:func_info, {:atom, Foo}, {:atom, :"-foo/0-fun-0-"}, 5},
      {:label, 20},
      {:test, :is_tuple, {:f, 23}, [x: 1]},
      {:test, :test_arity, {:f, 23}, [{:x, 1}, 2]},
      {:get_tuple_element, {:x, 1}, 0, {:x, 0}},                       # unpack inner reduce accumulator
      {:test, :is_eq_exact, {:f, 23}, [x: 0, x: 4]},                   # badmatch if it doesn't match the value from the closure environment (????)
      {:get_tuple_element, {:x, 1}, 1, {:x, 0}},
      {:test, :is_eq_exact, {:f, 23}, [x: 0, x: 3]},
      {:test, :is_map, {:f, 22}, [x: 3]},
      {:get_map_elements, {:f, 21}, {:x, 3}, {:list, [atom: nil, x: 0]}},
      {:test, :is_eq_exact, {:f, 21}, [x: 0, atom: true]},
      {:move, {:x, 2}, {:x, 0}},
      :return,
      {:label, 21},
      {:put_map_assoc, {:f, 0}, {:x, 3}, {:x, 0}, 5,
       {:list, [atom: nil, atom: true]}},
      {:test_heap, 5, 5},
      {:put_list, {:atom, nil}, {:x, 4}, {:x, 1}},
      {:put_tuple2, {:x, 0}, {:list, [x: 1, x: 0]}},
      :return,
      {:label, 22},
      {:case_end, {:x, 3}},
      {:label, 23},
      {:badmatch, {:x, 1}}
    ]}

The inner fun seems to be checking the value of the outer loop’s accumulators versus the inner loop (and if they disagree, jumps to label 23 and throws a bad match error). :thinking:

I don’t see any corresponding structure in the relevant-looking bit of the compiler:

Maybe the optimizer is breaking the nested loops somehow?

2 Likes

FWIW, I provided a PR with a fix:

Thanks, @al2o3cr for narrowing the issue down to outer/inner scopes name clash.

7 Likes