How to avoid Dialyzer complain about feature flags?

I’m trying to understand how to make Dialyzer happy :slight_smile:

If I following code

defmodule Test do
  @flag1 false

  def init() do
    IO.puts "Execute always"
    if @flag1 do
      IO.puts "Execute in feature flag"
    end
  end
end

Dialyzer complains that

lib/test.ex:6: The pattern 'true' can never match the type 'false'

I had the impression that Elixir will not compile unreachable code to beam files.
But if does not, here is the disassembly:

-file("lib/test.ex", 1).

-module('Elixir.Test').

-compile(no_auto_import).

-export(['__info__'/1, init/0]).

-spec '__info__'(attributes | compile | exports |
                 functions | macros | md5 | module) -> atom() |
                                                       [{atom(), any()} |
                                                        {atom(), byte(),
                                                         integer()}].

'__info__'(functions) -> [{init, 0}];
'__info__'(macros) -> [];
'__info__'(info) ->
    erlang:get_module_info('Elixir.Test', info).

init() ->
    'Elixir.IO':puts(<<"Execute always">>),
    case false of
      false -> nil;
      true -> 'Elixir.IO':puts(<<"Execute in feature flag">>)
    end.

Any way to remove it somehow? :slight_smile:

defmacro feature(feature_name, [do: block]) do
  if Module.get_attribute(__CALLER__.module, feature_name) do
    block
  end
end

This is a rough draft, but should work. You need to pass in the name of the feature as an atom though and it will look up the appropriate module attribute. So on your example you need to specify @flag1 and use :flag1 in the macro invocation.

Thank you!
I came up with less elegant version of macro, but it also worked.

But there is a catch - when flag does not exists, macro is replaced with nil which changes function return code.

Here is a disassembly when flag is turned off

init() ->
    'Elixir.IO':puts(<<"Execute always">>), nil.

I wonder if it is possible to make macro return no code at all.

No you can’t.

A mcaro will always inject code. But if you want to return the result of the gated block if the feature is active, what do you want to return if it is not? For me this feels like undefined behaviour.

Of course you can expand the macro a bit to have an else as well:

defmacro feature(name, clauses), do: build_feature(name, __CALLER__.module, clauses)

defp build_feature(feature_name, module, [do: do_block], do: build_feature(feature_name, module, do: do_block, else: nil)
defp build_feature(feature_name, module, [do: do_block, else: else_block]) do
  if Module.get_attribute(module, feature_name) do
    do_block
  else
    else_block
  end
end

Again, this is a very rough draft, and may fail to compile/work.

You are totally right here.

Anyway, I still have question, why Elixir generates code which is guaranteed to be dead :slight_smile:

AFAIK elixir just compiles everything to some erlang. Depending on the MIX_ENV some erl compiler flags are choosen which then do further optimisations.

So changing the MIX_ENV to prod might do some further optimisations. In dev usually one does usually compile with out or only shallow optimisations to safe time on recompile.

So it may be that the actual example you have shown might be gone away in actual BEAM-assembly (you have shown erlang IR as far as I can tell).

1 Like

I’ve tried also production mode.

That’s very interesting idea that may be I’m looking on IR. I’ll look on disassembly too.

Okkay, even when there is case present in IR, compiler does good job of stripping it away :slight_smile:

  {:function, :init, 0, 19,
   [{:line, 5}, {:label, 18}, {:func_info, {:atom, Test}, {:atom, :init}, 0},
    {:label, 19}, {:allocate, 0, 0},
    {:move, {:literal, "Execute always"}, {:x, 0}}, {:line, 6},
    {:call_ext, 1, {:extfunc, IO, :puts, 1}}, {:move, {:integer, 123}, {:x, 0}},
    {:deallocate, 0}, :return]},
1 Like

Well, just to conclude this post with solution

defmodule FeatureFlags do
  defmacro __using__(_args) do
    quote do
      import FeatureFlags, only: [feature_flag: 2]
    end
  end

  defmacro feature_flag(condition, do: block) do
    {condition_result, _} = Code.eval_quoted(condition, [], __CALLER__)
    if condition_result do
      block
    else
      nil
    end
  end
end

use in your code as

defmodule Sample do
   @flag1 true
   @debug_level 0

   def init() do
     feature_flag @flag1 do
        IO.puts "flag is enabled"
     end

     feature_flag @debug_level > 10 do
        IO.puts "extensive debug message"
     end
   end
end

PS. does not support else branch for feature flag yet.