Current section
Files
Jump to
Current section
Files
lib/parser.ex
defmodule Sammal.Parser do
@moduledoc """
A simple recursive parser for a Scheme-ish language.
"""
alias Sammal.{Expr, SammalError}
# TODO explore macros to clear up pattern matches?
@typedoc """
A form consists of a list of expressions.
"""
@type form :: [Expr]
@typedoc """
All the parse_* functions adhere to the following pattern:
{AST, tokens} -> {:ok, {new AST, remaining tokens}} | {:error, error struct}
"""
@type cursor :: {ast :: [form], remaining :: [Expr]}
@doc """
Parser entrypoint.
## Example
iex> {:ok, tokens} = Sammal.Tokenizer.tokenize("(define (x 10))")
iex> {:ok, {[[define, [_var, _val]]], []}} = Sammal.Parser.parse(tokens)
iex> define
%Sammal.Expr{ctx: "(define (x 10))", lex: "define", line: 0, row: 1, val: :define}
"""
@spec parse([Expr]) :: {:ok, cursor} | {:error, SammalError}
def parse(tokens), do: parse_all_expressions({[], tokens})
@doc """
Parse all parenthesis-enclosed expressions.
"""
@spec parse_all_expressions(cursor) :: {:ok, cursor} | {:error, SammalError}
def parse_all_expressions({ast, []}), do: {:ok, {ast, []}}
def parse_all_expressions({_ast, _tokens} = input) do
case parse_next(input) do
# TODO use helper
{:ok, {val, rest}} ->
parse_all_expressions({val, rest})
{:error, error} ->
{:error, error}
end
end
# TODO
# input
# |> first_parser
# |> _combine(second_parser)
# def pipe_parsers({ast, tokens} = input, parser) when is_function(parser) do
# case parser(input) do
# {:ok, {val, rest}} ->
# end
# end
@doc """
Parse a single parenthesis-enclosed expression.
"""
@spec parse_expression(cursor) :: {:ok, cursor} | {:error, SammalError}
def parse_expression({ast, [%Expr{lex: "("} = head | tokens]}) do
case parse_until({[], tokens}, ")") do
{:ok, {val, rest}} ->
{:ok, {ast ++ [val], rest}}
# TODO helper available?
{:error, nil} ->
{:error, SammalError.new(:unmatched_paren, head)}
{:error, error} ->
{:error, error}
end
end
def parse_expression({_, [t | _]}) do
{:error, SammalError.new(:unexpected, t, "(")}
end
@doc """
Parse the next atom or expression.
"""
@spec parse_next(cursor) :: {:ok, cursor} | {:error, SammalError}
def parse_next({ast, tokens} = input) do
# TODO instead of checking the first token, try parse_expression first and fallback to others? parser combinator-ish style
case tokens do
[%Expr{lex: "("} = head, %Expr{lex: ")"} | ts] ->
{:ok, {ast ++ [%{head | lex: "()", val: []}], ts}}
[%Expr{lex: "("} | _] ->
parse_expression(input)
[%Expr{lex: "'"} = expr | ts] ->
# TODO use helper
case parse_next({[], ts}) do
{:ok, {val, rest}} ->
{:ok, {ast ++ [[%{expr | val: :quote} | val]], rest}}
{:error, error} ->
{:error, error}
end
[%Expr{} = expr | rest] ->
{:ok, {ast ++ [expr], rest}}
end
end
@doc """
Parse up to (and including) the given lexeme.
"""
@spec parse_until(cursor, String.t) :: {:ok, cursor} | {:error, nil}
def parse_until({_, []}, _), do: {:error, nil} # Error struct is formed higher up the call stack where context is available
def parse_until({ast, [%Expr{lex: until} | rest]}, until), do: {:ok, {ast, rest}}
def parse_until({_ast, _tokens} = input, until) do
# TODO use helper
case parse_next(input) do
{:ok, val} -> parse_until(val, until)
{:error, error} -> {:error, error}
end
end
end