Constant folding (or lack thereof)

I’ve read in several blog posts and here in the forum about the fact that Erlang supports constants folding. But it seems things are not that simple. Suppose you have this code (simplified from a real project):

defmodule MyModule2 do
  import Phoenix.HTML, only: [sigil_e: 2]
  defp operator_key(), do: "my-operator-key"
  defp value_key(), do: "my-value-key"

  def f() do
    ~e"<%= operator_key() %> - <%= value_key() %>}"
  end
end

When you compile and disassemble it, you get:

-file("lib/forage_web/experiments/experiments.ex", 44).

-module('Elixir.MyModule2').

-compile([no_auto_import]).

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

-spec '__info__'(attributes | compile | functions |
		 macros | md5 | module | deprecated) -> any().

'__info__'(module) -> 'Elixir.MyModule2';
'__info__'(functions) -> [{f, 0}];
'__info__'(macros) -> [];
'__info__'(Key = attributes) ->
    erlang:get_module_info('Elixir.MyModule2', Key);
'__info__'(Key = compile) ->
    erlang:get_module_info('Elixir.MyModule2', Key);
'__info__'(Key = md5) ->
    erlang:get_module_info('Elixir.MyModule2', Key);
'__info__'(deprecated) -> [].

f() ->
    {safe,
     [begin
	__@5 = [begin
		  __@1 = <<>>,
		  [__@1 | case operator_key() of
			    {safe, __@2} -> __@2;
			    __@3 when erlang:is_binary(__@3) ->
				'Elixir.Plug.HTML':html_escape_to_iodata(__@3);
			    __@4 -> 'Elixir.Phoenix.HTML.Safe':to_iodata(__@4)
			  end]
		end
		| <<" - ">>],
	[__@5 | case value_key() of
		  {safe, __@6} -> __@6;
		  __@7 when erlang:is_binary(__@7) ->
		      'Elixir.Plug.HTML':html_escape_to_iodata(__@7);
		  __@8 -> 'Elixir.Phoenix.HTML.Safe':to_iodata(__@8)
		end]
      end
      | <<"}">>]}.

operator_key() -> <<"my-operator-key">>.

value_key() -> <<"my-value-key">>.

The operator_key() and value_key() are constants, so the case statements here:

[__@1 | case operator_key() of
	{safe, __@2} -> __@2;
	__@3 when erlang:is_binary(__@3) ->
	'Elixir.Plug.HTML':html_escape_to_iodata(__@3);
	__@4 -> 'Elixir.Phoenix.HTML.Safe':to_iodata(__@4)
	end]

and here (both of which can be resolved at compile time):

[__@5 | case value_key() of
		{safe, __@6} -> __@6;
		__@7 when erlang:is_binary(__@7) ->
			'Elixir.Plug.HTML':html_escape_to_iodata(__@7);
		__@8 -> 'Elixir.Phoenix.HTML.Safe':to_iodata(__@8)
	end]

should hasve been optimized away. Why aren’t they? Is there anything I’m not seeing?

Aside: what motivated me to look at this? Well, I have some keys that are shared in the same module (and across modules, too) and I wanted to gather them into the same place. Unfortunately, even if the compiler optimizes the case statements way, it’s not reasonable to expect the the ~e sigil will be able to optimize away the 'Elixir.Plug.HTML':html_escape_to_iodata(X) calls at compile time :slight_smile: So I’ll just embed the constants in the string anyway.

1 Like

Can you use macros for this defmacro operator_key, do: "my-operator-key"?

As for your def operator_key not being inlined, I’d guess that’s because some to_safe calls happen at runtime, and the compiler can’t prove they are not necessary so it leaves them as is.

1 Like

Those are not constants. They are function calls, and only a handful of functions known to be free of side effects are considered when folding.

1 Like

Just to be clear, I’m not asking for a way of making this work. I’m much more interesting in why exactly the “obvious” optimization isn’t possible.

But if you use macros instead it gets even more weird:


-module('Elixir.MyModule2').

-compile([no_auto_import]).

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

-spec '__info__'(attributes | compile | functions |
		 macros | md5 | module | deprecated) -> any().

'__info__'(module) -> 'Elixir.MyModule2';
'__info__'(functions) -> [{f, 0}];
'__info__'(macros) -> [];
'__info__'(Key = attributes) ->
    erlang:get_module_info('Elixir.MyModule2', Key);
'__info__'(Key = compile) ->
    erlang:get_module_info('Elixir.MyModule2', Key);
'__info__'(Key = md5) ->
    erlang:get_module_info('Elixir.MyModule2', Key);
'__info__'(deprecated) -> [].

f() ->
    {safe,
     [begin
	__@5 = [begin
		  __@1 = <<>>,
		  [__@1 | case <<"my-operator-key">> of
			    {safe, __@2} -> __@2;
			    __@3 when erlang:is_binary(__@3) ->
				'Elixir.Plug.HTML':html_escape_to_iodata(__@3);
			    __@4 -> 'Elixir.Phoenix.HTML.Safe':to_iodata(__@4)
			  end]
		end
		| <<" - ">>],
	[__@5 | case <<"my-value-key">> of
		  {safe, __@6} -> __@6;
		  __@7 when erlang:is_binary(__@7) ->
		      'Elixir.Plug.HTML':html_escape_to_iodata(__@7);
		  __@8 -> 'Elixir.Phoenix.HTML.Safe':to_iodata(__@8)
		end]
      end
      | <<"}">>]}.

as you can see, the case statement now tries to match a literal binary and even with that clue it fails to pick the correct branch at compile time :slight_smile:

1 Like

This optimization might be possible, but quoting from Erlang compiler optimizations - Stack Overflow

nobody has bothered to implement it

1 Like

But shouldn’t it be obvious to the Erlang compiler that those functions have no side effects?

1 Like

lol, that probably explains it then :smile:

1 Like