Trying to auto-generate a typespec for a longer opts keyword list parameters. Reduce function:
ts = args
|> Enum.map(&{String.to_atom(&1.name), MyMod.type_to_spec(&1.type)})
|> Enum.reduce(fn item, acc -> {:|, [], [item, acc]} end)
Generating 2 types, one hard-coded, one through my generated above:
@type test_1 :: unquote(ts)
@type test_2 :: {:user, String.t()} | {:top_p, float()} | {:temperature, float()}
Then using it in a function @spec
definition like usual with [test_1]
or [test_2]
.
Checking with t
:
iex(65)> t MyMod.test_1
@type test_1() ::
{:user, String.t()}
| {:top_p, float()}
| {:temperature, float()}
| {:stream, boolean()}
| {:stop, any()}
| {:presence_penalty, float()}
| {:n, integer()}
| {:max_tokens, integer()}
| {:logit_bias, map()}
| {:frequency_penalty, float()}
iex(65)> t MyMod.test_2
@type test_2() ::
{:user, String.t()} | {:top_p, float()} | {:temperature, float()}
So far so good, they look identical to me, with test_1
being longer
However, dialyzer/LSP are not catching the spec violation when using type_1
, but are reporting incorrect usage (as they should) with type_2
.
Tried to dig deeper and extracted the types with Code.Typespec.fetch_types
:
{:type, {:test_1,
{:type, 0, :union,
[
{:type, 0, :tuple, [{:atom, 0, :user}, {:remote_type, 0, [{:atom, 0, String}, {:atom, 0, :t}, []]} ]},
{:type, 0, :tuple, [{:atom, 0, :top_p}, {:type, 0, :float, []}]},
{:type, 0, :tuple, [{:atom, 0, :temperature}, {:type, 0, :float, []}]},
{:type, 0, :tuple, [{:atom, 0, :stream}, {:type, 0, :boolean, []}]},
{:type, 0, :tuple, [{:atom, 0, :stop}, {:type, 0, :any, []}]},
{:type, 0, :tuple, [{:atom, 0, :presence_penalty}, {:type, 0, :float, []}]},
{:type, 0, :tuple, [{:atom, 0, :n}, {:type, 0, :integer, []}]},
{:type, 0, :tuple, [{:atom, 0, :max_tokens}, {:type, 0, :integer, []}]},
{:type, 0, :tuple, [{:atom, 0, :logit_bias}, {:type, 0, :map, :any}]},
{:type, 0, :tuple, [{:atom, 0, :frequency_penalty}, {:type, 0, :float, []}]}
]}, []}}
{:type, {:test_2,
{:type, 610, :union,
[
{:type, 0, :tuple, [{:atom, 0, :user}, {:remote_type, 610, [{:atom, 0, String}, {:atom, 0, :t}, []]} ]},
{:type, 0, :tuple, [{:atom, 0, :top_p}, {:type, 610, :float, []}]},
{:type, 0, :tuple, [{:atom, 0, :temperature}, {:type, 610, :float, []}]}
]}, []}}
Looks also identical to me, with the difference that type_2
has line number annotations
Lastly converting back to quoted form to compare:
iex(67)> Code.Typespec.type_to_quoted x
{:"::", [],
[
{:test_1, [], []},
{:|, [line: 0],
[
{:{}, [line: 0], [:user, {{:., [line: 0], [String, :t]}, [line: 0], []}]},
{:|, [line: 0],
[
{:{}, [line: 0], [:top_p, {:float, [line: 0], []}]},
{:|, [line: 0],
[
{:{}, [line: 0], [:temperature, {:float, [line: 0], []}]},
{:|, [line: 0],
[
{:{}, [line: 0], [:stream, {:boolean, [line: 0], []}]},
{:|, [line: 0],
[
{:{}, [line: 0], [:stop, {:any, [line: 0], []}]},
{:|, [line: 0],
[
{:{}, [line: 0],
[:presence_penalty, {:float, [line: 0], []}]},
{:|, [line: 0],
[
{:{}, [line: 0], [:n, {:integer, [line: 0], []}]},
{:|, [line: 0],
[
{:{}, [line: 0], [:max_tokens, {:integer, ...}]},
{:|, [line: 0], [{:{}, ...}, {...}]}
]}
]}
]}
]}
]}
]}
]}
]}
]}
iex(68)> Code.Typespec.type_to_quoted y
{:"::", [],
[
{:test_2, [], []},
{:|, [line: 610],
[
{:{}, [line: 0],
[:user, {{:., [line: 610], [String, :t]}, [line: 610], []}]},
{:|, [line: 610],
[
{:{}, [line: 0], [:top_p, {:float, [line: 610], []}]},
{:{}, [line: 0], [:temperature, {:float, [line: 610], []}]}
]}
]}
]}
Still don’t see why test_2
is working but test_1
isn’t. Is the construct just too long so dialyzer is choking on it?
/EDIT: Usage of the types like this:
@type test_1 :: unquote(optional_args)
@type test_2 :: {:user, String.t()} | {:top_p, float()} | {:temperature, float()}
# test_1 or test_2 here
@spec unquote(name)(unquote_splicing(spec), [test_1]) :: {:ok, unquote(response_spec)} | {:error, any()}
I’ve been staring at this a bit too long. Any ideas?