defmodule ElixirST.Parser do
@moduledoc """
Parses an input string to session types (as Elixir data).
"""
alias ElixirST.ST
require ST
@typedoc false
@type session_type :: ST.session_type()
@typep session_type_tuple() :: ST.session_type_tuple()
@typep label :: ST.label()
@doc """
Parses a string into a session type data structure
## Example
iex> s = "rec Y.(+{!Hello(number, [{boolean, atom}]).Y, !Ok()})"
...> session_type = ElixirST.Parser.parse(s)
...> ElixirST.ST.st_to_string(session_type)
"rec Y.(+{!Hello(number, [{boolean, atom}]).Y, !Ok()})"
"""
@spec parse(bitstring()) :: session_type()
def parse(session_type) when is_bitstring(session_type) do
st =
session_type
|> String.to_charlist()
|> parse_charlist()
validate!(st)
st
end
@spec parse_charlist(charlist()) :: session_type()
defp parse_charlist(session_type_charlist) do
with {:ok, tokens, _} <- lexer(session_type_charlist) do
if tokens == [] do
# Empty input
%ST.Terminate{}
else
case :parser.parse(tokens) do
{:ok, session_type} ->
convert_to_structs(session_type, [])
{:error, {_line, :parser, error_info}} ->
throw("Error while parsing session type #{session_type_charlist}: #{:parser.format_error(error_info)}")
end
end
else
{:error, {_line, :lexer, error_info}, 1} ->
# todo: cuter error message needed
throw("Error in syntax of the session type #{session_type_charlist}: #{:lexer.format_error(error_info)}")
end
end
defp lexer(string) do
# IO.inspect tokens
:lexer.string(string)
end
# Convert session types from Erlang records to Elixir Structs.
# Throws error in case of branches/choices with same labels, or
# if the types are not valid.
@spec convert_to_structs(
# should be { , , [atom], }
{:send, atom, any, session_type_tuple()}
| {:recv, atom, any, session_type_tuple()}
| {:choice, [session_type_tuple()]}
| {:branch, [session_type_tuple()]}
| {:call, atom}
| {:recurse, atom, session_type_tuple(), boolean()}
| {:terminate},
[label()]
) :: session_type()
defp convert_to_structs(session_type, recurse_var)
defp convert_to_structs({:terminate}, _recurse_var) do
%ST.Terminate{}
end
defp convert_to_structs({send_recv, label, types, next}, recurse_var) when send_recv in [:send, :recv] do
checked_types = Enum.map(types, &ElixirST.TypeOperations.valid_type/1)
Enum.each(checked_types, fn
# todo fix. add line number
{:error, incorrect_types} -> throw("Invalid type/s: #{inspect(incorrect_types)}")
_ -> :ok
end)
case send_recv do
:send ->
%ST.Send{label: label, types: checked_types, next: convert_to_structs(next, recurse_var)}
:recv ->
%ST.Recv{label: label, types: checked_types, next: convert_to_structs(next, recurse_var)}
end
end
defp convert_to_structs({:choice, choices}, recurse_var) do
%ST.Choice{
choices:
Enum.reduce(
choices,
%{},
fn choice, map ->
converted_st = convert_to_structs(choice, recurse_var)
label = label(converted_st)
if Map.has_key?(map, label) do
throw("Cannot insert multiple choices with same label: #{label}.")
else
Map.put(map, label, converted_st)
end
end
)
}
end
defp convert_to_structs({:branch, branches}, recurse_var) do
%ST.Branch{
branches:
Enum.reduce(
branches,
%{},
fn branch, map ->
converted_st = convert_to_structs(branch, recurse_var)
label = label(converted_st)
if Map.has_key?(map, label) do
throw("Cannot insert multiple branches with same label: #{label}.")
else
Map.put(map, label, converted_st)
end
end
)
}
end
defp convert_to_structs({:recurse, label, body, outer_recurse}, recurse_var) do
# if label in recurse_var do
# throw("Cannot have multiple recursions with same variable: #{label}.")
# end
%ST.Recurse{label: label, body: convert_to_structs(body, [label | recurse_var]), outer_recurse: outer_recurse}
end
defp convert_to_structs({:call, label}, _recurse_var) do
%ST.Call_Recurse{label: label}
end
defp label(%ST.Send{label: label}) do
label
end
defp label(%ST.Recv{label: label}) do
label
end
defp label(_) do
throw("Following a branch/choice, a send or receive statement is required.")
end
# Performs validations on the session type.
@spec validate!(session_type()) :: boolean()
defp validate!(session_type)
defp validate!(%ST.Send{next: next}) do
validate!(next)
end
defp validate!(%ST.Recv{next: next}) do
validate!(next)
end
defp validate!(%ST.Choice{choices: choices}) do
res =
Enum.map(
choices,
fn
{_label, %ST.Send{next: next}} ->
validate!(next)
{_, other} ->
throw("Session type parsing validation error: Each branch needs a send as the first statement: #{ST.st_to_string(other)}.")
false
_ ->
throw("BAD - check")
end
)
# Return false if one (or more) false are found
Enum.find(res, true, fn x -> !x end)
end
defp validate!(%ST.Branch{branches: branches}) do
res =
Enum.map(
branches,
fn
{_label, %ST.Recv{next: next}} ->
validate!(next)
{_, other} ->
throw("Session type parsing validation error: Each branch needs a receive as the first statement: #{ST.st_to_string(other)}.")
false
_ ->
throw("BAD - check")
end
)
if false in res do
false
else
true
end
end
defp validate!(%ST.Recurse{body: body} = st) do
case body do
%ST.Recurse{} ->
throw("It is unnecessary to having multiple recursions following each other: '#{ST.st_to_string(st)}'")
_ ->
validate!(body)
end
end
defp validate!(%ST.Call_Recurse{}) do
true
end
defp validate!(%ST.Terminate{}) do
true
end
defp validate!(x) do
throw("Validation problem. Unknown input: #{inspect(x)}")
false
end
end