Skip to content

Commit

Permalink
Gettingthere
Browse files Browse the repository at this point in the history
  • Loading branch information
edgurgel committed Aug 12, 2024
1 parent 9b31b0e commit 1677429
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 48 deletions.
109 changes: 67 additions & 42 deletions lib/ham/type_checker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ defmodule Ham.TypeChecker do
@spec validate(module, atom, [any], any, list(module)) :: :ok | {:error, TypeMatchError.t()}
def validate(module, function_name, args, return_value, behaviours \\ []) do
case fetch_typespecs(module, function_name, length(args), behaviours) do
[] -> :ok
typespecs -> check_call(args, return_value, typespecs)
{:ok, typespecs} -> check_call(args, return_value, typespecs)
{:error, reason} -> {:error, %TypeMatchError{reasons: [reason]}}
end
end

Expand Down Expand Up @@ -89,51 +89,59 @@ defmodule Ham.TypeChecker do

case Cache.get(cache_key) do
nil ->
typespecs = do_fetch_typespecs(module, function_name, arity)
Cache.put(cache_key, typespecs)
typespecs
with {:ok, specs} <- do_fetch_typespecs(module, function_name, arity) do
Cache.put(cache_key, specs)
{:ok, specs}
end

typespecs ->
typespecs
{:ok, typespecs}
end
end

defp do_fetch_typespecs(module, function_name, arity) do
callbacks = fetch_specs(module)

callbacks
|> Enum.find_value([], fn
{{^function_name, ^arity}, typespecs} -> typespecs
_ -> false
end)
|> Enum.map(&guards_to_annotated_types(&1))
|> Enum.map(&Utils.replace_user_types(&1, module))
with {:ok, specs} <- fetch_specs(module) do
specs =
specs
|> Enum.find_value([], fn
{{^function_name, ^arity}, typespecs} -> typespecs
_ -> false
end)
|> Enum.map(&guards_to_annotated_types(&1))
|> Enum.map(&Utils.replace_user_types(&1, module))

{:ok, specs}
end
end

defp fetch_callback_typespecs(module, function_name, arity) do
cache_key = {:callback_typespecs, {module, function_name, arity}}

case Cache.get(cache_key) do
nil ->
typespecs = do_fetch_callback_typespecs(module, function_name, arity)
Cache.put(cache_key, typespecs)
typespecs
with {:ok, typespecs} <- do_fetch_callback_typespecs(module, function_name, arity) do
Cache.put(cache_key, typespecs)
{:ok, typespecs}
end

typespecs ->
typespecs
{:ok, typespecs}
end
end

defp do_fetch_callback_typespecs(module, function_name, arity) do
callbacks = fetch_callbacks(module)

callbacks
|> Enum.find_value([], fn
{{^function_name, ^arity}, typespecs} -> typespecs
_ -> false
end)
|> Enum.map(&guards_to_annotated_types(&1))
|> Enum.map(&Utils.replace_user_types(&1, module))
with {:ok, callbacks} = fetch_callbacks(module) do
callbacks =
callbacks
|> Enum.find_value([], fn
{{^function_name, ^arity}, typespecs} -> typespecs
_ -> false
end)
|> Enum.map(&guards_to_annotated_types(&1))
|> Enum.map(&Utils.replace_user_types(&1, module))

{:ok, callbacks}
end
end

defp guards_to_annotated_types(typespec = {:type, _, :fun, _}), do: typespec
Expand Down Expand Up @@ -175,35 +183,52 @@ defmodule Ham.TypeChecker do
defp fetch_specs(module) do
case Cache.get({:specs, module}) do
nil ->
{:ok, specs} = Code.Typespec.fetch_specs(module)
Cache.put({:specs, module}, specs)
specs
case Code.Typespec.fetch_specs(module) do
{:ok, specs} ->
Cache.put({:specs, module}, specs)
{:ok, specs}

:error ->
{:error, {:module_fetch_failure, module}}
end

specs ->
specs
{:ok, specs}
end
end

defp fetch_callbacks(behaviour_module) do
case Cache.get({:callbacks, behaviour_module}) do
nil ->
{:ok, callbacks} = Code.Typespec.fetch_callbacks(behaviour_module)
Cache.put({:callbacks, behaviour_module}, callbacks)
callbacks
case Code.Typespec.fetch_callbacks(behaviour_module) do
{:ok, callbacks} ->
Cache.put({:callbacks, behaviour_module}, callbacks)
{:ok, callbacks}

:error ->
{:error, {:module_fetch_failure, behaviour_module}}
end

callbacks ->
callbacks
{:ok, callbacks}
end
end

defp fetch_behaviours_typespecs(behaviours, function_name, arity) do
Enum.reduce_while(behaviours, {:ok, []}, fn behaviour, {:ok, acc} ->
case fetch_callback_typespecs(behaviour, function_name, arity) do
{:ok, callback_specs} -> {:cont, {:ok, [callback_specs | acc]}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
end

defp fetch_typespecs(module, function_name, arity, behaviours)
when is_atom(module) and is_atom(function_name) and is_integer(arity) do
behaviours
|> Enum.map(fn behaviour ->
fetch_callback_typespecs(behaviour, function_name, arity)
end)
|> List.flatten()
|> Enum.concat(fetch_typespecs(module, function_name, arity))
with {:ok, specs} <- fetch_typespecs(module, function_name, arity),
{:ok, callback_specs} <- fetch_behaviours_typespecs(behaviours, function_name, arity) do
{:ok, List.flatten([callback_specs | specs])}
end
end

defp arg_typespec(function_typespec, arg_index) do
Expand Down
14 changes: 8 additions & 6 deletions test/ham/type_checker_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ defmodule Ham.TypeCheckerTest do
test "unknown type" do
assert {:error, error} = TypeChecker.validate(EstModule, :unknown, ["a", 2], :not_ok)

assert Ham.TypeMatchError.translate(error) == [
"1st argument value \"a\" does not match 1st parameter's type number().",
"Value \"a\" does not match type integer() | float().",
"Returned value :not_ok does not match type number().",
"Value :not_ok does not match type integer() | float()."
]
assert Ham.TypeMatchError.translate(error) == ["Could not load module EstModule."]
end

test "unknown behaviour type" do
assert :ok =
TypeChecker.validate(MultiBehaviourImplementation, :other_foo, [1], 3, [
UnknownBehaviour
])
end
end

Expand Down

0 comments on commit 1677429

Please sign in to comment.