Comprehension syntax

Yes they are the same. A do block after a call adds the [do: ...] argument to the function/macro call on the left.

So if you call this (notice the parentheses for the if call)

if(a == 1) do
  :ok
end

The compiler kind of rewrites it to if(a == 1, [do: :ok])

This works for functions too:

defmodule Test do
  def myfun(value, block) do
    value |> IO.inspect(label: "value")
    block |> IO.inspect(label: "block")
  end
end

Test.myfun :some_value do
  a = 1
  b = 2
  a + b
end

This will output the following:

value: :some_value
block: [do: 3]

But as myfun/2 is a function and not a macro, the block is evaluated before being passed to the function, hence you see [do: 3], whereas with macros the quoted block is passed. This syntactic sugar is then less useful for functions.

The for macro is special, because if you pass into: %{}, do: stuff(), the do and into are in the same keyword list ; if you pass into: %{} and then a do end block, the compiler will add another argument to the for() call, containing just the do block:

iex(1)> quote do
...(1)> for a <- 1..2, into: %{} do
...(1)> {a, a*a} 
...(1)> end
...(1)> end
{:for, [],
 [
   {:<-, [],
    [{:a, [], Elixir}, {:.., [context: Elixir, import: Kernel], [1, 2]}]},
   [into: {:%{}, [], []}],
   [
     do: {{:a, [], Elixir},
      {:*, [context: Elixir, import: Kernel],
       [{:a, [], Elixir}, {:a, [], Elixir}]}}
   ]
 ]}
iex(2)> quote do                   
...(2)> for a <- 1..2, into: %{}, do: {a, a*a}
...(2)> end
{:for, [],
 [
   {:<-, [],
    [{:a, [], Elixir}, {:.., [context: Elixir, import: Kernel], [1, 2]}]},
   [
     into: {:%{}, [], []},
     do: {{:a, [], Elixir},
      {:*, [context: Elixir, import: Kernel],
       [{:a, [], Elixir}, {:a, [], Elixir}]}}
   ]
 ]}

I guess that is why for is defined in Kernel.SpecialForms as an error because it is handled at the compiler level.