Issues using programatically generated types with guards
lurodrigo opened this issue · comments
First of all, thank you for the great job in this library!
I'm trying to generate some types that depend on n
. The example below compiles and works as expected.
defmodule MyTypes do
use TypeCheck
@type! address :: String.t() when byte_size(address) == 42
# generate signed and unsigned int types
for n <- Range.new(8, 256, 8) do
@type! unquote({String.to_atom("int#{n}"), [], __MODULE__}) ::
unquote(Macro.escape(-Integer.pow(2, n - 1)..(Integer.pow(2, n - 1) - 1)))
@type! unquote({String.to_atom("uint#{n}"), [], __MODULE__}) ::
unquote(Macro.escape(0..(Integer.pow(2, n) - 1)))
end
# generate bytes types
for n <- 1..32 do
name = {String.to_atom("bytes#{n}"), [], __MODULE__}
# when byte_size(unquote(name)) <= unquote(n)
@type! unquote(name) :: binary()
end
end
defmodule MyTypesUse do
use TypeCheck
alias MyTypes
@spec! func_int(MyTypes.int32()) :: nil
def func_int(x) do
nil
end
@spec! func_bytes(MyTypes.bytes32()) :: nil
def func_bytes(x) do
nil
end
end
However, the moment I add a guard to the bytes{n}
types, it fails. Changing the bytes
lines to
defmodule MyTypes do
# ...
# generate bytes types
for n <- 1..32 do
name = {String.to_atom("bytes#{n}"), [], __MODULE__}
@type! unquote(name) :: binary() when byte_size(unquote(name)) <= unquote(n)
end
end
Results in a compilation error:
== Compilation error in file lib/my_types.ex ==
** (CompileError) lib/type_check/spec.ex:23: undefined function bytes32/0
(elixir 1.12.1) src/elixir_locals.erl:114: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3
(stdlib 3.15) erl_eval.erl:685: :erl_eval.do_apply/6
(elixir 1.12.1) lib/kernel/parallel_compiler.ex:319: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
Notice that MyTypes
compiles correctly. The compilation error only happens when we try to use it in a spec. It happens not only with remote types, but in the same module the types were defined too. If you move func_bytes
to MyTypes
defmodule MyTypes do
# ...
@spec! func_bytes(bytes32()) :: nil
def func_bytes(x) do
nil
end
# ...
end
you get
== Compilation error in file lib/my_types.ex ==
** (CompileError) lib/my_types.ex:19: imported TypeCheck.Internals.UserTypes.MyTypes.bytes32/0 conflicts with local function
(elixir 1.12.1) src/elixir_locals.erl:94: :elixir_locals."-ensure_no_import_conflict/3-lc$^0/1-0-"/2
(elixir 1.12.1) src/elixir_locals.erl:95: anonymous fn/3 in :elixir_locals.ensure_no_import_conflict/3
(stdlib 3.15) erl_eval.erl:685: :erl_eval.do_apply/6
(elixir 1.12.1) lib/kernel/parallel_compiler.ex:319: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
I'm not sure whether it's a but or an error in my assumptions. I'm willing to contribute to helping solve these if it's indeed a bug, just need to discuss it first as I'm not familiar with the internals.
Thank you for this bugreport!
I'll investigate to find out what is going wrong, because it definitely does seem like this should 'just work'.
I was able to reproduce the problem and debug it.
The issue here is that the variable that is given the name of the type as part of the runtime type-check of the guard, seems to be given too late. Or at least the Elixir compiler decides at some point that it has to refer to a function and then runs into an import conflict (which has to do with how types are built internally).
I will dig deeper to find out how to fix this edge case as part of the library.
For now, there are two simple ways to fix this particular problem however:
defmodule MyTypes do
use TypeCheck
for n <- 1..32 do
name = {String.to_atom("bytes#{n}"), [], __MODULE__}
@type! unquote(name) :: (str :: binary() when byte_size(str) <= unquote(n))
end
@spec! func_bytes(bytes32()) :: nil
def func_bytes(x) do
nil
end
end
In this case we circumvent the problem by just using a different name inside the guard alltogether.
Another is to use Macro.var
:
defmodule MyTypes do
use TypeCheck
for n <- 1..32 do
name = Macro.var(:"bytes#{n}", nil)
@type! unquote(name) :: binary() when byte_size(unquote(name)) <= unquote(n)
end
@spec! func_bytes(bytes32()) :: nil
def func_bytes(x) do
nil
end
end
In this case we make it clear that name
really is a variable that does not refer to anything in the current module. The Elixir compiler will then not try to turn it into a function.
Again, I think there is something odd going on in the code that TypeCheck generates for guards that contain the types' name itself in the guard which I will look to fix,
but I hope that these two examples allow you to keep on working in the meantime.
Thank you so much for the detailed explanation! It's more than enough for now.
The design of how named types / guards works has since been slightly changed; to use a guard you should now always use the syntax:
@type! name :: (val :: binary()) when byte_size(val) <= unquote(n))
That is, do not refer to the name of the type itself in the guard, but add a name inside the type. Guards will no longer recognize the name of the type itself.
There are a number of reasons why this is done:
- The type would otherwise read odd if you have a type accepting parameters.
- It is always super clear where a particular name comes from. Using a guard to refer to a name somewhere in a nested type (with this I mean, a type that was defined elsewhere) is no longer supported (c.f. #4 ). This improves the 'opaqueness' of types, but it also improves runtime efficiency.