defmodule LixParser do use LixParser.Types @moduledoc ~S""" # Abstract Lix is a simple Lisp dialect which is parsed by `LixParser.parse` into an AST. This AST can then be interpreted as seen fit. An example of a useful interpretation can be found in [`LixEval`](https://hex.pm/packages/lix_eval) which defines a minimum of special forms (`def`, `fn`, `defn`, `let` and `use`) The syntax of Lix is defined by the following doctests # Syntax ## Literals ### Numbers iex(1)> parse("42") {:ok, {:lit, 42}} As long as we get an `:ok` result we can use `parse!` too iex(2)> parse!("43") {:lit, 43} There are floats too iex(3)> parse!("-44.0") {:lit, -44.0} ### Atoms iex(4)> parse!(":atom") {:lit, :atom} ### Strings iex(5)> parse!(~s{"hello"}) {:lit, "hello"} ## Identifiers As no semantic checks can be made by the parser, the ast is augemented by the position in case of all other constructs. This will allow an interpreter, evaluator or semantic checker to point to the correct position in the source. iex(6)> parse!("variable") {:id, {1, 1}, ~W[variable]} The name of the identifier is a list because we can have compound names, as follows iex(7)> parse!(" alpha.beta") {:id, {3, 1}, ~W[alpha beta]} And we can have more of this of course iex(8)> parse!("\n\t a.b.c") {:id, {4, 2}, ~W[a b c]} Note that positioning does not know about tabwidth in this version of [`Minipeg`](https://gitlab.com/lab42-hex-projects/minipeg) and thus just assumes tabwidth 1 Operators are just identifiers too iex(9)> parse!("+") {:id, {1, 1}, ["+"]} They can consist of up to three symbols iex(10)> parse!("@@-") {:id, {1, 1}, ["@@-"]} ## Compound Expressions Literals and Identifiers are the **only scalar** expressions in Lix. They can be combined into lists (`(...)`), quoted lists (`[...]`) and maps (`{...}`) as follows ### Lists (s-expressions) iex(11)> parse!("(+ 2 4)") {:list, {1, 1}, [{:id, {2, 1}, ["+"]}, {:lit, 2}, {:lit, 4}]} ### Quoted lists iex(12)> parse!(~s{[(:a) "b"]}) {:quoted, {1, 1}, [{:list, {2, 1}, [lit: :a]}, {:lit, "b"}]} ### Maps iex(13)> parse!("{:a 1 b 2}") {:map, {1, 1}, [{:lit, :a}, {:lit, 1}, {:id, {7, 1}, ~W[b]}, {:lit, 2}]} ## Error Handling Few, experienced with parsing, would argue that error handling is the most difficult aspect of good parsing. In this early version of the `LixParser` error handling is very rudimentary, which is just an euphemisim for _outright terrible_. Hopefully this will get better along with better support from the underlying PEG library which is Minipeg. ### Bad number This should not parse, but does iex(14)> parse("42.") {:ok, {:lit, 42}} iex(15)> parse(".42") {:error, "no alternative could be parsed in :1,1"} Missing closing parens iex(16)> parse("(") {:error, "no alternative could be parsed in :1,1"} Odd number of elements in a map iex(17)> parse("{:a 1 :b}") {:error, "no alternative could be parsed in :1,1"} """ @spec parse(binary()) :: result_t() defdelegate parse(input), to: LixParser.Parser @spec parse!(binary()) :: ast_t() def parse!(input) do # N.B. No wuth here {:ok, ast} = parse(input) ast end end # SPDX-License-Identifier: Apache-2.0