defmodule ElixirST.SessionTypechecking do
alias ElixirST.{ST, TypeOperations}
alias ElixirSTError
require Logger
@moduledoc """
Elixir code is typechecked against a pre-define session type.
"""
# Session type checking a whole module, which may include multiple functions with multiple session type definitions
@spec session_typecheck_module(
%{ST.name_arity() => ST.Function.t()},
%{ST.name_arity() => ST.session_type()},
atom(),
list
) :: list
def session_typecheck_module(
all_functions,
function_session_type,
_module_name,
_options \\ []
) do
# Logger.debug("Starting session typechecking for module #{inspect(module_name)}")
for {{name, arity}, expected_session_type} <- function_session_type do
function = lookup_function!(all_functions, {name, arity})
%ST.Function{
types_known?: types_known?
} = function
if not types_known? do
# Get line number from meta
line = function.meta[:line] || 1
raise ElixirSTError,
message: "Function #{name}/#{arity} has unknown return type. Use @spec to set parameter and return types.",
lines: [line]
end
env = %{
# :ok or :error or :warning
:state => :ok,
# error message
:error_data => nil,
# lines where errors occur
:error_lines => [],
# :x => :atom
:variable_ctx => %{},
# Expected session type
# rec X.(!A().X)
:session_type => expected_session_type,
# Expected type
:type => :any,
# {name, arity} => %ST.Function
:functions => all_functions,
# {name, arity} => rec X.(!A().X)
:function_session_type_ctx => function_session_type
}
result_env = session_typecheck_by_function(function, env)
# %{
# state: result_env[:state],
# error_data: result_env[:error_data],
# variable_ctx: result_env[:variable_ctx],
# session_type: ST.st_to_string(result_env[:session_type]),
# type: result_env[:type]
# # functions: result_env[:functions],
# # function_session_type_ctx: result_env[:function_session_type_ctx]
# }
# # |> Logger.debug()
case result_env[:state] do
:ok ->
Logger.info("Session typechecking for #{name}/#{arity} terminated successfully")
:error ->
Logger.error("Session typechecking for #{name}/#{arity} found an error. ")
Logger.error(result_env.error_data)
raise ElixirSTError, message: result_env.error_data, lines: result_env.error_lines
end
result_env
end
end
@spec session_typecheck_by_function(ST.Function.t(), map()) :: map()
def session_typecheck_by_function(%ST.Function{} = function, env) do
%ST.Function{
name: name,
arity: arity,
bodies: bodies,
meta: meta,
return_type: expected_return_type,
parameters: parameters,
param_types: param_types
} = function
all_results =
for {ast, parameters} <- List.zip([bodies, parameters]) do
# Initialize the variable context with the parameters and their types
variable_ctx =
Enum.zip(parameters, param_types)
|> Enum.map(fn {var, type} -> TypeOperations.get_vars(var, type) end)
|> List.flatten()
case Enum.find(variable_ctx, nil, fn {f, _} -> f == :error end) do
{:error, message} ->
# Check if there are any errors
%{state: :error,
error_lines: [meta[:line] || 1],
error_data: message
}
_ ->
# No errors, proceed by typechecking the ast
env = %{env | variable_ctx: Enum.into(variable_ctx, %{})}
{_ast, res_env} = Macro.prewalk(ast, env, &typecheck/2)
res_env
end
end
Enum.reduce_while(all_results, hd(all_results), fn result, _acc ->
case result[:state] do
:error ->
{:halt, result}
_ ->
# Check return type
same_type = TypeOperations.equal?(result[:type], expected_return_type)
cond do
result[:session_type] != %ST.Terminate{} ->
{:halt,
%{
result
| state: :error,
error_lines: append_error_line(result[:error_lines], meta[:line]),
error_data:
"Unfulfilled session type for #{name}/#{arity}. Remaining session type: " <>
ST.st_to_string_current(result[:session_type])
}}
same_type == false ->
{:halt,
%{
result
| state: :error,
error_lines: append_error_line(result[:error_lines], meta[:line]),
error_data:
"Return type for #{name}/#{arity} is #{TypeOperations.string(result[:type])} but expected " <>
TypeOperations.string(expected_return_type)
}}
true ->
{:cont, result}
end
end
end)
end
@spec typecheck(ST.ast(), map()) :: {ST.ast(), map()}
def typecheck(
_node,
%{
state: :error,
error_data: _error_data,
variable_ctx: _,
session_type: _,
type: _,
functions: _,
function_session_type_ctx: _
} = env
) do
# Logger.error("ElixirST Error! " <> error_data)
{nil, env}
end
# Block
def typecheck({:__block__, meta, args}, env) do
# Logger.debug("Typechecking: Block")
node = {:__block__, meta, nil}
env =
Enum.reduce_while(args, env, fn current_node, env_acc ->
{_ast, new_env} = Macro.prewalk(current_node, env_acc, &typecheck/2)
case new_env[:state] do
:error ->
{:halt, new_env}
_ ->
{:cont, %{new_env | variable_ctx: Map.merge(env_acc[:variable_ctx], new_env[:variable_ctx])}}
end
end)
{node, env}
end
# Literals
def typecheck(node, env)
when is_atom(node) or is_number(node) or is_binary(node) or is_boolean(node) or
is_float(node) or is_integer(node) or is_nil(node) or is_pid(node) do
# Logger.debug("Typechecking: Literal: #{inspect(node)} #{TypeOperations.typeof(node)}")
{node, %{env | type: TypeOperations.typeof(node)}}
end
# Tuples
def typecheck({:{}, meta, args}, env) when is_list(args) do
node = {:{}, meta, []}
# Logger.debug("Typechecking: {}")
{types_list, new_env} =
Enum.map(args, fn arg -> elem(Macro.prewalk(arg, env, &typecheck/2), 1) end)
|> Enum.reduce_while({[], env}, fn result, {types_list, env_acc} ->
case result[:state] do
:error ->
{:halt, {[], result}}
_ ->
{:cont, {types_list ++ [result[:type]], %{env_acc | variable_ctx: Map.merge(env_acc[:variable_ctx], result[:variable_ctx] || %{})}}}
end
end)
{node, %{new_env | type: {:tuple, types_list}}}
end
# Tuples (of size 2)
def typecheck({arg1, arg2}, env) do
node = {:{}, [], [arg1, arg2]}
typecheck(node, env)
end
# Lists and List operations
def typecheck([], env) do
{[], %{env | type: {:list, :any}}}
end
def typecheck([{:|, meta, [operand1, operand2]}], env) do
node = {:|, meta, []}
process_binary_operations(node, meta, :|, operand1, operand2, {:list, nil}, false, false, env)
end
def typecheck(node, env) when is_list(node) do
typechecked_nodes = Enum.map(node, fn t -> elem(Macro.prewalk(t, env, &typecheck/2), 1) end)
result =
Enum.reduce_while(typechecked_nodes, %{hd(typechecked_nodes) | type: hd(typechecked_nodes)[:type]}, fn result, env_acc ->
case result[:state] do
:error ->
{:halt, result}
_ ->
if TypeOperations.equal?(result[:type], env_acc[:type]) do
{:cont, %{result | variable_ctx: Map.merge(env_acc[:variable_ctx], result[:variable_ctx] || %{})}}
else
{:halt,
%{
result
| state: :error,
error_data: "Malformed list (" <> inspect(result[:type]) <> ", " <> inspect(env_acc[:type]) <> "): " <> inspect(node)
}}
end
end
end)
{[], %{result | type: {:list, result[:type]}}}
end
def typecheck({{:., meta1, [:erlang, operator]}, meta2, [arg1, arg2]}, env)
when operator in [:++, :--] do
# Logger.debug("Typechecking: Erlang #{operator}")
node = {{:., meta1, []}, meta2, []}
{node, result_env} = process_binary_operations(node, meta2, operator, arg1, arg2, {:list, nil}, true, false, env)
if result_env[:state] == :error do
{node, result_env}
else
case result_env[:type] do
{:list, _} ->
{node, result_env}
_ ->
{node,
%{
result_env
| state: :error,
error_lines: append_error_line(result_env[:error_lines], meta1[:line]),
error_data: "Expected type of list but found " <> TypeOperations.string(result_env[:type])
}}
end
end
end
# Arithmetic operations
def typecheck({{:., meta1, [:erlang, operator]}, meta2, [arg1, arg2]}, env)
when operator in [:+, :-, :*, :/] do
# Logger.debug("Typechecking: Erlang #{operator}")
node = {{:., meta1, []}, meta2, []}
{node, env_res} = process_binary_operations(node, meta2, operator, arg1, arg2, :number, false, false, env)
# IO.inspect("return Erlang 2 #{operator}")
# IO.inspect env_res[:state]
# IO.inspect env_res[:error_data]
# IO.inspect env_res[:error_lines]
{node, env_res}
end
# too complex in extended elixir: [:and, :or]
# Elixir format: [:==, :!=, :===, :!== , :>, :<, :<=, :>= ]
# Extended Elixir format: [:==, :"/=", :"=:=", :"=/=", :>, :<, :"=<", :">="]
def typecheck({{:., meta1, [:erlang, operator]}, meta2, [arg1, arg2]}, env)
when operator in [:==, :"/=", :"=:=", :"=/=", :>, :<, :"=<", :>=] do
node = {{:., meta1, []}, meta2, []}
# improve: convert operator from extended elixir to elixir
process_binary_operations(node, meta2, operator, arg1, arg2, :any, false, true, env)
end
# Not
def typecheck({{:., _meta1, [:erlang, :not]}, meta2, [arg]} = node, env) do
process_unary_operations(node, meta2, arg, :boolean, env)
end
# Negate
def typecheck({{:., _meta1, [:erlang, :-]}, meta2, [arg]}, env) do
# Logger.debug("Typechecking: Erlang negation")
node = {nil, meta2, []}
process_unary_operations(node, meta2, arg, :number, env)
end
def typecheck({{:., _meta1, [:erlang, erlang_function]}, meta2, _arg}, env)
when erlang_function not in [:send, :self] do
# Logger.debug("Typechecking: Erlang others #{erlang_function} (not supported)")
node = {nil, meta2, []}
{node,
%{
env
| state: :error,
error_lines: append_error_line(env[:error_lines], meta2[:line]),
error_data: "Unknown erlang function #{inspect(erlang_function)}"
}}
end
# Binding operator
def typecheck({:=, meta, [pattern, expr]}, env) do
# Logger.debug("Typechecking: Binding operator (i.e. =)")
node = {:=, meta, []}
{_expr_ast, expr_env} = Macro.prewalk(expr, env, &typecheck/2)
case expr_env[:state] do
:error ->
{node, expr_env}
_ ->
pattern = if is_list(pattern), do: pattern, else: [pattern]
pattern_vars = TypeOperations.var_pattern(pattern, [expr_env[:type]])
case pattern_vars do
{:error, msg} -> {node, %{expr_env | state: :error, error_lines: append_error_line(env[:error_lines], meta[:line]), error_data: msg}}
_ -> {node, %{expr_env | variable_ctx: Map.merge(expr_env[:variable_ctx], pattern_vars || %{})}}
end
end
end
# Variables
def typecheck({x, meta, arg}, env) when is_atom(arg) do
# Logger.debug("Typechecking: Variable #{inspect(x)} with type #{inspect(env[:variable_ctx][x])}")
node = {x, meta, arg}
if Map.has_key?(env[:variable_ctx], x) do
{node, %{env | type: env[:variable_ctx][x]}}
else
{node, %{env | state: :error, error_lines: append_error_line(env[:error_lines], meta[:line]), error_data: "Variable #{x} was not found"}}
end
end
# Case
def typecheck({:case, meta, [expr, body | _]}, env) do
# Logger.debug("Typechecking: Case")
# body contains [do: [ {:->, _, [ [ when/condition ], work ]}, other_cases... ] ]
node = {:case, meta, []}
cases = process_cases(body[:do])
case_line = meta[:line]
{_expr_ast, expr_env} = Macro.prewalk(expr, env, &typecheck/2)
result =
case expr_env[:state] do
:error ->
{:error, case_line, {:inner_error, expr_env[:error_lines], expr_env[:error_data]}}
_ ->
# Get label, parameters and remaining ast from the source ast
all_cases_result =
Enum.map(cases, fn {line, lhs, rhs} ->
pattern_vars = TypeOperations.var_pattern([lhs], [expr_env[:type]]) || %{}
case pattern_vars do
{:error, msg} ->
{:error, line, msg}
_ ->
env = %{env | variable_ctx: Map.merge(env[:variable_ctx], pattern_vars)}
{_case_ast, case_env} = Macro.prewalk(rhs, env, &typecheck/2)
case case_env[:state] do
:error ->
{:error, line, {:inner_error, case_env[:error_lines], case_env[:error_data]}}
_ ->
case_env
end
end
end)
case process_cases_result(all_cases_result) do
{:error, line, message} ->
{:error, line || meta[:line], message}
result ->
result
end
end
case result do
{:error, _, _} = error -> {node, append_error(env, error)}
_ -> {node, %{env | session_type: result[:session_type], type: result[:type]}}
end
end
# Send Function
def typecheck({{:., _meta1, [:erlang, :send]}, meta2, [send_destination, send_body | _]}, env) do
# Logger.debug("Typechecking: Erlang send")
node = {nil, meta2, []}
{_ast1, send_destination_env} = Macro.prewalk(send_destination, env, &typecheck/2)
{_ast2, send_body_env} = Macro.prewalk(send_body, env, &typecheck/2)
try do
if send_destination_env[:state] == :error do
throw({:error, send_destination_env[:error_data]})
end
if send_body_env[:state] == :error do
throw({:error, send_body_env[:error_data]})
end
if TypeOperations.equal?(send_destination_env[:type], :pid) == false do
throw({:error, "Expected pid in send statement, but found #{inspect(send_destination_env[:type])}"})
end
case send_body_env[:type] do
{:tuple, _} -> :ok
_ -> throw({:error, "Expected a tuple in send statement containing {:label, ...}"})
end
{:tuple, [label_type | parameter_types]} = send_body_env[:type]
[label | parameters] = tuple_to_list(send_body)
if TypeOperations.equal?(label_type, :atom) == false do
throw({:error, "First item in tuple should be a literal/atom"})
end
# Unfold if session type starts with rec X.
session_type = ST.unfold(env[:session_type])
%ST.Send{label: expected_label, types: expected_types, next: remaining_session_types} =
case session_type do
%ST.Send{} = st ->
st
%ST.Choice{choices: choices} ->
if choices[label] do
choices[label]
else
throw(
{:error,
"Cannot match send statement `#{Macro.to_string(send_body)}` " <>
"with #{ST.st_to_string_current(session_type)}"}
)
end
x ->
throw({:error, "Found a send/choice, but expected #{ST.st_to_string(x)}."})
end
if expected_label != label do
throw({:error, "Expected send with label #{inspect(expected_label)} but found #{inspect(label)}."})
end
if length(expected_types) != length(parameter_types) do
throw(
{:error,
"Session type payload length mismatch. Expected " <>
"#{ST.st_to_string_current(session_type)} (length = " <>
"#{length(expected_types)}), but found #{Macro.to_string(send_body)} " <>
"(length = #{length(parameter_types)})."}
)
end
if TypeOperations.equal?(parameter_types, expected_types) == false do
throw(
{:error,
"Incorrect payload types. Expected " <>
"#{ST.st_to_string_current(session_type)} " <>
"but found #{Macro.to_string(parameters)} with type/s #{inspect(parameter_types)}"}
)
end
{node, %{send_body_env | session_type: remaining_session_types}}
catch
{:error, message} ->
{node, append_error(env, {:error, meta2[:line], message})}
end
end
# Receive
def typecheck({:receive, meta, [body | _]}, env) do
# body contains [do: [ {:->, _, [ [ when/condition ], work ]}, other_cases... ] ]
# Logger.debug("Typechecking: receive")
node = {:receive, meta, []}
cases = process_cases(body[:do])
receive_line = meta[:line]
try do
# In case of one receive branch, it should match with a %ST.Recv{}
# In case of more than one receive branch, it should match with a %ST.Branch{}
# Unfold if session type starts with rec X.(...)
session_type = ST.unfold(env[:session_type])
branches_session_types =
case session_type do
%ST.Branch{branches: branches} -> branches
%ST.Recv{label: label, types: types, next: next} -> %{label => %ST.Recv{label: label, types: types, next: next}}
x -> throw({:error, receive_line, "Found a receive/branch, but expected #{ST.st_to_string_current(x)}."})
end
# Each branch from the session type should have an equivalent branch in the receive cases
if map_size(branches_session_types) != length(cases) do
throw(
{:error, receive_line,
"[in branch/receive] Mismatch in number of receive and & branches. " <>
"Expected session type #{ST.st_to_string_current(session_type)}"}
)
end
# Get label, parameters and remaining AST from the source AST
all_branches_result =
Enum.map(cases, fn {line, lhs, rhs} ->
[head | _] = tuple_to_list(lhs)
if branches_session_types[head] do
%ST.Recv{types: expected_types, next: remaining_st} = branches_session_types[head]
pattern_vars = TypeOperations.var_pattern([lhs], [{:tuple, [:atom] ++ expected_types}]) || %{}
case pattern_vars do
{:error, msg} ->
{:error, line, msg}
_ ->
env = %{
env
| session_type: remaining_st,
variable_ctx: Map.merge(env[:variable_ctx], pattern_vars)
}
{_branch_ast, branch_env} = Macro.prewalk(rhs, env, &typecheck/2)
case branch_env[:state] do
:error -> {:error, line, {:inner_error, branch_env[:error_lines], branch_env[:error_data]}}
_ -> branch_env
end
end
else
throw({:error, line, "Receive branch with label #{inspect(head)} did not match session type"})
end
end)
case process_cases_result(all_branches_result) do
{:error, line, message} ->
throw({:error, line || receive_line, message})
result ->
{node, %{env | session_type: result[:session_type], type: result[:type]}}
end
catch
{:error, _, _} = error ->
{node, append_error(env, error)}
end
end
# Hardcoded stuff (not ideal)
def typecheck({{:., _meta1, [:erlang, :self]}, meta2, []}, env) do
# Logger.debug("Typechecking: Erlang self")
node = {nil, meta2, []}
{node, %{env | type: :pid}}
end
def typecheck({{:., _meta, [IO, :puts]}, meta2, _}, env) do
# Logger.debug("Typechecking: IO.puts")
node = {nil, meta2, []}
{node, %{env | type: :atom}}
end
def typecheck({{:., _meta, [IO, :gets]}, meta2, _}, env) do
# Logger.debug("Typechecking: IO.gets")
node = {nil, meta2, []}
{node, %{env | type: :binary}}
end
def typecheck({{:., _meta, _call}, meta2, _}, env) do
# Logger.debug("Typechecking: Remote function call (#{inspect(call)})")
node = {nil, meta2, []}
{node, %{env | state: :error, error_lines: append_error_line(env[:error_lines], meta2[:line]), error_data: "Remote functions not allowed."}}
end
def typecheck({:., meta, _}, env) do
node = {nil, meta, []}
{node, env}
end
# Functions
def typecheck({name, meta, args}, env) when is_list(args) do
# Logger.debug("Typechecking: Function #{inspect(name)}")
node = {name, meta, []}
name_arity = {name, length(args)}
line = meta[:line]
try do
function =
case lookup_function(env[:functions], name_arity) do
{:error, message} ->
# Function does not exist in current module
throw({:error, message})
{:ok, function} ->
function
end
if not function.types_known? do
throw({:error, "Function #{name}/#{length(args)} has unknown return type. Use @spec to set parameter and return types."})
end
argument_types =
for arg <- args do
# Checks for parameter types
{_ast, argument_env} = Macro.prewalk(arg, env, &typecheck/2)
if argument_env[:state] == :error do
throw({:error, argument_env[:error_data]})
end
argument_env[:type]
end
# Check argument types
Enum.zip(function.param_types, argument_types)
|> Enum.map(fn {expected, actual} ->
unless TypeOperations.equal?(expected, actual) do
throw(
{:error,
"Argument type error when calling #{name}/#{length(args)}: " <>
"Expect #{TypeOperations.string(expected)} but found #{TypeOperations.string(actual)}"}
)
end
end)
# if TypeOperations.equal?(argument_env[:type])
if env[:function_session_type_ctx][name_arity] do
# Function with known session type (i.e. def with @session)
function_session_type = env[:function_session_type_ctx][name_arity]
expected_session_type = env[:session_type]
if ST.equal?(function_session_type, expected_session_type) do
{node, %{env | session_type: %ST.Terminate{}, type: function.return_type}}
else
throw(
{:error,
"Function #{name}/#{length(args)} has session type #{ST.st_to_string(function_session_type)} " <>
"but was expecting #{ST.st_to_string_current(expected_session_type)}."}
)
end
else
# Function with unknown session type (i.e. defp)
new_env = %{
env
| variable_ctx: %{},
function_session_type_ctx: Map.merge(env[:function_session_type_ctx], %{name_arity => env[:session_type]})
}
new_env = session_typecheck_by_function(function, new_env)
cond do
new_env[:state] == :error ->
throw({:error, {:inner_error, new_env[:error_lines], new_env[:error_data]}})
new_env[:session_type] != %ST.Terminate{} ->
throw({:error, "Function #{name}/#{length(args)} does not match the session type " <> ST.st_to_string(env[:session_type])})
true ->
{node, %{env | session_type: new_env[:session_type], type: new_env[:type]}}
end
end
catch
{:error, details} ->
{node, append_error(env, {:error, line, details})}
end
end
def typecheck(other, env) do
# Logger.debug("Typechecking: other #{inspect(other)}")
{other, env}
end
# Returns the lhs and rhs for all cases (i.e. lhs -> rhs)
# Includes also the line number of each case
defp process_cases(cases) do
Enum.map(cases, fn
{:->, meta, [[{:when, _, [var, _cond | _]}], rhs | _]} ->
line = meta[:line]
{line, var, rhs}
{:->, meta, [[lhs], rhs | _]} ->
line = meta[:line]
{line, lhs, rhs}
end)
end
# Reduces a list of environments, ensuring that the type and session type are the same
# In case of error, the error line number may be nil
defp process_cases_result(all_cases) when is_list(all_cases) do
Enum.reduce_while(all_cases, hd(all_cases), fn curr_case, acc ->
case curr_case do
{:error, line, message} ->
{:halt, {:error, line, message}}
_ ->
common_type = TypeOperations.equal?(curr_case[:type], acc[:type])
if common_type == false do
{:halt,
{:error, nil,
"Types #{inspect(curr_case[:type])} and #{inspect(acc[:type])} do not match. Different " <>
"cases should have end up with the same type."}}
else
if ST.equal?(curr_case[:session_type], acc[:session_type]) do
{:cont, %{curr_case | type: curr_case[:type]}}
else
{:halt,
{:error, nil,
"Mismatch in session type following the case: " <>
"#{ST.st_to_string(curr_case[:session_type])} and " <>
"#{ST.st_to_string(acc[:session_type])}"}}
end
end
end
end)
end
# Append line numbers + message to the current environment.
# If errors occur withing a sub-expression, then append the error_lines to keep a stack of error lines
defp append_error(env, {:error, line, {:inner_error, existing_error_lines, message}}) do
%{env | state: :error, error_lines: append_error_line(existing_error_lines, line), error_data: message}
end
defp append_error(env, {:error, line, message}) do
%{env | state: :error, error_lines: append_error_line(env.error_lines, line), error_data: message}
end
defp append_error_line(error_lines, nil) do
error_lines
end
defp append_error_line(error_lines, line) when is_list(error_lines) do
[line | error_lines]
end
defp process_binary_operations(node, meta, operator, arg1, arg2, allowed_type, any_type, is_comparison, env) do
{_op1_ast, op1_env} = Macro.prewalk(arg1, env, &typecheck/2)
{_op2_ast, op2_env} = Macro.prewalk(arg2, env, &typecheck/2)
line = meta[:line]
try do
if op1_env[:state] == :error do
throw({:error, {:inner_error, op1_env[:error_lines], op1_env[:error_data]}})
end
if op2_env[:state] == :error do
throw({:error, {:inner_error, op2_env[:error_lines], op2_env[:error_data]}})
end
cond do
is_comparison ->
{node,
%{
op1_env
| type: :boolean,
variable_ctx: Map.merge(op1_env[:variable_ctx], op2_env[:variable_ctx] || %{})
}}
operator == :| ->
same_type = TypeOperations.equal?({:list, op1_env[:type]}, op2_env[:type])
if same_type do
{node,
%{
op1_env
| type: {:list, op1_env[:type]},
variable_ctx: Map.merge(op1_env[:variable_ctx], op2_env[:variable_ctx] || %{})
}}
else
{node,
%{
op1_env
| state: :error,
error_lines: append_error_line(env[:error_lines], line),
error_data:
"Operator type problem in [a | b]: b should be a list of the type of a. Found " <>
"#{TypeOperations.string(op1_env[:type])}, #{TypeOperations.string(op2_env[:type])}"
}}
end
true ->
same_type = TypeOperations.equal?(op1_env[:type], op2_env[:type])
if same_type do
if any_type || TypeOperations.equal?(op1_env[:type], allowed_type) do
{node,
%{
op1_env
| type: op1_env[:type],
variable_ctx: Map.merge(op1_env[:variable_ctx], op2_env[:variable_ctx] || %{})
}}
else
throw(
{:error,
"Operator type problem in #{Atom.to_string(operator)}: #{TypeOperations.string(op1_env[:type])}, " <>
"#{TypeOperations.string(op2_env[:type])} is not of type #{inspect(allowed_type)}"}
)
end
else
throw(
{:error,
"Operator type problem in #{Atom.to_string(operator)}: #{TypeOperations.string(op1_env[:type])}, " <>
"#{TypeOperations.string(op2_env[:type])} are not of the same type"}
)
end
end
catch
{:error, details} ->
{node, append_error(env, {:error, line, details})}
end
end
defp process_unary_operations(node, meta, arg1, expected_type, env) do
{_op1_ast, op1_env} = Macro.prewalk(arg1, env, &typecheck/2)
case op1_env[:state] do
:error ->
{node, op1_env}
_ ->
same_type = TypeOperations.equal?(op1_env[:type], expected_type)
if same_type do
{node, op1_env}
else
{node,
%{
op1_env
| state: :error,
error_lines: append_error_line(env[:error_lines], meta[:line]),
error_data: "Type problem: Found expression of type #{inspect(op1_env[:type])} but expected a #{inspect(expected_type)}"
}}
end
end
end
defp tuple_to_list({arg1, arg2}) do
[arg1, arg2]
end
defp tuple_to_list({:{}, _, args}) do
args
end
# defp remove_nils(list) do
# Enum.filter(
# list,
# fn
# {nil, _} -> false
# _ -> true
# end
# )
# end
# defp error_message(message, meta) do
# line =
# if meta[:line] do
# "[Line #{meta[:line]}] "
# else
# ""
# end
# line <> message
# end
defp lookup_function(all_functions, {name, arity}) do
try do
{:ok, lookup_function!(all_functions, {name, arity})}
catch
{:error, x} -> {:error, x}
end
end
defp lookup_function!(all_functions, {name, arity}) do
if all_functions[{name, arity}] do
all_functions[{name, arity}]
else
# Function does not exist in current module
throw({:error, "Function #{name}/#{arity} was not found in the current module."})
end
end
end