Qqwy / elixir-type_check

TypeCheck: Fast and flexible runtime type-checking for your Elixir projects.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Compilation error when using `conforms?`

lbueso opened this issue · comments

commented

When a type is defined as follows:

defmodule A do
  use TypeCheck
  @type! t() :: %{v: integer()}

  @spec! f(t()) :: integer()
  def f(x), do: x.v
end

TypeCheck works correctly, but if I try to use TypeCheck.conforms? in the same module where a type is defined, I get a complation error:

defmodule A do
  use TypeCheck
  @type! t() :: %{v: integer()}


  def main() do
    x = 1
    TypeCheck.conforms?(x, t())
  end
end
== Compilation error in file lib/examples.ex ==
** (UndefinedFunctionError) function A.t/0 is undefined (function not available)
    A.t()
    (stdlib 3.17) erl_eval.erl:685: :erl_eval.do_apply/6
    (elixir 1.13.4) lib/code.ex:797: Code.eval_quoted/3
    (type_check 0.12.1) lib/type_check/type.ex:96: TypeCheck.Type.build_unescaped/4
    (type_check 0.12.1) lib/type_check.ex:202: TypeCheck."MACRO-conforms?"/4
    (type_check 0.12.1) expanding macro: TypeCheck.conforms?/2

Thank you for your bug report. 😊
This indeed should not happen, and I'll do my best to fix it shortly (which will probably be some time later this week).

I started working on this.
Turns out that there is a bit of a conondrum with how the library currently is implemented that makes fixing this very difficult.

To be able to support recursive types and the likes, we currently:

  • First accumulate all type definitions
  • Then in a before_compile callback, define a separate module that only contains the "type functions", and import this in the body of the normal module.
  • The types can now be found from within @spec! and the likes (any other macro in the module body).
  • The type-functions themselves are added to the (end of the) module body.
  • Then we compile the module itself.
  • If types are ever referred to inside a function directly, the definition in the main module is used.

However, it seems that when you use a macro inside a function and refer to a type from within, then this macro's expansion happens before the before_compile callback step.

At this point we do have some of the type information available inside a module attribute (Module.get_attribute(__MODULE__, TypeCheck.TypeDefs)), for all the types that were already seen.
But importing type definitions from there requires manually altering how Elixir does its function lookups in this very specific instance. This feels like a brittle solution.

However, I do not have an alternative idea as of right now.

Essentially, the problem is the same as in this minimal example:

defmodule MacroModule do
  defmacro precompute(x) do
    result = Code.eval_quoted(x)
    quote do
      unquote(result)
    end
  end
end

defmodule Example do
  require MacroModule
  def foo() do
    42
  end

  def bar(y) do
    MacroModule.precompute(foo()) * 3
  end
end
** (UndefinedFunctionError) function Example.foo/0 is undefined (function not available)
    Example.foo()
    nofile:23: (file)
    iex:16: (file)

I do not think there is a way to fix this problem in the general sense.
But maybe we can lookup local types as mentioned before in some restricted cases.