[Feature Request] Provide compile error or warning on undefined remote function calls

111 views
Skip to first unread message

Myron Marston

unread,
May 6, 2016, 12:04:05 AM5/6/16
to elixir-lang-core

Given the following module definition:

defmodule MyMod do
  def fun_1 do
    0
  end

  def fun_2 do
    fon_1 + 1
  end
end

…Elixir provides a nice compile-time error informing me that MyMod.fon_1/1 does not exist:

** (CompileError) test/foo_test.exs:7: undefined function fon_1/0
    (stdlib) lists.erl:1337: :lists.foreach/2
    (stdlib) erl_eval.erl:669: :erl_eval.do_apply/6
    (stdlib) erl_eval.erl:122: :erl_eval.exprs/5

But if I call an undefined remote function:

defmodule MyMod do
  def fun_1 do
    0
  end

  def fun_2 do
    MyMod.fon_1 + 1
  end
end

…then Elixir does not provide a compile-time error. Instead, a runtime error is raised when fun_2 is called:

     ** (UndefinedFunctionError) undefined function MyMod.fon_1/0
     stacktrace:
       MyMod.fon_1()
       test/foo_test.exs:7: MyMod.fun_2/0
       test/foo_test.exs:15

Is there anything preventing Elixir from providing a compile-time error in this case? Obviously, if a remote function call is made using a module variable (e.g. var.foo()) the compiler can’t statically know what functions var exports. But if the remote function call is made on a static alias, it seems like the compiler should be able to figure it out.

Thanks,
Mron

José Valim

unread,
May 6, 2016, 2:44:26 AM5/6/16
to elixir-l...@googlegroups.com
There is a very similar discussion on this topic a couple days ago. I also remember we discussed it in the past. Can you please try looking up in the archives and let us know if you found it?
--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CADUxQmtuLSokWRJA0prd9bnguSepcYL6fyFWcTA%3Dw%2BntNYn88A%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.


--


José Valim
Skype: jv.ptec
Founder and Director of R&D

Myron Marston

unread,
May 6, 2016, 1:26:31 PM5/6/16
to elixir-lang-core, jose....@plataformatec.com.br
I looked in the archives before posting this and couldn't find anything related.  But maybe my search-fu is just week (I searched on "remote function").

The topic you're thinking of from a few days ago might be my proposal for `defmodulep` (it also involves remote function calls), but it's pretty different (in my mind, at least):

To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-core+unsubscribe@googlegroups.com.

José Valim

unread,
May 6, 2016, 2:00:56 PM5/6/16
to Myron Marston, elixir-lang-core
But maybe my search-fu is just week (I searched on "remote function").

It was my bad, apparently it was over the elixir-lang-talk mailing list: https://groups.google.com/forum/#!topic/elixir-lang-talk/Q3-bCnm6nxI

I did not search earlier because I was not at my computer. :)

Anyway, the issue with this feature is exactly the same that appeared during the defmodulep discussion: the only way to know if a function exists in another module is by compiling that module and today the compiler does not work under such assumptions. It would be a medium-sized change to the compiler and code with circular dependencies (arguable a bad thing anyway) would no longer compile as it would run into a deadlock.  So it is at least a backwards incompatible change.

The best option to provide such features in the short-term is by doing a post-compilation check. Post-compilation checks could also detect circular dependencies which we would need to deprecate if we want to have such features at compile time in the future.

It is definitely a topic I am interested on but it also plays against many of the runtime features in the language. For example, if we imagine protocols compile to something such as this:

    def to_string(binary) when is_binary(binary) do
      String.Chars.Binary.to_string(binary)
    end

This could would no longer compile without adding a bit of indirection.

Myron Marston

unread,
May 6, 2016, 3:26:58 PM5/6/16
to elixir-lang-core, myron....@gmail.com, jose....@plataformatec.com.br
Anyway, the issue with this feature is exactly the same that appeared during the defmodulep discussion: the only way to know if a function exists in another module is by compiling that module and today the compiler does not work under such assumptions.

It seems like mix compiles libraries in topological order (based on declared dependency relationships) so for modules provided by a dependency (or by Elixir itself) could it provide this?

I guess such an approach would only provide the compiler errors for cases where you are attempting to call a function on a module from another library, not for cases where you are attempting to call a function from another module in the same project.  It'd be an improvement in my book, though :).

The best option to provide such features in the short-term is by doing a post-compilation check. Post-compilation checks could also detect circular dependencies which we would need to deprecate if we want to have such features at compile time in the future.

It's not clear to me why we have to deprecate circular module dependencies to support this feature as a post-compilation check.  My mental model of Elixir's compiler is extremely limited, but here's an approach I'm imagining:
  • Before compilation, start an agent that tracks a map of functions to a list of call sites.
  • During compilation, at every remote function call site, add it to the agent so there is a record that `SomeModule.some_function/3` (or whatever) was called from a particular line of source code.
  • After compilation (as a post-compilation check), iterate over all remote function calls recorded in the agent.  For each, attempt to load the named module, and see if the called function is defined on that module.  If the module cannot be loaded or if the function is not defined, print an error.
This scheme would allow circular module dependencies as we have now, assuming the post-compilation step does not run until the entire project has finished compilation.  Circular project dependencies would not be allowed but from what I understand, they are already not allowed.

Any reason such an approach wouldn't work with the existing compiler?

Myron

José Valim

unread,
May 6, 2016, 3:48:07 PM5/6/16
to Myron Marston, elixir-lang-core
It seems like mix compiles libraries in topological order (based on declared dependency relationships) so for modules provided by a dependency (or by Elixir itself) could it provide this?

For modules that have been previously defined, we can provide this feature but it would be incomplete. For example, if the whole module does not exist, we wouldn't know if it was a typo or something that was meant to be defined by the current project. I honestly think a partial behaviour such as this would be confusing.

It's not clear to me why we have to deprecate circular module dependencies to support this feature as a post-compilation check.

Sorry, we don't need to deprecate circular dependencies to support post-compilation check. I wanted to say we need to use post-compilation checks if we want to deprecate circular module dependencies.

This scheme would allow circular module dependencies as we have now, assuming the post-compilation step does not run until the entire project has finished compilation.  Circular project dependencies would not be allowed but from what I understand, they are already not allowed.

Circular dependencies are allowed at runtime but not at compile time.
  
Any reason such an approach wouldn't work with the existing compiler?

There are many approaches that would work, they all compromise somehow. In particular, if it is a post-compilation check, I don't think it belongs to the compiler but as a separate step, probably a Mix task.

Reply all
Reply to author
Forward
0 new messages