Compilation error when using `conforms?`
lbueso opened this issue · comments
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.