Skip to content

Instantly share code, notes, and snippets.

@rrichardsonv
Created May 8, 2020 17:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rrichardsonv/0c8c138e3aa821630ab035ceedd53826 to your computer and use it in GitHub Desktop.
Save rrichardsonv/0c8c138e3aa821630ab035ceedd53826 to your computer and use it in GitHub Desktop.
Shunting yard elixir implementation for keyword list tokens
defmodule ShuntOnEm do
@moduledoc """
Shunting yard algorithm as explained at https://en.wikipedia.org/wiki/Shunting-yard_algorithm
Refactored and repurposed from https://gist.github.com/pixyj/73bebd14be17ce680e9219f642044964
For use, primarily in traversing tokens produced by the wonderful nimbleparsec
## Examples
```
iex> tokens = [
{:left, [val: "(", rank: 21]},
{:num, [val: "3"]},
{:op, [val: "*", rank: 15, key: :binary_expression]},
{:num, [val: "2"]},
{:op, [val: "+", rank: 14, key: :binary_expression]},
{:num, [val: "5"]},
{:right, [val: ")", rank: 21]},
{:op, [val: "*", rank: 15, key: :binary_expression]},
{:num, [val: "4"]},
]
iex> tokens |> ShuntingYard.parse()
[
binary_expression: [
operator: "*",
right: {:num, [val: "4"]},
left: {:binary_expression,
[
operator: "+",
right: {:num, [val: "5"]},
left: {:binary_expression,
[operator: "*", right: {:num, [val: "2"]}, left: {:num, [val: "3"]}]}
]}
]
]
```
"""
@type token :: {atom(), Keyword.t()}
@spec add_to_queue(token, [token()], integer()) :: [token()]
defp add_to_queue(token, output_queue, call_id) do
output_queue ++ [token]
end
@spec has_parens?([token()]) :: boolean()
defp has_parens?(stack) do
Enum.any?(stack, fn
{k, _} when k in [:left, :right] -> true
_ -> false
end)
end
@spec parse([token()]) :: [token()]
def parse(tokens) do
{_, output_queue, stack} = parse_impl(tokens, [], [])
if has_parens?(stack) do
raise "Mismatched parens error"
end
to_tree(output_queue, stack)
end
defp to_tree(queue, stack), do: do_to_tree(Enum.reverse(queue), stack)
defp do_to_tree(queue, []), do: queue
defp do_to_tree(queue, [token | stack]) do
{children, rest} = Enum.split(queue, 2)
do_to_tree([format_op_token(token, children) | rest], stack)
end
defp format_op_token({:op, props}, children) do
{token_key, props} = Keyword.pop(props, :key)
token_props =
Enum.reduce(props, [], fn
{:val, val}, acc ->
[{:operator, val} | acc]
{:rank, _}, acc ->
acc
entry, acc ->
acc ++ [entry]
end)
{token_key, add_children(token_props, children)}
end
defp add_children(props, [right, left]), do: props ++ [right: right, left: left]
@spec parse_impl([token()], [token()], [token()]) :: {[token()], [token()], [token()]}
def parse_impl([], output_queue, stack), do: {[], output_queue, stack}
def parse_impl([first | rest], output_queue, stack) do
{output_queue, stack} = add_token(first, output_queue, stack)
parse_impl(rest, output_queue, stack)
end
@spec add_token(token(), [token()], [token()]) :: {[token()], [token()]}
def add_token({:num, _} = token, output_queue, stack),
do: {add_to_queue(token, output_queue, 1), stack}
def add_token({:left, _} = token, output_queue, stack),
do: {output_queue, [token | stack]}
def add_token({:right, _} = token, output_queue, stack),
do: add_right_paren(token, output_queue, stack)
def add_token({:op, _} = token, output_queue, stack),
do: add_operator(token, output_queue, stack)
def add_token(_token, _output_queue, _stack),
do: raise "Invalid token present"
@spec add_right_paren(token(), [token()], [token()]) :: {[token()], [token()]}
defp add_right_paren(_token, _output_queue, []),
do: raise "Matching parens not found"
defp add_right_paren(_token, output_queue, [{:left, _} | rest]),
do: {output_queue, rest}
defp add_right_paren(token, output_queue, [top_op | rest]) do
condensed_queue =
output_queue
|> to_tree([top_op])
|> Enum.reverse()
add_right_paren(token, condensed_queue, rest)
end
@spec add_operator(token(), [token()], [token()]) :: {[token()], [token()]}
defp add_operator(token, output_queue, stack) do
{partial_queue, s1} = pop_higher_precendence_ops_to_queue(token, [], stack)
next_queue =
output_queue
|> to_tree(partial_queue)
|> Enum.reverse()
{next_queue, [token | s1]}
end
defp pop_higher_precendence_ops_to_queue(_token, q, []),
do: {q, []}
defp pop_higher_precendence_ops_to_queue(_token, q, [{op_type, _} | _] = stack)
when op_type in [:right, :left], do: {q, stack}
defp pop_higher_precendence_ops_to_queue(token, q, [top_op | rest] = stack) do
[token, top_op]
|> Enum.map(fn {_, props} -> props end)
|> Enum.map(&Keyword.fetch!(&1, :rank))
|> case do
[token_rank, top_rank] when token_rank <= top_rank ->
pop_higher_precendence_ops_to_queue(token, q ++ [top_op], rest)
_ ->
{q, stack}
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment