Gradualizer icon indicating copy to clipboard operation
Gradualizer copied to clipboard

dialyzer source code module attribute support

Open tim2CF opened this issue 6 years ago • 3 comments
trafficstars

Hello! I would like to discuss possibility of dialyzer source code module attribute support in gradualizer. The reason for this are some places, for example in elixir core, like this:

https://github.com/elixir-lang/elixir/blob/a6da9e59b73877b60b97957742552f904b2ced1b/lib/elixir/lib/protocol.ex#L704-L706

which explicitly says us - hey, we know this place is not safe, and we know what we are doing. Of course this is compromise, but probably this make sense? At the moment gradualizer says

The pattern #{'__struct__' := __@1} on line 1 doesn't have the type term()
The clause on line 1 cannot be reached

probably because it can't handle dialyzer module attributes

tim2CF avatar Mar 26 '19 14:03 tim2CF

Hi @tim2CF

  • we were thinking about adding gradualizer specific attributes.

  • in the general case I imagine dialyzer options are not really applicable for gradualizer. however in the special case of :nowarn_function it makes sense, and could result in Gradualizer just skipping checking that function all together.

  • if Gradualizer would parse and understand a few dialyzer attributes that would reduce a bit of noise and duplication in the code. Although in the general case it would be good not to mix the two and gradualizer to have its separate module-inlined options.

  • in this particular example any pattern should be a subtype of term() so there must be some other error, I suspect a bug in Gradualizer. Did you run Gradualizer on the Protocol module itself or could you post a minimal Elixir code example that triggers the same error?

gomoripeti avatar Mar 26 '19 14:03 gomoripeti

Hi again @gomoripeti Thanks for response! This bug appears if I'm trying to define any custom protocol, so minimal Elixir code is

defprotocol Foo do
  def bar(_)
end

If we translate generated BEAM file to Erlang, probably problematic place is

-spec impl_for(term()) -> atom() | nil.

impl_for(#{'__struct__' := __@2 = __@1})
    when erlang:is_atom(__@2) ->
    struct_impl_for(__@1);

Full snipped of decompiled Erlang code is

here

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

-module('Elixir.Foo').

-compile([no_auto_import, debug_info,
	  {inline, [{struct_impl_for, 1}]}]).

-callback bar(t()) -> term().

-spec 'impl_for!'(term()) -> atom().

-spec impl_for(term()) -> atom() | nil.

-spec '__protocol__'(module) -> 'Elixir.Foo';
		    (functions) -> [{bar, 1}, ...];
		    ('consolidated?') -> boolean();
		    (impls) -> not_consolidated |
			       {consolidated, [module()]}.

-export_type([t/0]).

-type t() :: term().

-dialyzer({nowarn_function,
	   [{'__protocol__', 1}, {impl_for, 1},
	    {'impl_for!', 1}]}).

-protocol([{fallback_to_any, false}]).

-export(['__info__'/1, '__protocol__'/1, bar/1,
	 impl_for/1, 'impl_for!'/1]).

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

'__info__'(module) -> 'Elixir.Foo';
'__info__'(functions) ->
    [{'__protocol__', 1}, {bar, 1}, {impl_for, 1},
     {'impl_for!', 1}];
'__info__'(macros) -> [];
'__info__'(Key = attributes) ->
    erlang:get_module_info('Elixir.Foo', Key);
'__info__'(Key = compile) ->
    erlang:get_module_info('Elixir.Foo', Key);
'__info__'(Key = md5) ->
    erlang:get_module_info('Elixir.Foo', Key);
'__info__'(deprecated) -> [].

'__protocol__'(module) -> 'Elixir.Foo';
'__protocol__'(functions) -> [{bar, 1}];
'__protocol__'('consolidated?') -> false;
'__protocol__'(impls) -> not_consolidated.

bar(__@1) -> ('impl_for!'(__@1)):bar(__@1).

impl_for(#{'__struct__' := __@2 = __@1})
    when erlang:is_atom(__@2) ->
    struct_impl_for(__@1);
impl_for(__@1) when erlang:is_tuple(__@1) ->
    case case
	   'Elixir.Code':'ensure_compiled?'('Elixir.Foo.Tuple')
	     of
	   false -> false;
	   true ->
	       erlang:function_exported('Elixir.Foo.Tuple', '__impl__',
					1);
	   __@2 -> erlang:error({badbool, 'and', __@2})
	 end
	of
      true -> 'Elixir.Foo.Tuple':'__impl__'(target);
      false -> nil
    end;
impl_for(__@1) when erlang:is_atom(__@1) ->
    case case
	   'Elixir.Code':'ensure_compiled?'('Elixir.Foo.Atom')
	     of
	   false -> false;
	   true ->
	       erlang:function_exported('Elixir.Foo.Atom', '__impl__',
					1);
	   __@2 -> erlang:error({badbool, 'and', __@2})
	 end
	of
      true -> 'Elixir.Foo.Atom':'__impl__'(target);
      false -> nil
    end;
impl_for(__@1) when erlang:is_list(__@1) ->
    case case
	   'Elixir.Code':'ensure_compiled?'('Elixir.Foo.List')
	     of
	   false -> false;
	   true ->
	       erlang:function_exported('Elixir.Foo.List', '__impl__',
					1);
	   __@2 -> erlang:error({badbool, 'and', __@2})
	 end
	of
      true -> 'Elixir.Foo.List':'__impl__'(target);
      false -> nil
    end;
impl_for(__@1) when erlang:is_map(__@1) ->
    case case
	   'Elixir.Code':'ensure_compiled?'('Elixir.Foo.Map')
	     of
	   false -> false;
	   true ->
	       erlang:function_exported('Elixir.Foo.Map', '__impl__',
					1);
	   __@2 -> erlang:error({badbool, 'and', __@2})
	 end
	of
      true -> 'Elixir.Foo.Map':'__impl__'(target);
      false -> nil
    end;
impl_for(__@1) when erlang:is_bitstring(__@1) ->
    case case
	   'Elixir.Code':'ensure_compiled?'('Elixir.Foo.BitString')
	     of
	   false -> false;
	   true ->
	       erlang:function_exported('Elixir.Foo.BitString',
					'__impl__', 1);
	   __@2 -> erlang:error({badbool, 'and', __@2})
	 end
	of
      true -> 'Elixir.Foo.BitString':'__impl__'(target);
      false -> nil
    end;
impl_for(__@1) when erlang:is_integer(__@1) ->
    case case
	   'Elixir.Code':'ensure_compiled?'('Elixir.Foo.Integer')
	     of
	   false -> false;
	   true ->
	       erlang:function_exported('Elixir.Foo.Integer',
					'__impl__', 1);
	   __@2 -> erlang:error({badbool, 'and', __@2})
	 end
	of
      true -> 'Elixir.Foo.Integer':'__impl__'(target);
      false -> nil
    end;
impl_for(__@1) when erlang:is_float(__@1) ->
    case case
	   'Elixir.Code':'ensure_compiled?'('Elixir.Foo.Float')
	     of
	   false -> false;
	   true ->
	       erlang:function_exported('Elixir.Foo.Float', '__impl__',
					1);
	   __@2 -> erlang:error({badbool, 'and', __@2})
	 end
	of
      true -> 'Elixir.Foo.Float':'__impl__'(target);
      false -> nil
    end;
impl_for(__@1) when erlang:is_function(__@1) ->
    case case
	   'Elixir.Code':'ensure_compiled?'('Elixir.Foo.Function')
	     of
	   false -> false;
	   true ->
	       erlang:function_exported('Elixir.Foo.Function',
					'__impl__', 1);
	   __@2 -> erlang:error({badbool, 'and', __@2})
	 end
	of
      true -> 'Elixir.Foo.Function':'__impl__'(target);
      false -> nil
    end;
impl_for(__@1) when erlang:is_pid(__@1) ->
    case case
	   'Elixir.Code':'ensure_compiled?'('Elixir.Foo.PID')
	     of
	   false -> false;
	   true ->
	       erlang:function_exported('Elixir.Foo.PID', '__impl__',
					1);
	   __@2 -> erlang:error({badbool, 'and', __@2})
	 end
	of
      true -> 'Elixir.Foo.PID':'__impl__'(target);
      false -> nil
    end;
impl_for(__@1) when erlang:is_port(__@1) ->
    case case
	   'Elixir.Code':'ensure_compiled?'('Elixir.Foo.Port')
	     of
	   false -> false;
	   true ->
	       erlang:function_exported('Elixir.Foo.Port', '__impl__',
					1);
	   __@2 -> erlang:error({badbool, 'and', __@2})
	 end
	of
      true -> 'Elixir.Foo.Port':'__impl__'(target);
      false -> nil
    end;
impl_for(__@1) when erlang:is_reference(__@1) ->
    case case
	   'Elixir.Code':'ensure_compiled?'('Elixir.Foo.Reference')
	     of
	   false -> false;
	   true ->
	       erlang:function_exported('Elixir.Foo.Reference',
					'__impl__', 1);
	   __@2 -> erlang:error({badbool, 'and', __@2})
	 end
	of
      true -> 'Elixir.Foo.Reference':'__impl__'(target);
      false -> nil
    end;
impl_for(_) -> nil.

'impl_for!'(__@1) ->
    case impl_for(__@1) of
      __@2 when __@2 =:= nil orelse __@2 =:= false ->
	  erlang:error('Elixir.Protocol.UndefinedError':exception([{protocol,
								    'Elixir.Foo'},
								   {value,
								    __@1}]));
      __@3 -> __@3
    end.

struct_impl_for(__@1) ->
    __@2 = 'Elixir.Module':concat('Elixir.Foo', __@1),
    case case 'Elixir.Code':'ensure_compiled?'(__@2) of
	   false -> false;
	   true -> erlang:function_exported(__@2, '__impl__', 1);
	   __@3 -> erlang:error({badbool, 'and', __@3})
	 end
	of
      true -> __@2:'__impl__'(target);
      false -> nil
    end.

tim2CF avatar Mar 26 '19 15:03 tim2CF

thanks for the example, I have a look into it

gomoripeti avatar Mar 26 '19 17:03 gomoripeti