├── .gitignore ├── README.md ├── config └── config.exs ├── examples ├── number_adder.exs ├── roman_numerals.exs └── simple_xml.exs ├── lib ├── ex_spirit.ex └── ex_spirit │ ├── detail │ └── tree_map.ex │ ├── parser.ex │ ├── parser │ └── text.ex │ └── parserx.ex ├── mix.exs ├── mix.lock └── test ├── ex_spirit ├── parser_test.exs └── parserx_test.exs ├── ex_spirit_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # And ignore the elixir language server temp files 23 | /.elixir_ls 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExSpirit 2 | 3 | Spirit-style PEG-like parsing library for Elixir. 4 | 5 | Please see `ExSpirit.Parser` for details about the parser. 6 | 7 | ## Installation 8 | 9 | Available in [hex.pm](https://hex.pm/packages/ex_spirit). 10 | 11 | Add to dependencies with: 12 | 13 | ```elixir 14 | def deps do 15 | [{:ex_spirit, "~> 0.1"}] 16 | end 17 | ``` 18 | 19 | Full docs can be found at: 20 | 21 | ## Examples 22 | 23 | See the examples directory for examples and run them with `mix run examples/`. 24 | 25 | Current examples are: 26 | 27 | ### number_adder.exs 28 | 29 | Takes a list of simple integers of base 10, separated by commas, with optional spaces between them, adds them together, and returns them (all within the parser), requires at least one number. 30 | 31 | Example Run: 32 | 33 | ```text 34 | $ mix run examples/number_adder.exs 35 | Input simple number separated by comma's and optionally spaces and press enter: 36 | 37 | :1:1: Parse error: Parsing uint with radix of 10 had 0 digits but 1 minimum digits were required 38 | RuleStack: [added_number] 39 | Input: 40 | 41 | $ mix run examples/number_adder.exs 42 | d 43 | Input simple number separated by comma's and optionally spaces and press enter: 44 | :1:1: Parse error: Parsing uint with radix of 10 had 0 digits but 1 minimum digits were required 45 | RuleStack: [added_number] 46 | Input: d 47 | 48 | $ mix run examples/number_adder.exs 49 | Input simple number separated by comma's and optionally spaces and press enter: 50 | 42 51 | Result: 42 52 | 53 | $ mix run examples/number_adder.exs 54 | Input simple number separated by comma's and optionally spaces and press enter: 55 | 1,2,3 , 4, 5 ,6 , 7 56 | Result: 28 57 | 58 | $ mix run examples/number_adder.exs 59 | Input simple number separated by comma's and optionally spaces and press enter: 60 | 1 , 61 | Result: 1 62 | Leftover: " ,\n" 63 | ``` 64 | 65 | ### roman_numerals.exs 66 | 67 | Takes a typed in roman numeral from stdin and an enter, parses out the number up to the thousands position and reports back any errors and remaining leftovers. 68 | 69 | Example Run: 70 | 71 | ```text 72 | $ mix run examples/roman_numerals.exs 73 | Input Roman Numerals and press enter: 74 | MDMXXIV 75 | Result: 1924 76 | 77 | $ mix run examples/roman_numerals.exs 78 | Input Roman Numerals and press enter: 79 | zzzz 80 | Result: 0 81 | Leftover: "zzzz\n" 82 | 83 | $ mix run examples/roman_numerals.exs 84 | Input Roman Numerals and press enter: 85 | XVIzzz 86 | Result: 16 87 | Leftover: "zzz\n" 88 | ``` 89 | 90 | ### simple_xml.exs 91 | 92 | A simple xml parser, no attributes, just nodes and text. 93 | 94 | Example Run: 95 | 96 | ```text 97 | $ mix run examples/simple_xml.exs 98 | Input a single line of xml-like syntax: 99 | Some textHi and more 100 | Result: {"test1", ["Some text", {"test2", ["Hi"]}, " and more"]} 101 | 102 | $ mix run examples/simple_xml.exs 103 | Input a single line of xml-like syntax: 104 | How about an improperly terminated tag 105 | :1:48: Expectation Failure: literal `a-tag` did not match the input 106 | RuleStack: [tag, node_] 107 | Input: b-tag> 108 | 109 | $ mix run examples/simple_xml.exs 110 | Input a single line of xml-like syntax: 111 | < 112 | :1:1: Parse error: Repeating over a parser failed due to not reaching the minimum amount of 1 with only a repeat count of 0 113 | RuleStack: [text, node_] 114 | Input: < 115 | ``` 116 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :ex_spirit, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:ex_spirit, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /examples/number_adder.exs: -------------------------------------------------------------------------------- 1 | defmodule NumberAdder do 2 | use ExSpirit.Parser, text: true 3 | 4 | defrule added_number( 5 | seq([ 6 | uint(), 7 | repeat(char(?,) |> uint()) 8 | ]) 9 | ), map: Enum.sum() 10 | 11 | def from_string(input) do 12 | parse(input, seq([added_number(), ignore(repeat(char(?\s)))]), skipper: repeat(char(?\s))) 13 | end 14 | 15 | end 16 | 17 | IO.puts("Input simple number separated by comma's and optionally spaces and press enter: ") 18 | case IO.read(:line) |> NumberAdder.from_string() do 19 | %{error: nil, result: result, rest: "\n"} -> IO.puts("Result: #{inspect result}") 20 | %{error: nil, result: result, rest: rest} -> IO.puts("Result: #{inspect result}\nLeftover: #{inspect rest}") 21 | %{error: error} -> IO.puts("#{Exception.message(error)}") 22 | end 23 | -------------------------------------------------------------------------------- /examples/roman_numerals.exs: -------------------------------------------------------------------------------- 1 | defmodule RomanNumerals do 2 | use ExSpirit.Parser, text: true 3 | 4 | defrule thousands( 5 | alt([ 6 | lit(?M) |> success(1000), 7 | success(0), 8 | ]) 9 | ) 10 | 11 | defrule hundreds(context) do 12 | import ExSpirit.TreeMap 13 | symbols_ = new() 14 | |> add_text("" , 0) 15 | |> add_text("C" , 100) 16 | |> add_text("CC" , 200) 17 | |> add_text("CCC" , 300) 18 | |> add_text("CD" , 400) 19 | |> add_text("D" , 500) 20 | |> add_text("DC" , 600) 21 | |> add_text("DCC" , 700) 22 | |> add_text("DCCC", 800) 23 | |> add_text("DM" , 900) 24 | context |> symbols(symbols_) 25 | end 26 | 27 | defrule tens(context) do 28 | import ExSpirit.TreeMap 29 | symbols_ = new() 30 | |> add_text("" , 0) 31 | |> add_text("X" , 10) 32 | |> add_text("XX" , 20) 33 | |> add_text("XXX" , 30) 34 | |> add_text("XL" , 40) 35 | |> add_text("L" , 50) 36 | |> add_text("LX" , 60) 37 | |> add_text("LXX" , 70) 38 | |> add_text("LXXX" , 80) 39 | |> add_text("XC" , 90) 40 | context |> symbols(symbols_) 41 | end 42 | 43 | defrule ones(context) do 44 | import ExSpirit.TreeMap 45 | symbols_ = new() 46 | |> add_text("" , 0) 47 | |> add_text("I" , 1) 48 | |> add_text("II" , 2) 49 | |> add_text("III" , 3) 50 | |> add_text("IV" , 4) 51 | |> add_text("V" , 5) 52 | |> add_text("VI" , 6) 53 | |> add_text("VII" , 7) 54 | |> add_text("VIII" , 8) 55 | |> add_text("IX" , 9) 56 | context |> symbols(symbols_) 57 | end 58 | 59 | defrule roman_numerals( 60 | seq([ 61 | thousands(), 62 | hundreds(), 63 | tens(), 64 | ones(), 65 | ]) 66 | ), map: Enum.sum() 67 | 68 | def from_string(input) do 69 | parse(input, roman_numerals()) 70 | end 71 | 72 | end 73 | 74 | IO.puts("Input Roman Numerals and press enter: ") 75 | case IO.read(:line) |> RomanNumerals.from_string() do 76 | %{error: nil, result: result, rest: "\n"} -> IO.puts("Result: #{inspect result}") 77 | %{error: nil, result: result, rest: rest} -> IO.puts("Result: #{inspect result}\nLeftover: #{inspect rest}") 78 | %{error: error} -> IO.puts("#{Exception.message(error)}") 79 | end 80 | -------------------------------------------------------------------------------- /examples/simple_xml.exs: -------------------------------------------------------------------------------- 1 | defmodule SimpleXML do 2 | use ExSpirit.Parser, text: true 3 | 4 | defrule text( chars(-?<) ) 5 | 6 | defrule tag_name( chars([?a..?z, ?A..?Z, ?0..?9, ?_, ?-]) ) 7 | 8 | defrule tag( 9 | lit(?<) |> tag_name() |> put_state(:tagname, :result) |> lit(?>) |> expect(seq([ 10 | get_state_into(:tagname, tag(&1, repeat(node_()))), 11 | lit(") 12 | ])) 13 | ) 14 | 15 | defrule node_( 16 | alt([ 17 | tag(), 18 | text(), 19 | ]) 20 | ) 21 | 22 | def from_string(input) do 23 | parse(input, node_()) 24 | end 25 | 26 | end 27 | 28 | IO.puts("Input a single line of xml-like syntax: ") 29 | case IO.read(:line) |> SimpleXML.from_string() do 30 | %{error: nil, result: result, rest: "\n"} -> IO.puts("Result: #{inspect result}") 31 | %{error: nil, result: result, rest: rest} -> IO.puts("Result: #{inspect result}\nLeftover: #{inspect rest}") 32 | %{error: error} -> IO.puts("#{Exception.message(error)}") 33 | end 34 | -------------------------------------------------------------------------------- /lib/ex_spirit.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSpirit do 2 | end 3 | -------------------------------------------------------------------------------- /lib/ex_spirit/detail/tree_map.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSpirit.TreeMap do 2 | 3 | defstruct root: %{} 4 | 5 | def new(), do: %__MODULE__{} 6 | 7 | def add_text(tm, str, value) when is_binary(str), do: add(tm, String.to_charlist(str), value) 8 | def add(tm, bin, value) when is_binary(bin), do: add(tm, :erlang.binary_to_list(bin), value) 9 | def add(tm, list, value) when is_list(list) do 10 | %{tm | 11 | root: add_(tm.root, list, value) 12 | } 13 | end 14 | 15 | defp add_(map, [], value), do: Map.put(map, [], value) 16 | defp add_(map, [c | rest], value) do 17 | case map[c] do 18 | nil -> Map.put_new(map, c, add_(%{}, rest, value)) 19 | submap -> Map.put(map, c, add_(submap, rest, value)) 20 | end 21 | end 22 | 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/ex_spirit/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSpirit.Parser do 2 | @moduledoc """ 3 | 4 | `ExSpirit.Parser` is the parsing section of ExSpirit, designed to parse out some 5 | kind of stream of data (whether via a binary, a list, or perhaps an actual 6 | stream) into a data structure of your own design. 7 | 8 | 9 | ## Definitions 10 | 11 | - **Terminal Parser**: 12 | A terminal parser is one that does not operate over any other parser, it is 13 | 'terminal' in its location. 14 | 15 | - **Combination Parser**: 16 | A combination parser is one that takes a parser as an input and does something 17 | with it, whether that is repeating it, surrounding it, or ignoring its output 18 | as a few examples. 19 | 20 | ## Usage 21 | 22 | Just add `use ExSpirit.Parser` to a module to make it into a parsing module. 23 | 24 | To add text parsing functions from the `ExSpirit.Parsing.Text` module then add 25 | `text: true` to the use call. For example: 26 | 27 | defmodule MyModule do 28 | use ExSpirit.Parser, text: true 29 | end 30 | 31 | Note that the `ExSpirit.Parser` module must be `use`ed, and not `import`ed! 32 | The functions and macros below are meant to be defined inside your own module 33 | throught code generation orchestrated by the `__using__` macro. 34 | 35 | Importing the module will not bring any useful functions or macros into your scope, 36 | only "virtual" functions and macros that are used for documentation only. 37 | """ 38 | 39 | defmodule Context do 40 | @moduledoc """ 41 | This structure carries the state of the parser. 42 | Contains the following keys: 43 | 44 | * `:filename` - the name of the file (or `""` if no file is specified) 45 | * `:position` - index of the current byte in the input. 46 | * `:line` - current line number (starting at `1`) 47 | * `:column` - current column number (starting at `1`) 48 | * `:rest` - what's left of the input at this point 49 | * `:skipper` - the parser to be used as skipper 50 | * `:result` - the result at this point 51 | * `:error` - the error at this point. If there is no error, it will be `nil`. 52 | you can match on this key to see if the previous parser has failed. 53 | If the following matches, the parser hasn't failed: `%{error: nil} = context`. 54 | * `:rulestack` - The stack of rules at the current position. 55 | Useful for debugging purposes 56 | * `:state` - 57 | * `:userdata` - arbitrary data, defined by the user. 58 | This is useful for a number of things, like keeping track of indentation levels 59 | in indentation-sensitive languages, like [Python], [Haskell] or [Pug]. 60 | 61 | [Python]: https://www.python.org/ 62 | [Haskell]: https://www.haskell.org/ 63 | [Pug]: https://pugjs.org/api/getting-started.html 64 | 65 | The `Context` is always available at any moment, and ExSpirit provides 66 | certain utilities to make it easy to access and update the context. 67 | 68 | In fact, ExSprit is a very general parser that can be viewed as a 69 | pipeline composed of a sequence of transformations, each of which 70 | takes up a context and return a new context: 71 | 72 | new_context = parse_rule(old_context) 73 | 74 | The new context might depend not only on the remainder of the input (`:rest`), 75 | but also on any of the values contained in the context. 76 | This is what makes it possible to use ExSpirit for context-sensitive languages. 77 | Many parsers hide the context from the user. 78 | While hiding the context *might* create a cleaner API, it makes it harder 79 | to have good error reporting and to store position information along the parse tree. 80 | It also makes it impossible to parse context-sensitive languages, such as 81 | XML, HTML, as well as most indentation-sensitive languages (as described above). 82 | ExSpirit, being a fully general parser, has no such limitations. 83 | 84 | When parsing context-free languages (like many programming languages), 85 | you don't need anything more powerful than a PEG parser, which doesn't require 86 | access to the context, except maybe for position tagging. 87 | 88 | In that case, you can use ExSpirit while ignoring the context. 89 | It will be transparently threaded through your rules, and you can focus 90 | only on the stream you're parsing, and ignore the other parameters. 91 | Just imagine that the parsers are taking up elements of a stream 92 | (e.g. characters from a string) and returning a result. 93 | 94 | ## State System 95 | 96 | The state system uses the following parsers to update the state: 97 | 98 | * `ExSpirit.Parser.put_state/3` 99 | * `ExSpirit.Parser.push_state/3` 100 | 101 | And the following parser tp get the state into another parser: 102 | 103 | * `ExSpirit.Parser.get_state_into/3` 104 | 105 | The state system can be very useful in context-sensitive languages, such as XML. 106 | An XML document is composed of matched pairs of opening and closing tags 107 | of the form: `...`. The opening tag must match the closing tag. 108 | 109 | This can't be done without access to the state, because otherwise the parser 110 | that parses the closing tag would be unaware of what the opening tag was. 111 | 112 | Let's look at the following simple XML parser example (full code [here][github_example]) 113 | 114 | 115 | defmodule SimpleXML do 116 | use ExSpirit.Parser, text: true 117 | 118 | defrule text( chars(-?<) ) 119 | defrule tag_name( chars([?a..?z, ?A..?Z, ?0..?9, ?_, ?-]) ) 120 | 121 | defrule tag( 122 | # We put the result of the parser into the state... 123 | lit(?<) |> tag_name() |> put_state(:tagname, :result) |> lit(?>) |> expect(seq([ 124 | # ... we get the restult into the state, and feed it 125 | # into the parser responsible for parsing the closing tag 126 | get_state_into(:tagname, tag(&1, repeat(node_()))), 127 | lit(") 128 | ])) 129 | ) 130 | 131 | defrule node_( 132 | alt([ 133 | tag(), 134 | text(), 135 | ]) 136 | ) 137 | end 138 | """ 139 | defstruct( 140 | filename: "", 141 | position: 0, # In bytes 142 | line: 1, 143 | column: 1, 144 | rest: "", 145 | skipper: nil, 146 | result: nil, 147 | error: nil, 148 | rulestack: [], 149 | state: %{}, 150 | userdata: nil 151 | ) 152 | end 153 | 154 | defmodule ImportInsteadOfUseException do 155 | defexception name: nil, kind: nil 156 | 157 | def message(exc) do 158 | """ 159 | You have tried to call the `#{exc.name}` #{exc.kind}. 160 | 161 | You seem to have imported the `ExSpirit.Parser` module. 162 | The `ExSpirit.Parser` module must be `use`d and not `import`ed. 163 | Please add to your module: 164 | 165 | `use ExSpirit.Parser` (or `use ExSpirit.Parser, text: true`) instead of 166 | `import ExSpirit.Parser` 167 | """ 168 | end 169 | end 170 | 171 | defmodule ParseException do 172 | defexception message: "", context: %Context{}, extradata: nil 173 | 174 | def message(exc) do 175 | c = exc.context 176 | "#{c.filename}:#{c.line}:#{c.column}: Parse error: #{exc.message}\n\tRuleStack: [#{Enum.join(c.rulestack, ", ")}]\n\tInput: #{String.slice(c.rest, 0, 255)}" 177 | end 178 | end 179 | 180 | defmodule ExpectationFailureException do 181 | defexception message: "", context: %Context{}, extradata: nil 182 | 183 | def message(exc) do 184 | c = exc.context 185 | "#{c.filename}:#{c.line}:#{c.column}: Expectation Failure: #{exc.message}\n\tRuleStack: [#{Enum.join(c.rulestack, ", ")}]\n\tInput: #{String.slice(c.rest, 0, 255)}" 186 | end 187 | 188 | def makeContextFailed(%{error: nil} = _good_context) do 189 | throw "Exceptation Failure Failed due to passing in a context that was actually good!" 190 | end 191 | 192 | def makeContextFailed(%{error: %{message: message, context: context, extradata: extradata}} = bad_context) do 193 | %{bad_context | 194 | result: nil, 195 | error: %__MODULE__{ 196 | message: message, 197 | context: context, 198 | extradata: extradata, 199 | } 200 | } 201 | end 202 | 203 | def makeContextFailed(%{error: %{message: message} = exc} = bad_context) do 204 | %{bad_context | 205 | result: nil, 206 | error: %__MODULE__{ 207 | message: message, 208 | context: bad_context, 209 | extradata: exc, 210 | } 211 | } 212 | end 213 | 214 | def makeContextFailed(%{error: error} = bad_context) do 215 | %{bad_context | 216 | result: nil, 217 | error: %__MODULE__{ 218 | message: inspect(error), 219 | context: bad_context, 220 | extradata: error, 221 | } 222 | } 223 | end 224 | end 225 | 226 | 227 | @doc """ 228 | Defining a rule defines a parser as well as some associated information. 229 | 230 | Such associated information can be the its name for error reporting purposes, 231 | a mapping function so you can convert the output on the fly 232 | (fantastic for in-line AST generation for example!), among others. 233 | 234 | It is used like any other normal terminal rule. 235 | 236 | All of the following examples use this definition of rules in a module: 237 | 238 | defmodule ExSpirit.Parser do 239 | use ExSpirit.Tests.Parser, text: true 240 | 241 | defrule testrule( 242 | seq([ uint(), lit(?\s), uint() ]) 243 | ) 244 | 245 | defrule testrule_pipe( 246 | seq([ uint(), lit(?\s), uint() ]) 247 | ), pipe_result_into: Enum.map(fn i -> i-40 end) 248 | 249 | defrule testrule_fun( 250 | seq([ uint(), lit(?\s), uint() ]) 251 | ), fun: (fn context -> %{context | result: {"altered", context.result}} end).() 252 | 253 | defrule testrule_context(context) do 254 | %{context | result: "always success"} 255 | end 256 | 257 | defrule testrule_context_arg(context, value) do 258 | %{context | result: value} 259 | end 260 | end 261 | 262 | 263 | ## Examples 264 | 265 | # You can use `defrule`s as any other terminal parser 266 | iex> import ExSpirit.Parser 267 | iex> import ExSpirit.Tests.Parser 268 | iex> contexts = parse("42 64", testrule()) 269 | iex> {contexts.error, contexts.result, contexts.rest} 270 | {nil, [42, 64], ""} 271 | 272 | # `defrule`'s also set up a stack of calls down a context so you know 273 | # 'where' an error occured, so name the rules descriptively 274 | iex> import ExSpirit.Parser 275 | iex> import ExSpirit.Tests.Parser 276 | iex> contexts = parse("42 fail", testrule()) 277 | iex> {contexts.error.context.rulestack, contexts.result, contexts.rest} 278 | {[:testrule], nil, "fail"} 279 | 280 | # `defrule`s can map the result to return a different one: 281 | iex> import ExSpirit.Parser 282 | iex> import ExSpirit.Tests.Parser 283 | iex> contexts = parse("42 64", testrule_pipe()) 284 | iex> {contexts.error, contexts.result, contexts.rest} 285 | {nil, [2, 24], ""} 286 | 287 | # `defrule`s can also operate over the context itself to do anything 288 | iex> import ExSpirit.Parser 289 | iex> import ExSpirit.Tests.Parser 290 | iex> contexts = parse("42 64", testrule_fun()) 291 | iex> {contexts.error, contexts.result, contexts.rest} 292 | {nil, {"altered", [42, 64]}, ""} 293 | 294 | # `defrule`s can also be a context function by only passing in `context` 295 | iex> import ExSpirit.Parser 296 | iex> import ExSpirit.Tests.Parser 297 | iex> contexts = parse("42 64", testrule_context()) 298 | iex> {contexts.error, contexts.result, contexts.rest} 299 | {nil, "always success", "42 64"} 300 | 301 | # `defrule`s with a context can have other arguments too, but context 302 | # must always be first 303 | iex> import ExSpirit.Parser 304 | iex> import ExSpirit.Tests.Parser 305 | iex> contexts = parse("42 64", testrule_context_arg(:success)) 306 | iex> {contexts.error, contexts.result, contexts.rest} 307 | {nil, :success, "42 64"} 308 | """ 309 | defmacro defrule({name, _, [parser_ast]}) when is_atom(name), do: defrule_impl(name, parser_ast, []) 310 | 311 | defmacro defrule({name, _, [{:context, _, _} = context_ast | rest_args_ast]}, do: do_ast) when is_atom(name) do 312 | quote location: :keep do 313 | def unquote(name)(unquote(context_ast), unquote_splicing(rest_args_ast)) do 314 | context = unquote(context_ast) 315 | if !valid_context?(context) do 316 | context 317 | else 318 | rule_context = %{context | rulestack: [unquote(name) | context.rulestack]} 319 | return_context = unquote(do_ast) 320 | %{return_context | 321 | rulestack: context.rulestack, 322 | state: context.state, 323 | } 324 | end 325 | end 326 | end 327 | end 328 | 329 | defmacro defrule({name, _, [parser_ast]}, opts) when is_atom(name), do: defrule_impl(name, parser_ast, opts) 330 | 331 | defp defrule_impl(name, parser_ast, opts) do 332 | orig_context_ast = quote do orig_context end 333 | context_ast = quote do context end 334 | quote location: :keep do 335 | def unquote(name)(unquote(orig_context_ast)) do 336 | unquote(context_ast) = %{unquote(orig_context_ast) | rulestack: [unquote(name) | unquote(orig_context_ast).rulestack]} 337 | unquote(context_ast) = unquote(context_ast) |> unquote(parser_ast) 338 | unquote(context_ast) = unquote(defrule_impl_pipe(orig_context_ast, context_ast, opts[:pipe_result_into])) 339 | unquote(context_ast) = unquote(defrule_impl_fun(context_ast, opts[:fun])) 340 | %{unquote(context_ast) | 341 | rulestack: unquote(orig_context_ast).rulestack, 342 | state: unquote(orig_context_ast).state, 343 | } 344 | end 345 | end 346 | end 347 | 348 | defp defrule_impl_pipe(_orig_context_ast, context_ast, nil), do: context_ast 349 | defp defrule_impl_pipe(orig_context_ast, context_ast, map_ast) do 350 | quote location: :keep do 351 | case unquote(context_ast) do 352 | %{error: nil} = good_context -> 353 | result = good_context.result |> unquote(map_ast) 354 | if Exception.exception?(result) do 355 | %{unquote(orig_context_ast) | error: result} 356 | else 357 | %{good_context | result: result} 358 | end 359 | bad_context -> bad_context 360 | end 361 | end 362 | end 363 | 364 | defp defrule_impl_fun(context_ast, nil), do: context_ast 365 | defp defrule_impl_fun(context_ast, fun_ast) do 366 | quote location: :keep do 367 | case unquote(context_ast) do 368 | %{error: nil} = good_context -> 369 | good_context |> unquote(fun_ast) 370 | bad_context -> bad_context 371 | end 372 | end 373 | end 374 | 375 | @doc """ 376 | The parse function is applied to the input and a parser call, such as in: 377 | 378 | ## Examples 379 | iex> import ExSpirit.Parser 380 | iex> import ExSpirit.Tests.Parser 381 | iex> context = parse("42", uint()) 382 | iex> {context.error, context.result, context.rest} 383 | {nil, 42, ""} 384 | """ 385 | defmacro parse(rest, parser, opts \\ []) do 386 | filename = opts[:filename] || quote do "" end 387 | skipper = case opts[:skipper] do 388 | nil -> nil 389 | fun -> quote do fn context -> context |> unquote(fun) end end 390 | end 391 | quote location: :keep do 392 | %ExSpirit.Parser.Context{ 393 | filename: unquote(filename), 394 | skipper: unquote(skipper), 395 | rest: unquote(rest), 396 | } |> unquote(parser) 397 | end 398 | end 399 | 400 | @doc """ 401 | Tests whether the context is valid. 402 | """ 403 | defmacro valid_context?(context_ast) do 404 | quote location: :keep do 405 | case unquote(context_ast) do 406 | %{error: nil} -> true 407 | _ -> false 408 | end 409 | end 410 | end 411 | 412 | @doc false 413 | defmacro valid_context_matcher(), do: quote(do: %{error: nil}) 414 | 415 | @doc """ 416 | Runs the skipper now 417 | 418 | ## Examples 419 | iex> import ExSpirit.Parser 420 | iex> import ExSpirit.Tests.Parser 421 | iex> context = parse(" a", skip(), skipper: chars(?\\s, 0)) 422 | iex> {context.error, context.result, context.rest} 423 | {nil, nil, "a"} 424 | """ 425 | defmacro skip(context_ast) do 426 | quote location: :keep do 427 | case unquote(context_ast) do 428 | %{skipper: nil, error: nil} = context -> context 429 | %{skipper: skipper, error: nil} = context -> 430 | case %{context | skipper: nil} |> skipper.() do 431 | %{error: nil} = skipped_context -> 432 | %{skipped_context | 433 | skipper: context.skipper, 434 | result: context.result, 435 | } 436 | bad_skipped_context -> 437 | %{bad_skipped_context | 438 | skipper: context.skipper, 439 | } 440 | end 441 | bad_context -> bad_context 442 | end 443 | end 444 | end 445 | 446 | @doc """ 447 | The Sequence operator runs all of the parsers in the inline list (cannot be a 448 | variable) and returns their results as a list. 449 | 450 | Any `nil`'s returned are not added to the result list, and if the result list 451 | has only a single value returned then it returns that value straight away 452 | without being wrapped in a list. 453 | 454 | ## Examples 455 | # `seq` parses a sequence returning the return of all of them, removing nils, 456 | # as a list if more than one or the raw value if only one, if any fail then 457 | # all fail. 458 | iex> import ExSpirit.Parser 459 | iex> import ExSpirit.Tests.Parser 460 | iex> contexts = parse("42 64", seq([uint(), lit(" "), uint()])) 461 | iex> {contexts.error, contexts.result, contexts.rest} 462 | {nil, [42, 64], ""} 463 | 464 | # `seq` Here is sequence only returning a single value 465 | iex> import ExSpirit.Parser 466 | iex> import ExSpirit.Tests.Parser 467 | iex> contexts = parse("42Test", seq([uint(), lit("Test")])) 468 | iex> {contexts.error, contexts.result, contexts.rest} 469 | {nil, 42, ""} 470 | """ 471 | defmacro seq(context_ast, [first_seq | rest_seq]) do 472 | # context_binding = quote do context end 473 | quote location: :keep do 474 | case unquote(context_ast) do 475 | %{error: nil} = context -> 476 | context |> unquote(seq_expand(first_seq, rest_seq)) 477 | bad_context -> bad_context 478 | end 479 | end 480 | end 481 | 482 | defp seq_expand(this_ast, [next_ast | rest_ast]) do 483 | quote do 484 | unquote(this_ast) |> case do 485 | %{error: nil, result: nil} = good_context -> 486 | good_context |> unquote(seq_expand(next_ast, rest_ast)) 487 | %{error: nil, result: result} = good_context -> 488 | case good_context |> unquote(seq_expand(next_ast, rest_ast)) do 489 | %{error: nil, result: nil} = return_context -> %{return_context | result: result} 490 | %{error: nil, result: results} = return_context when is_list(results) -> %{return_context | result: [result | results]} 491 | %{error: nil, result: results} = return_context -> %{return_context | result: [result, results]} 492 | bad_context -> bad_context 493 | end 494 | bad_context -> bad_context 495 | end 496 | end 497 | end 498 | defp seq_expand(this_ast, []) do 499 | this_ast 500 | end 501 | 502 | @doc """ 503 | The alternative parser runs the parsers in the inline list (cannot be a 504 | variable) and returns the result of the first one that succeeds, or the error 505 | of the last one. 506 | 507 | ## Examples 508 | iex> import ExSpirit.Parser 509 | iex> import ExSpirit.Tests.Parser 510 | iex> contexts = parse("FF", alt([uint(16), lit("Test")])) 511 | iex> {contexts.error, contexts.result, contexts.rest} 512 | {nil, 255, ""} 513 | 514 | iex> import ExSpirit.Parser 515 | iex> import ExSpirit.Tests.Parser 516 | iex> contexts = parse("Test", alt([uint(16), lit("Test")])) 517 | iex> {contexts.error, contexts.result, contexts.rest} 518 | {nil, nil, ""} 519 | 520 | """ 521 | defmacro alt(context_ast, [first_choice | rest_choices]) do 522 | context_binding = Macro.var(:original_context, :alt) 523 | quote location: :keep do 524 | case unquote(context_ast) do 525 | %{error: nil} = unquote(context_binding) -> 526 | unquote(context_binding) |> unquote(alt_expand(context_binding, [], first_choice, rest_choices)) 527 | bad_context -> bad_context 528 | end 529 | end 530 | end 531 | 532 | defp alt_expand(original_context_ast, err_contexts, this_ast, [next_ast | rest_ast]) do 533 | bad_context = Macro.var(String.to_atom("bad_context_#{:erlang.unique_integer([:positive])}"), :alt) 534 | quote location: :keep do 535 | unquote(this_ast) |> case do 536 | %{error: nil} = good_context -> good_context 537 | %{error: %ExSpirit.Parser.ExpectationFailureException{}} = bad_context -> bad_context 538 | unquote(bad_context) -> 539 | unquote(original_context_ast) 540 | |> unquote(alt_expand(original_context_ast, [bad_context | err_contexts], next_ast, rest_ast)) 541 | end 542 | end 543 | end 544 | defp alt_expand(original_context_ast, err_contexts, this_ast, []) do 545 | bad_context = Macro.var(String.to_atom("bad_context_#{:erlang.unique_integer([:positive])}"), :alt) 546 | err_contexts = :lists.reverse(err_contexts, [bad_context]) 547 | quote location: :keep do 548 | unquote(this_ast) |> case do 549 | %{error: nil} = good_context -> good_context 550 | %{error: %ExSpirit.Parser.ExpectationFailureException{}} = bad_context -> bad_context 551 | unquote(bad_context) -> 552 | %{unquote(original_context_ast)| 553 | result: nil, 554 | error: %ExSpirit.Parser.ParseException{ 555 | message: "Alt failed all branches:\n\t\t#{Enum.join(unquote( 556 | Enum.map(err_contexts, "e(do: unquote(&1).error.message)) 557 | ), "\n\t\t")}", 558 | context: unquote(original_context_ast), 559 | extradata: unquote(err_contexts) 560 | }, 561 | } 562 | end 563 | end 564 | end 565 | 566 | @doc """ 567 | Wraps the result of the passed in parser in a standard erlang 2-tuple, 568 | where the first element the tag that you pass in 569 | and the second is the result of the parser. 570 | 571 | ## Examples 572 | iex> import ExSpirit.Parser 573 | iex> import ExSpirit.Tests.Parser 574 | iex> context = parse("ff", tag(:integer, uint(16))) 575 | iex> {context.error, context.result, context.rest} 576 | {nil, {:integer, 255}, ""} 577 | """ 578 | defmacro tag(context_ast, tag_ast, parser_ast) do 579 | quote location: :keep do 580 | case unquote(context_ast) |> unquote(parser_ast) do 581 | %{error: nil} = good_context -> %{good_context | result: {unquote(tag_ast), good_context.result}} 582 | bad_context -> bad_context 583 | end 584 | end 585 | end 586 | 587 | @doc """ 588 | The `no_skip` combination parser takes a parser and clears the skipper so they 589 | do no skipping. 590 | 591 | Good to parse non-skippable content within a large parser. 592 | 593 | ## Examples 594 | iex> import ExSpirit.Parser 595 | iex> import ExSpirit.Tests.Parser 596 | iex> context = parse(" Test:42 ", lit("Test:") |> no_skip(uint()), skipper: lit(?\\s)) 597 | iex> {context.error, context.result, context.rest} 598 | {nil, 42, " "} 599 | """ 600 | defmacro no_skip(context_ast, parser_ast) do 601 | quote location: :keep do 602 | context_no_skip = unquote(context_ast) 603 | noskip_context = %{context_no_skip | skipper: nil} 604 | return_context = noskip_context |> unquote(parser_ast) 605 | %{return_context | skipper: context_no_skip.skipper} 606 | end 607 | end 608 | 609 | @doc """ 610 | The `skipper` combination parser takes a parser and changes the skipper within 611 | it to the one you pass in for the duration of the parser that you pass in. 612 | 613 | ### Examples 614 | # You can change a skipper for a parser as well with `skipper` 615 | iex> import ExSpirit.Parser 616 | iex> import ExSpirit.Tests.Parser 617 | iex> context = parse(" Test:\t42 ", lit("Test:") |> skipper(uint(), lit(?\\t)), skipper: lit(?\\s)) 618 | iex> {context.error, context.result, context.rest} 619 | {nil, 42, " "} 620 | """ 621 | defmacro skipper(context_ast, parser_ast, skipper_ast) do 622 | quote location: :keep do 623 | context_skipper = unquote(context_ast) 624 | skipper = fn context -> context |> unquote(skipper_ast) end 625 | newskip_context = %{context_skipper | skipper: skipper} 626 | return_context = newskip_context |> unquote(parser_ast) 627 | %{return_context | skipper: context_skipper.skipper} 628 | end 629 | end 630 | 631 | @doc """ 632 | Takes and runs a parser but ignores the result of the parser, instead returning `nil`. 633 | 634 | Can be given the option of `pass_result: true` to pass the previous result on. 635 | 636 | ## Examples 637 | # `ignore` will run the parser but return no result 638 | iex> import ExSpirit.Parser 639 | iex> import ExSpirit.Tests.Parser 640 | iex> context = parse("Test", ignore(char([?a..?z, ?T]))) 641 | iex> {context.error, context.result, context.rest} 642 | {nil, nil, "est"} 643 | 644 | # `ignore` will pass on the previous result if you want it to 645 | iex> import ExSpirit.Parser 646 | iex> import ExSpirit.Tests.Parser 647 | iex> context = parse("42Test", uint() |> ignore(char([?a..?z, ?T]), pass_result: true)) 648 | iex> {context.error, context.result, context.rest} 649 | {nil, 42, "est"} 650 | """ 651 | defmacro ignore(context_ast, parser_ast, opts \\ []) do 652 | quote location: :keep do 653 | case unquote(context_ast) do 654 | %{error: nil} = context -> 655 | return_context = context |> unquote(parser_ast) 656 | %{return_context | result: unquote(if(opts[:pass_result], do: quote(do: unquote(context_ast).result), else: quote(do: nil)))} 657 | bad_context -> bad_context 658 | end 659 | end 660 | end 661 | 662 | @doc """ 663 | The `branch` combination parser is designed for efficient branching based on 664 | the result from another parser. 665 | 666 | It allows you to parse something, and using the result of that parser 667 | you can then either lookup the value in a map or call into a user function, 668 | either of which can return a parser function that will then be used to continue parsing. 669 | 670 | It takes two arguments, the first of which is the initial parser, the second 671 | is either a user function of `value -> parserFn` or a map of 672 | `values => parserFn` where the value key is looked up from the result of the 673 | first parser. If the parserFn is `nil` then `branch` fails, else the parserFn 674 | is executed to continue parsing. Because of the anonymous function calls this 675 | has a slight overhead so only use this if switching parsers dynamically based 676 | on a parsed value that is more complex then a simple `alt` parser or the count 677 | is more than a few branches in size. 678 | 679 | This returns only the output from the parser in the map, not the lookup 680 | parser. 681 | 682 | ### Examples 683 | iex> import ExSpirit.Parser 684 | iex> import ExSpirit.Tests.Parser 685 | iex> symbol_map = %{?b => &uint(&1, 2), ?d => &uint(&1, 10), ?x => &uint(&1, 16)} 686 | iex> context = parse("b101010", branch(char(), symbol_map)) 687 | iex> {context.error, context.result, context.rest} 688 | {nil, 42, ""} 689 | iex> context = parse("d213478", branch(char(), symbol_map)) 690 | iex> {context.error, context.result, context.rest} 691 | {nil, 213478, ""} 692 | iex> context = parse("xe1DCf", branch(char(), symbol_map)) 693 | iex> {context.error, context.result, context.rest} 694 | {nil, 925135, ""} 695 | iex> context = parse("a", branch(char(), symbol_map)) 696 | iex> {context.error.message, context.result, context.rest} 697 | {"Tried to branch to `97` but it was not found in the symbol_map", nil, ""} 698 | 699 | iex> import ExSpirit.Parser 700 | iex> import ExSpirit.Tests.Parser 701 | iex> symbol_mapper = fn 702 | iex> ?b -> &uint(&1, 2) 703 | iex> ?d -> &uint(&1, 10) 704 | iex> ?x -> &uint(&1, 16) 705 | iex> _value -> nil # Always have a default case. :-) 706 | iex> end 707 | iex> context = parse("b101010", branch(char(), symbol_mapper)) 708 | iex> {context.error, context.result, context.rest} 709 | {nil, 42, ""} 710 | iex> context = parse("d213478", branch(char(), symbol_mapper)) 711 | iex> {context.error, context.result, context.rest} 712 | {nil, 213478, ""} 713 | iex> context = parse("xe1DCf", branch(char(), symbol_mapper)) 714 | iex> {context.error, context.result, context.rest} 715 | {nil, 925135, ""} 716 | iex> context = parse("a", branch(char(), symbol_mapper)) 717 | iex> {context.error.message, context.result, context.rest} 718 | {"Tried to branch to `97` but it was not found in the symbol_map", nil, ""} 719 | """ 720 | defmacro branch(context_ast, parser_ast, symbol_map_ast) do 721 | quote location: :keep do 722 | context = unquote(context_ast) 723 | symbol_map = unquote(symbol_map_ast) 724 | case context |> unquote(parser_ast) do 725 | %{error: nil, result: lookup} = lookup_context -> 726 | if is_function(symbol_map, 1) do 727 | symbol_map.(lookup) 728 | else 729 | symbol_map[lookup] 730 | end 731 | |> case do 732 | nil -> 733 | %{lookup_context | 734 | result: nil, 735 | error: %ExSpirit.Parser.ParseException{message: "Tried to branch to `#{inspect lookup}` but it was not found in the symbol_map", context: context, extradata: symbol_map}, 736 | } 737 | found_parser_fun -> 738 | lookup_context |> found_parser_fun.() 739 | end 740 | bad_context -> bad_context 741 | end 742 | end 743 | end 744 | 745 | @doc """ 746 | Takes a parser but if it fails then it returns a hard error 747 | that will prevent further parsers, even in branch tests, from running. 748 | 749 | The purpose of this parser is to hard mention parsing errors at the correct 750 | parsing site, so that if you are parsing an `alt` of parsers, but you parse 751 | out a 'let' for example, followed by an identifier, if the identifier fails 752 | then you do not want to let the alt try the next one but instead fail out hard 753 | with an error message related to the proper place the parse failed instead of 754 | trying other parsers that you know will not succeed anyway. 755 | 756 | ### Examples 757 | iex> import ExSpirit.Parser 758 | iex> import ExSpirit.Tests.Parser 759 | iex> context = parse("do 10", lit("do ") |> expect(uint())) 760 | iex> {context.error, context.result, context.rest} 761 | {nil, 10, ""} 762 | 763 | iex> import ExSpirit.Parser 764 | iex> import ExSpirit.Tests.Parser 765 | iex> context = parse("do nope", lit("do ") |> expect(uint())) 766 | iex> %ExSpirit.Parser.ExpectationFailureException{} = context.error 767 | iex> {context.error.message, context.result, context.rest} 768 | {"Parsing uint with radix of 10 had 0 digits but 1 minimum digits were required", nil, "nope"} 769 | 770 | iex> import ExSpirit.Parser 771 | iex> import ExSpirit.Tests.Parser 772 | iex> context = parse("do nope", alt([ lit("do ") |> expect(uint()), lit("blah") ])) 773 | iex> %ExSpirit.Parser.ExpectationFailureException{} = context.error 774 | iex> {context.error.message, context.result, context.rest} 775 | {"Parsing uint with radix of 10 had 0 digits but 1 minimum digits were required", nil, "nope"} 776 | 777 | # Difference without the `expect` 778 | iex> import ExSpirit.Parser 779 | iex> import ExSpirit.Tests.Parser 780 | iex> context = parse("do nope", alt([ lit("do ") |> uint(), lit("blah") ])) 781 | iex> %ExSpirit.Parser.ParseException{} = context.error 782 | iex> {context.error.message =~ "Alt failed all branches:", context.result, context.rest} 783 | {true, nil, "do nope"} 784 | """ 785 | defmacro expect(context_ast, parser_ast) do 786 | quote location: :keep do 787 | context = unquote(context_ast) 788 | if !valid_context?(context) do 789 | context 790 | else 791 | case context |> unquote(parser_ast) do 792 | %{error: nil} = good_context -> good_context 793 | bad_context -> ExSpirit.Parser.ExpectationFailureException.makeContextFailed(bad_context) 794 | end 795 | end 796 | end 797 | end 798 | 799 | @doc """ 800 | Repeats over a parser for bounded number of times, returning the results as a list. 801 | 802 | It does have a slight overhead compared to known execution times 803 | due to an anonmous function call, but that is necessary when 804 | performing a dynamic number of repetitions without mutable variables. 805 | 806 | The optional arguments are the minimum number of repeats required, default of 807 | `0`, and the maximum number of repeats, default of `-1` (infinite). 808 | 809 | ## Examples 810 | iex> import ExSpirit.Parser, only: :macros 811 | iex> import ExSpirit.Tests.Parser 812 | iex> context = parse("TTTX", repeat(char(?T))) 813 | iex> {context.error, context.result, context.rest} 814 | {nil, [?T, ?T, ?T], "X"} 815 | 816 | iex> import ExSpirit.Parser, only: :macros 817 | iex> import ExSpirit.Tests.Parser 818 | iex> context = parse("TTTX", repeat(char(?T), 1)) 819 | iex> {context.error, context.result, context.rest} 820 | {nil, [?T, ?T, ?T], "X"} 821 | 822 | iex> import ExSpirit.Parser, only: :macros 823 | iex> import ExSpirit.Tests.Parser 824 | iex> context = parse("TTTX", repeat(char(?T), 1, 10)) 825 | iex> {context.error, context.result, context.rest} 826 | {nil, [?T, ?T, ?T], "X"} 827 | 828 | iex> import ExSpirit.Parser, only: :macros 829 | iex> import ExSpirit.Tests.Parser 830 | iex> context = parse("TTTX", repeat(char(?T), 1, 2)) 831 | iex> {context.error, context.result, context.rest} 832 | {nil, [?T, ?T], "TX"} 833 | 834 | iex> import ExSpirit.Parser, only: :macros 835 | iex> import ExSpirit.Tests.Parser 836 | iex> context = parse("TTTX", repeat(char(?T), 4)) 837 | iex> {context.error.message, context.result, context.rest} 838 | {"Repeating over a parser failed due to not reaching the minimum amount of 4 with only a repeat count of 3", nil, "X"} 839 | 840 | iex> import ExSpirit.Parser, only: :macros 841 | iex> import ExSpirit.Tests.Parser 842 | iex> context = parse("TTT", repeat(char(?T), 4)) 843 | iex> {context.error.message, context.result, context.rest} 844 | {"Repeating over a parser failed due to not reaching the minimum amount of 4 with only a repeat count of 3", nil, ""} 845 | 846 | iex> import ExSpirit.Parser, only: :macros 847 | iex> import ExSpirit.Tests.Parser 848 | iex> context = parse("", repeat(char(?T))) 849 | iex> {context.error, context.result, context.rest} 850 | {nil, [], ""} 851 | """ 852 | defmacro repeat(context_ast, parser_ast, minimum \\ 0, maximum \\ -1) do 853 | quote location: :keep do 854 | unquote(context_ast) |> repeatFn(fn(c) -> c |> unquote(parser_ast) end, unquote(minimum), unquote(maximum)) 855 | end 856 | end 857 | 858 | @doc """ 859 | The repeat function parser allows you to pass in a parser function to repeat 860 | over, but is otherwise identical to `repeat`, especially as `repeat` delegates 861 | to `repeatFn`. 862 | 863 | See `ExSpirit.Parser.repeat/4` for more. 864 | 865 | ## Examples 866 | iex> import ExSpirit.Parser, only: :macros 867 | iex> import ExSpirit.Tests.Parser 868 | iex> context = parse("TTTX", repeatFn(fn c -> c |> char(?T) end)) 869 | iex> {context.error, context.result, context.rest} 870 | {nil, [?T, ?T, ?T], "X"} 871 | """ 872 | # TODO: Refactor 873 | # Until refactored, keep in sync with the implementation in `__using__` 874 | def repeatFn(context, parser, minimum \\ 0, maximum \\ -1) when is_function(parser, 1) do 875 | repeatFn(context, parser, minimum, maximum, [], 0) 876 | end 877 | defp repeatFn(context, _parser, _minimum, maximum, results, maximum) do 878 | %{context | 879 | result: :lists.reverse(results), 880 | } 881 | end 882 | defp repeatFn(context, parser, minimum, maximum, results, count) do 883 | case context |> parser.() do 884 | %{error: nil, result: result} = good_context -> repeatFn(good_context, parser, minimum, maximum, [result | results], count + 1) 885 | %{error: %ExSpirit.Parser.ExpectationFailureException{}} = bad_context -> bad_context 886 | bad_context -> 887 | if minimum <= count do 888 | %{context | 889 | result: :lists.reverse(results), 890 | } 891 | else 892 | %{bad_context | 893 | result: nil, 894 | error: %ExSpirit.Parser.ParseException{message: "Repeating over a parser failed due to not reaching the minimum amount of #{minimum} with only a repeat count of #{count}", context: context, extradata: count}, 895 | } 896 | end 897 | end 898 | end 899 | 900 | @doc """ 901 | The success parser always returns the passed in value, default of nil, 902 | successfully like a parsed value. 903 | 904 | ## Examples 905 | iex> import ExSpirit.Parser 906 | iex> context = parse("", success(42)) 907 | iex> {context.error, context.result, context.rest} 908 | {nil, 42, ""} 909 | """ 910 | def success(context, value \\ nil) do 911 | if !valid_context?(context) do 912 | context 913 | else 914 | %{context | 915 | result: value 916 | } 917 | end 918 | end 919 | 920 | @doc """ 921 | The fail parser always fails, documenting the user information passed in 922 | 923 | ## Examples 924 | 925 | iex> import ExSpirit.Parser 926 | iex> context = parse("", fail(42)) 927 | iex> {context.error.extradata, context.result, context.rest} 928 | {42, nil, ""} 929 | """ 930 | def fail(context, reason \\ nil) do 931 | if !valid_context?(context) do 932 | context 933 | else 934 | %{context | 935 | result: nil, 936 | error: %ExSpirit.Parser.ParseException{message: "Fail parser called with user reason of: #{inspect reason}", context: context, extradata: reason}, 937 | } 938 | end 939 | end 940 | 941 | @doc """ 942 | Runs a function with the context. 943 | 944 | TODO: Expand this *a lot*. 945 | 946 | ### Examples 947 | iex> import ExSpirit.Parser 948 | iex> fun = fn c -> %{c|result: 42} end 949 | iex> context = parse("a", pipe_context_into(fun.())) 950 | iex> {context.error, context.result, context.rest} 951 | {nil, 42, "a"} 952 | """ 953 | defmacro pipe_context_into(context_ast, mapper_ast) do 954 | quote location: :keep do 955 | context = unquote(context_ast) 956 | if !valid_context?(context) do 957 | context 958 | else 959 | context |> unquote(mapper_ast) 960 | end 961 | end 962 | end 963 | 964 | @doc """ 965 | Runs a function with the result 966 | 967 | ### Examples 968 | iex> import ExSpirit.Parser 969 | iex> fun = fn nil -> 42 end 970 | iex> context = parse("a", pipe_result_into(fun.())) 971 | iex> {context.error, context.result, context.rest} 972 | {nil, 42, "a"} 973 | """ 974 | defmacro pipe_result_into(context_ast, mapper_ast) do 975 | quote location: :keep do 976 | context = unquote(context_ast) 977 | if !valid_context?(context) do 978 | context 979 | else 980 | result = context.result |> unquote(mapper_ast) 981 | if Exception.exception?(result) do 982 | %{context | 983 | error: result, 984 | result: nil, 985 | } 986 | else 987 | %{context | 988 | result: result, 989 | } 990 | end 991 | end 992 | end 993 | end 994 | 995 | @doc """ 996 | Runs a function and parser with the both the context before and after the 997 | function call. 998 | 999 | ### Examples 1000 | iex> import ExSpirit.Parser 1001 | iex> import ExSpirit.Tests.Parser 1002 | iex> fun = fn {pre, post} -> %{post|result: {pre, post}} end 1003 | iex> context = parse("42", pipe_context_around(fun.(), uint())) 1004 | iex> {pre, post} = context.result 1005 | iex> {context.error, pre.column, post.column, context.rest} 1006 | {nil, 1, 3, ""} 1007 | """ 1008 | defmacro pipe_context_around(context_ast, mapper_ast, parser_ast) do 1009 | quote location: :keep do 1010 | context_map_context_around = unquote(context_ast) 1011 | if !valid_context?(context_map_context_around) do 1012 | context_map_context_around 1013 | else 1014 | case context_map_context_around |> unquote(parser_ast) do 1015 | %{error: nil} = new_context -> 1016 | {context_map_context_around, new_context} |> unquote(mapper_ast) 1017 | bad_context -> bad_context 1018 | end 1019 | end 1020 | end 1021 | end 1022 | 1023 | @doc """ 1024 | Puts something into the state at the specified key 1025 | 1026 | ## Examples 1027 | iex> import ExSpirit.Parser 1028 | iex> import ExSpirit.Tests.Parser 1029 | iex> context = parse("42", uint() |> put_state(:test, :result)) 1030 | iex> {context.error, context.result, context.rest, context.state} 1031 | {nil, 42, "", %{test: 42}} 1032 | """ 1033 | defmacro put_state(context_ast, key, from) 1034 | defmacro put_state(context_ast, key, :context) do 1035 | quote location: :keep do 1036 | context = unquote(context_ast) 1037 | if !valid_context?(context) do 1038 | context 1039 | else 1040 | %{context | 1041 | state: Map.put(context.state, unquote(key), context), 1042 | } 1043 | end 1044 | end 1045 | end 1046 | defmacro put_state(context_ast, key, :result) do 1047 | quote location: :keep do 1048 | context = unquote(context_ast) 1049 | if !valid_context?(context) do 1050 | context 1051 | else 1052 | %{context | 1053 | state: Map.put(context.state, unquote(key), context.result), 1054 | } 1055 | end 1056 | end 1057 | end 1058 | 1059 | @doc """ 1060 | Puts something into the state at the specified key 1061 | 1062 | ## Examples 1063 | iex> import ExSpirit.Parser 1064 | iex> import ExSpirit.Tests.Parser 1065 | iex> context = parse("42", uint() |> push_state(:test, :result)) 1066 | iex> {context.error, context.result, context.rest, context.state} 1067 | {nil, 42, "", %{test: [42]}} 1068 | """ 1069 | defmacro push_state(context_ast, key, from) 1070 | defmacro push_state(context_ast, key, :context) do 1071 | quote location: :keep do 1072 | context = unquote(context_ast) 1073 | if !valid_context?(context) do 1074 | context 1075 | else 1076 | %{context | 1077 | state: Map.update(context.state, unquote(key), [context], fn x -> [context | List.wrap(x)] end), 1078 | } 1079 | end 1080 | end 1081 | end 1082 | defmacro push_state(context_ast, key, :result) do 1083 | quote location: :keep do 1084 | context = unquote(context_ast) 1085 | if !valid_context?(context) do 1086 | context 1087 | else 1088 | %{context | 1089 | state: Map.update(context.state, unquote(key), [context.result], fn x -> [context.result | List.wrap(x)] end), 1090 | } 1091 | end 1092 | end 1093 | end 1094 | 1095 | @doc """ 1096 | Get something(s) from the state and put it into the locations in the parser 1097 | that are marked with &1-* bindings 1098 | 1099 | ### Examples 1100 | iex> import ExSpirit.Parser 1101 | iex> import ExSpirit.Tests.Parser 1102 | iex> context = parse("A:A", char() |> put_state(:test, :result) |> lit(?:) |> get_state_into([:test], char(&1))) 1103 | iex> {context.error, context.result, context.rest} 1104 | {nil, ?A, ""} 1105 | 1106 | iex> import ExSpirit.Parser 1107 | iex> import ExSpirit.Tests.Parser 1108 | iex> context = parse("A:B", char() |> put_state(:test, :result) |> lit(?:) |> get_state_into([:test], char(&1))) 1109 | iex> {String.starts_with?(context.error.message, "Tried parsing out any of the the characters of"), context.result, context.rest} 1110 | {true, nil, "B"} 1111 | 1112 | iex> import ExSpirit.Parser 1113 | iex> import ExSpirit.Tests.Parser 1114 | iex> context = parse("A:B", char() |> put_state(:test, :result) |> lit(?:) |> get_state_into(:test, :result)) 1115 | iex> {context.error, context.result, context.rest} 1116 | {nil, ?A, "B"} 1117 | """ 1118 | defmacro get_state_into(context_ast, key, :result) do 1119 | quote location: :keep do 1120 | context = unquote(context_ast) 1121 | if !valid_context?(context) do 1122 | context 1123 | else 1124 | %{context | 1125 | result: context.state[unquote(key)] 1126 | } 1127 | end 1128 | end 1129 | end 1130 | 1131 | defmacro get_state_into(context_ast, key, :context) do 1132 | quote location: :keep do 1133 | context = unquote(context_ast) 1134 | if !valid_context?(context) do 1135 | context 1136 | else 1137 | case context.state[unquote(key)] do 1138 | %ExSpirit.Parser.Context{} = old_context -> old_context 1139 | _ -> 1140 | %{context | 1141 | result: nil, 1142 | error: %ExSpirit.Parser.ParseException{message: "Attempted to get a context out of the state at `#{inspect unquote(key)}` but there was no context there", context: context, extradata: key}, 1143 | } 1144 | end 1145 | end 1146 | end 1147 | end 1148 | 1149 | defmacro get_state_into(context_ast, keys, parser_ast) do 1150 | keys = if !is_list(keys), do: [keys], else: keys 1151 | context_binding = quote do context end 1152 | parser_ast = Macro.postwalk(parser_ast, fn 1153 | {:&, _, [0]} -> quote do unquote(context_binding).state end 1154 | {:&, _, [pos]} = orig_ast -> 1155 | case Enum.at(keys, pos-1) do 1156 | nil -> orig_ast 1157 | {key, default} -> quote do Map.get(unquote(context_binding).state, unquote(key), unquote(default)) end 1158 | key -> quote do unquote(context_binding).state[unquote(key)] end 1159 | end 1160 | ast -> ast 1161 | end) 1162 | quote location: :keep do 1163 | unquote(context_binding) = unquote(context_ast) 1164 | if !valid_context?(unquote(context_binding)) do 1165 | unquote(context_binding) 1166 | else 1167 | unquote(context_binding) |> unquote(parser_ast) 1168 | end 1169 | end 1170 | end 1171 | 1172 | @doc """ 1173 | Looks ahead to confirm success, but does not update the context when 1174 | successful. 1175 | 1176 | ## Examples 1177 | iex> import ExSpirit.Parser 1178 | iex> import ExSpirit.Tests.Parser 1179 | iex> context = parse("AA", lit(?A) |> lookahead(lit(?A)) |> char()) 1180 | iex> {context.error, context.result, context.rest} 1181 | {nil, ?A, ""} 1182 | 1183 | iex> import ExSpirit.Parser 1184 | iex> import ExSpirit.Tests.Parser 1185 | iex> context = parse("AB", lit(?A) |> lookahead(lit(?A)) |> char()) 1186 | iex> {String.starts_with?(context.error.message, "Lookahead failed"), context.result, context.rest} 1187 | {true, nil, "B"} 1188 | """ 1189 | defmacro lookahead(context_ast, parser_ast) do 1190 | quote location: :keep do 1191 | context = unquote(context_ast) 1192 | if !valid_context?(context) do 1193 | context 1194 | else 1195 | case context |> unquote(parser_ast) do 1196 | %{error: nil} -> context 1197 | bad_context -> 1198 | %{context | 1199 | result: nil, 1200 | error: %ExSpirit.Parser.ParseException{message: "Lookahead failed", context: context, extradata: bad_context}, 1201 | } 1202 | end 1203 | end 1204 | end 1205 | end 1206 | 1207 | @doc """ 1208 | Looks ahead to confirm failure, but does not update the context when 1209 | failed. 1210 | 1211 | ## Examples 1212 | iex> import ExSpirit.Parser 1213 | iex> import ExSpirit.Tests.Parser 1214 | iex> context = parse("AB", lit(?A) |> lookahead_not(lit(?A)) |> char()) 1215 | iex> {context.error, context.result, context.rest} 1216 | {nil, ?B, ""} 1217 | 1218 | iex> import ExSpirit.Parser 1219 | iex> import ExSpirit.Tests.Parser 1220 | iex> context = parse("AA", lit(?A) |> lookahead_not(lit(?A)) |> char()) 1221 | iex> {String.starts_with?(context.error.message, "Lookahead_not failed"), context.result, context.rest} 1222 | {true, nil, "A"} 1223 | """ 1224 | defmacro lookahead_not(context_ast, parser_ast) do 1225 | quote location: :keep do 1226 | context = unquote(context_ast) 1227 | if !valid_context?(context) do 1228 | context 1229 | else 1230 | case context |> unquote(parser_ast) do 1231 | %{error: nil} = bad_context -> 1232 | %{context | 1233 | result: nil, 1234 | error: %ExSpirit.Parser.ParseException{message: "Lookahead_not failed", context: context, extradata: bad_context}, 1235 | } 1236 | _context -> context 1237 | end 1238 | end 1239 | end 1240 | end 1241 | 1242 | @doc """ 1243 | Returns the entire parsed text from the parser, regardless of the actual return value. 1244 | 1245 | ## Examples 1246 | iex> import ExSpirit.Parser 1247 | iex> import ExSpirit.Tests.Parser 1248 | iex> context = parse("A256B", lexeme(char() |> uint())) 1249 | iex> {context.error, context.result, context.rest} 1250 | {nil, "A256", "B"} 1251 | """ 1252 | defmacro lexeme(context_ast, parser_ast) do 1253 | quote location: :keep do 1254 | context = unquote(context_ast) 1255 | if !valid_context?(context) do 1256 | context 1257 | else 1258 | case context |> unquote(parser_ast) do 1259 | %{error: nil} = good_context -> 1260 | bytes = good_context.position - context.position 1261 | case context.rest do 1262 | <> -> 1263 | %{good_context | 1264 | result: parsed, 1265 | rest: newRest, 1266 | } 1267 | _ -> 1268 | %{context | 1269 | result: nil, 1270 | error: %ExSpirit.Parser.ParseException{message: "Lexeme failed, should be impossible, length needed is #{bytes} but available is only #{byte_size(context.rest)}", context: context, extradata: good_context}, 1271 | } 1272 | end 1273 | bad_context -> bad_context 1274 | end 1275 | end 1276 | end 1277 | end 1278 | 1279 | @doc """ 1280 | Success if there at the "End Of Input", else fails. 1281 | 1282 | If the argument is statically `pass_result: true` 1283 | then it passes on the prior return value. 1284 | 1285 | If the argument is statically `result: whatever` with `whatever` being what 1286 | you want to return, then it will set the result to that value on success. 1287 | `pass_result` must be set to false to use `result: value` or it is skipped. 1288 | 1289 | ## Examples 1290 | iex> import ExSpirit.Parser 1291 | iex> import ExSpirit.Tests.Parser 1292 | iex> context = parse("42", uint() |> eoi()) 1293 | iex> {context.error, context.result, context.rest} 1294 | {nil, nil, ""} 1295 | 1296 | iex> import ExSpirit.Parser 1297 | iex> import ExSpirit.Tests.Parser 1298 | iex> context = parse("42", uint() |> eoi(pass_result: true)) 1299 | iex> {context.error, context.result, context.rest} 1300 | {nil, 42, ""} 1301 | 1302 | iex> import ExSpirit.Parser 1303 | iex> import ExSpirit.Tests.Parser 1304 | iex> context = parse("42", uint() |> eoi(result: :success)) 1305 | iex> {context.error, context.result, context.rest} 1306 | {nil, :success, ""} 1307 | 1308 | iex> import ExSpirit.Parser 1309 | iex> import ExSpirit.Tests.Parser 1310 | iex> context = parse("42a", uint() |> eoi()) 1311 | iex> {is_map(context.error), context.result, context.rest} 1312 | {true, nil, "a"} 1313 | """ 1314 | defmacro eoi(context_ast, opts \\ []) do 1315 | quote location: :keep do 1316 | context = unquote(context_ast) 1317 | if !valid_context?(context) do 1318 | context 1319 | else 1320 | case context do 1321 | %{rest: ""} -> 1322 | unquote(if(opts[:pass_result], do: quote(do: context), else: quote(do: %{context | result: unquote(opts[:result])}))) 1323 | _ -> 1324 | %{context | 1325 | result: nil, 1326 | error: %ExSpirit.Parser.ParseException{message: "eoi failed, not at End Of Input", context: context}, 1327 | } 1328 | end 1329 | end 1330 | end 1331 | end 1332 | 1333 | @doc """ 1334 | When this module is `use`d then it will import what is required, define some inline functions for speed, and load in 1335 | other parsers. 1336 | 1337 | ## Paramters: 1338 | 1339 | * text: true|false -> Will use the Text Parsing module as well 1340 | """ 1341 | defmacro __using__(opts) do 1342 | text_use_ast = if(opts[:text], do: quote(do: use ExSpirit.Parser.Text), else: nil) 1343 | quote location: :keep do 1344 | import ExSpirit.Parser, only: :macros 1345 | 1346 | def repeatFn(context, parser, minimum \\ 0, maximum \\ -1) when is_function(parser, 1) do 1347 | repeatFn(context, parser, minimum, maximum, [], 0) 1348 | end 1349 | defp repeatFn(context, _parser, _minimum, maximum, results, maximum) do 1350 | %{context | 1351 | result: :lists.reverse(results), 1352 | } 1353 | end 1354 | defp repeatFn(context, parser, minimum, maximum, results, count) do 1355 | case context |> parser.() do 1356 | %{error: nil, result: result} = good_context -> repeatFn(good_context, parser, minimum, maximum, [result | results], count + 1) 1357 | %{error: %ExSpirit.Parser.ExpectationFailureException{}} = bad_context -> bad_context 1358 | bad_context -> 1359 | if minimum <= count do 1360 | %{context | 1361 | result: :lists.reverse(results), 1362 | } 1363 | else 1364 | %{bad_context | 1365 | result: nil, 1366 | error: %ExSpirit.Parser.ParseException{message: "Repeating over a parser failed due to not reaching the minimum amount of #{minimum} with only a repeat count of #{count}", context: context, extradata: count}, 1367 | } 1368 | end 1369 | end 1370 | end 1371 | 1372 | unquote(text_use_ast) 1373 | 1374 | end 1375 | end 1376 | 1377 | end 1378 | -------------------------------------------------------------------------------- /lib/ex_spirit/parser/text.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSpirit.Parser.Text do 2 | @moduledoc """ 3 | 4 | ExSpirit.Parser.Text is a set of parser specifically for parsing out utf-8 5 | text from a binary. 6 | 7 | 8 | # Parsers 9 | 10 | ## `lit` 11 | 12 | The literal parser matches out a specific string or character, entirely 13 | ignoring the result and returning `nil`. 14 | 15 | ### Examples 16 | 17 | ```elixir 18 | 19 | # `lit` matches a specific string or character 20 | iex> import ExSpirit.Parser 21 | iex> import ExSpirit.Tests.Parser 22 | iex> context = parse("Test 42", lit("Test")) 23 | iex> {context.error, context.result, context.rest} 24 | {nil, nil, " 42"} 25 | 26 | # `lit` matches a specific string or character 27 | iex> import ExSpirit.Parser 28 | iex> import ExSpirit.Tests.Parser 29 | iex> context = parse("Test 42", lit(?T)) 30 | iex> {context.error, context.result, context.rest} 31 | {nil, nil, "est 42"} 32 | 33 | ``` 34 | 35 | ## `uint` 36 | 37 | The unsigned integer parser parses a plain number from the input with a few 38 | options. 39 | 40 | - The first argument is the radix, defaults to 10, everything from 2 to 36 is 41 | supported. 42 | - The second argument is the minimum character count, defaults 1, valid at 1+. 43 | If the characters able to be parsed as a number is less than this value then 44 | the parser fails. 45 | - The third argument is the maximum character count, defaults -1 (unlimited), 46 | valid values are -1, or 1+. It stops parsing at this amount of characters 47 | and returns what it has parsed so far, if there are more number characters 48 | still to be parsed then they will be handled by the next parser. 49 | 50 | ### Examples 51 | 52 | ```elixir 53 | 54 | # `uint` parses out an unsigned integer, default radix of 10 with a min size of 1 and max of unlimited 55 | iex> import ExSpirit.Parser 56 | iex> import ExSpirit.Tests.Parser 57 | iex> context = parse("42", uint()) 58 | iex> {context.error, context.result, context.rest} 59 | {nil, 42, ""} 60 | 61 | # `uint` parsing out base-2 62 | iex> import ExSpirit.Parser 63 | iex> import ExSpirit.Tests.Parser 64 | iex> context = parse("101", uint(2)) 65 | iex> {context.error, context.result, context.rest} 66 | {nil, 5, ""} 67 | 68 | # `uint` parsing out base-16 lower-case, can be mixed too 69 | iex> import ExSpirit.Parser 70 | iex> import ExSpirit.Tests.Parser 71 | iex> context = parse("ff", uint(16)) 72 | iex> {context.error, context.result, context.rest} 73 | {nil, 255, ""} 74 | 75 | # `uint` parsing out base-16 upper-case, can be mixed too 76 | iex> import ExSpirit.Parser 77 | iex> import ExSpirit.Tests.Parser 78 | iex> context = parse("FFF", uint(16)) 79 | iex> {context.error, context.result, context.rest} 80 | {nil, 4095, ""} 81 | 82 | ``` 83 | 84 | 85 | 86 | 87 | ## Text (UTF-8 Binaries) parsing 88 | 89 | ```elixir 90 | 91 | # `char` can parse out any single character 92 | iex> import ExSpirit.Parser 93 | iex> import ExSpirit.Tests.Parser 94 | iex> context = parse("Test", char()) 95 | iex> {context.error, context.result, context.rest} 96 | {nil, ?T, "est"} 97 | 98 | # `char` can parse out any 'specific' single character as well 99 | iex> import ExSpirit.Parser 100 | iex> import ExSpirit.Tests.Parser 101 | iex> context = parse("Test", char(?T)) 102 | iex> {context.error, context.result, context.rest} 103 | {nil, ?T, "est"} 104 | 105 | # `char` can parse out anything 'but' a 'specific' single character too, 106 | # just negate it, don't mix positive and negative matchers in the same set 107 | # unless there is only one negative matcher and it is at the end of the list 108 | iex> import ExSpirit.Parser 109 | iex> import ExSpirit.Tests.Parser 110 | iex> context = parse("Nope", char(-?T)) 111 | iex> {context.error, context.result, context.rest} 112 | {nil, ?N, "ope"} 113 | 114 | # `char` can parse out any 'specific' single character from a range too 115 | iex> import ExSpirit.Parser 116 | iex> import ExSpirit.Tests.Parser 117 | iex> context = parse("Test", char(?A..?Z)) 118 | iex> {context.error, context.result, context.rest} 119 | {nil, ?T, "est"} 120 | 121 | # `char` can parse out any but a 'specific' single character from a range 122 | iex> import ExSpirit.Parser 123 | iex> import ExSpirit.Tests.Parser 124 | iex> context = parse("42", char(-?A..-?Z)) 125 | iex> {context.error, context.result, context.rest} 126 | {nil, ?4, "2"} 127 | 128 | # `char` can parse out any 'specific' single character from a list of 129 | # characters or ranges too 130 | iex> import ExSpirit.Parser 131 | iex> import ExSpirit.Tests.Parser 132 | iex> context = parse("Test", char([?a..?z, ?T])) 133 | iex> {context.error, context.result, context.rest} 134 | {nil, ?T, "est"} 135 | 136 | # `char` can parse out any but a 'specific' single character from a list of 137 | # characters or ranges too 138 | iex> import ExSpirit.Parser 139 | iex> import ExSpirit.Tests.Parser 140 | iex> context = parse("42", char([?a..?z, -?T])) 141 | iex> {context.error, context.result, context.rest} 142 | {nil, ?4, "2"} 143 | 144 | # a mixed list is fine if the negated ones are at the end of it, only 145 | iex> import ExSpirit.Parser 146 | iex> import ExSpirit.Tests.Parser 147 | iex> context = parse("Test", char([?a..?z, ?T, -?A..-?Z])) 148 | iex> {context.error, context.result, context.rest} 149 | {nil, ?T, "est"} 150 | 151 | # a mixed list is fine if the negated ones are at the end of it, only, 152 | # here is how a failure looks 153 | iex> import ExSpirit.Parser 154 | iex> import ExSpirit.Tests.Parser 155 | iex> context = parse("Rest", char([?a..?z, ?T, -?A..-?Z])) 156 | iex> {String.starts_with?(context.error.message, "Tried parsing out any of the the characters of"), context.result, context.rest} 157 | {true, nil, "Rest"} 158 | 159 | # `chars` parser is like char but it parses all matching as a binary 160 | iex> import ExSpirit.Parser 161 | iex> import ExSpirit.Tests.Parser 162 | iex> context = parse("TEST", chars(?A..?Z)) 163 | iex> {context.error, context.result, context.rest} 164 | {nil, "TEST", ""} 165 | 166 | # `chars` parser is like char but it parses all matching as a binary 167 | iex> import ExSpirit.Parser 168 | iex> import ExSpirit.Tests.Parser 169 | iex> context = parse("42", chars(?A..?Z, 0)) 170 | iex> {context.error, context.result, context.rest} 171 | {nil, "", "42"} 172 | 173 | # `chars` parser is like char but it parses all matching as a binary 174 | iex> import ExSpirit.Parser 175 | iex> import ExSpirit.Tests.Parser 176 | iex> context = parse("ABCDE", chars(?A..?Z, 1, 3)) 177 | iex> {context.error, context.result, context.rest} 178 | {nil, "ABC", "DE"} 179 | 180 | # `chars` parser is like char but it parses all matching as a binary 181 | iex> import ExSpirit.Parser 182 | iex> import ExSpirit.Tests.Parser 183 | iex> context = parse("42", chars(?A..?Z)) 184 | iex> {String.starts_with?(context.error.message, "Tried parsing out characters of"), context.result, context.rest} 185 | {true, nil, "42"} 186 | 187 | # `chars` parser is like char but it parses all matching as a binary 188 | iex> import ExSpirit.Parser 189 | iex> import ExSpirit.Tests.Parser 190 | iex> context = parse("TEST42", chars(?A..?Z)) 191 | iex> {context.error, context.result, context.rest} 192 | {nil, "TEST", "42"} 193 | 194 | # `chars1` parser is like chars but it parses all matching as a binary 195 | # also takes an initial single-char matcher 196 | iex> import ExSpirit.Parser 197 | iex> import ExSpirit.Tests.Parser 198 | iex> context = parse("_TEST42", chars1(?_, ?A..?Z)) 199 | iex> {context.error, context.result, context.rest} 200 | {nil, "_TEST", "42"} 201 | 202 | # `chars1` parser is like chars but it parses all matching as a binary 203 | # also takes an initial single-char matcher 204 | iex> import ExSpirit.Parser 205 | iex> import ExSpirit.Tests.Parser 206 | iex> context = parse("_TEST42", chars1([?a-?z, ?_], [?_, ?A..?Z])) 207 | iex> {context.error, context.result, context.rest} 208 | {nil, "_TEST", "42"} 209 | 210 | # `symbols` takes a ExSpirit.TreeMap, which is a structure designed for fast 211 | # lookups, though slow insertions, so please cache the data structure at 212 | # compile-time if possible. This `symbols` parser will take the text input 213 | # stream and match it on the TreeMap to find the longest-matching string, 214 | # then it will take the return value, if a function then it will run it as 215 | # as parserFn, else it will return it as a value 216 | iex> import ExSpirit.Parser 217 | iex> import ExSpirit.Tests.Parser 218 | iex> alias ExSpirit.TreeMap, as: TreeMap 219 | iex> symbol_TreeMap = TreeMap.new() |> TreeMap.add_text("int", &uint(&1)) |> TreeMap.add_text("char", &char(&1)) 220 | iex> context = parse("int42", symbols(symbol_TreeMap)) 221 | iex> {context.error, context.result, context.rest} 222 | {nil, 42, ""} 223 | iex> context = parse("charT", symbols(symbol_TreeMap)) 224 | iex> {context.error, context.result, context.rest} 225 | {nil, ?T, ""} 226 | iex> context = parse("in", symbols(symbol_TreeMap)) 227 | iex> {String.starts_with?(context.error.message, "Tried matching out symbols and got to `i` but failed"), context.result, context.rest} 228 | {true, nil, "in"} 229 | iex> context = parse("", symbols(symbol_TreeMap)) 230 | iex> {String.starts_with?(context.error.message, "Tried matching out symbols and got the end of the line but failed to find a value"), context.result, context.rest} 231 | {true, nil, ""} 232 | 233 | iex> import ExSpirit.Parser 234 | iex> import ExSpirit.Tests.Parser 235 | iex> alias ExSpirit.TreeMap, as: TreeMap 236 | iex> symbol_TreeMap = TreeMap.new() |> TreeMap.add_text("let", 1) |> TreeMap.add_text("letmap", 2) |> TreeMap.add_text("", 0) 237 | iex> context = parse("", symbols(symbol_TreeMap)) 238 | iex> {context.error, context.result, context.rest} 239 | {nil, 0, ""} 240 | iex> context = parse("A", symbols(symbol_TreeMap)) 241 | iex> {context.error, context.result, context.rest} 242 | {nil, 0, "A"} 243 | iex> context = parse("let", symbols(symbol_TreeMap)) 244 | iex> {context.error, context.result, context.rest} 245 | {nil, 1, ""} 246 | iex> context = parse("letmap", symbols(symbol_TreeMap)) 247 | iex> {context.error, context.result, context.rest} 248 | {nil, 2, ""} 249 | iex> context = parse("letma", symbols(symbol_TreeMap)) 250 | iex> {context.error, context.result, context.rest} 251 | {nil, 1, "ma"} 252 | 253 | 254 | ``` 255 | """ 256 | 257 | defmacro __using__(_) do 258 | quote location: :keep do 259 | 260 | 261 | # Binary literal 262 | def lit(context, literal) when is_binary(literal) do 263 | if !valid_context?(context) do 264 | context 265 | else 266 | context = skip(context) 267 | lit_size = byte_size(literal) 268 | case context.rest do 269 | <<^literal::binary-size(lit_size), rest::binary>> -> 270 | lit_chars = String.length(literal) 271 | <<_::binary-size(lit_size), rest::binary>> = context.rest 272 | %{context | 273 | rest: rest, 274 | position: context.position + lit_size, 275 | column: context.column + lit_chars, 276 | result: nil 277 | } 278 | _ -> 279 | %{context | 280 | error: %ExSpirit.Parser.ParseException{message: "literal `#{literal}` did not match the input", context: context} 281 | } 282 | end 283 | end 284 | end 285 | 286 | # Character literal 287 | def lit(context, literal) when is_integer(literal) do 288 | if !valid_context?(context) do 289 | context 290 | else 291 | context = skip(context) 292 | case context.rest do 293 | <<^literal::utf8, rest::binary>> -> 294 | lit_size = byte_size(<>) 295 | %{context | 296 | rest: rest, 297 | position: context.position + lit_size, 298 | column: context.column + 1, 299 | result: nil 300 | } 301 | _ -> 302 | %{context | 303 | error: %ExSpirit.Parser.ParseException{message: "literal `#{<>}` did not match the input", context: context} 304 | } 305 | end 306 | end 307 | end 308 | 309 | 310 | # Unisgned Integer parsing 311 | def uint(context, radix \\ 10, minDigits \\ 1, maxDigits \\ -1) do 312 | if !valid_context?(context) do 313 | context 314 | else 315 | context = skip(context) 316 | if radix <= 10 do 317 | uint_10(context, radix, minDigits, maxDigits, 0, 0) 318 | else 319 | uint_36(context, radix, minDigits, maxDigits, 0, 0) 320 | end 321 | end 322 | end 323 | 324 | defp uint_10(context, _radix, minDigits, maxDigits, num, maxDigits) do 325 | %{context | result: num, position: context.position + maxDigits, column: context.column + maxDigits} 326 | end 327 | defp uint_10(%{rest: <>} = context, radix, minDigits, maxDigits, num, digits) when c>=?0 and c<=?0+radix-1 do 328 | uint_10(%{context|rest: rest}, radix, minDigits, maxDigits, (num*radix)+(c-?0), digits+1) 329 | end 330 | defp uint_10(context, _radix, minDigits, _maxDigits, num, digits) when minDigits<=digits do 331 | %{context | result: num, position: context.position + digits, column: context.column + digits} 332 | end 333 | defp uint_10(context, radix, minDigits, _maxDigits, num, digits) when minDigits>digits do 334 | %{context | 335 | error: %ExSpirit.Parser.ParseException{message: "Parsing uint with radix of #{radix} had #{digits} digits but #{minDigits} minimum digits were required", context: context} 336 | } 337 | end 338 | 339 | defp uint_36(context, _radix, minDigits, maxDigits, num, maxDigits) do 340 | %{context | result: num, position: context.position + maxDigits, column: context.column + maxDigits} 341 | end 342 | defp uint_36(%{rest: <>} = context, radix, minDigits, maxDigits, num, digits) when (c>=?0 and c<=?0+radix-1) or (c>=?a and c<=?a+radix-11) or (c>=?A and c<=?A+radix-11) do 343 | num = if c > ?9 do 344 | c = if c >= ?a do c else c + (?a - ?A) end 345 | (num*radix)+(c-?a+10) 346 | else 347 | (num*radix)+(c-?0) 348 | end 349 | uint_36(%{context|rest: rest}, radix, minDigits, maxDigits, num, digits+1) 350 | end 351 | defp uint_36(context, _radix, minDigits, _maxDigits, num, digits) when minDigits<=digits do 352 | %{context | result: num, position: context.position + digits, column: context.column + digits} 353 | end 354 | defp uint_36(context, radix, minDigits, _maxDigits, num, digits) when minDigits>digits do 355 | %{context | 356 | error: %ExSpirit.Parser.ParseException{message: "Parsing uint with radix of #{radix} had #{digits} digits but #{minDigits} minimum digits were required", context: context} 357 | } 358 | end 359 | 360 | 361 | 362 | # Parse out any character 363 | def char(context) do 364 | if !valid_context?(context) do 365 | context 366 | else 367 | case skip(context) do 368 | %{rest: <>} = good_context -> 369 | %{good_context | 370 | result: c, 371 | rest: rest, 372 | position: good_context.position + byte_size(<>), 373 | column: if(c===?\n, do: 1, else: good_context.column+1), 374 | line: good_context.line + if(c===?\n, do: 1, else: 0), 375 | } 376 | bad_context -> 377 | %{bad_context | 378 | error: %ExSpirit.Parser.ParseException{message: "Tried parsing out a character but the end of input was reached", context: context}, 379 | } 380 | end 381 | end 382 | end 383 | 384 | # Parse out a single character 385 | def char(context, c) when is_integer(c), do: char(context, [c]) 386 | 387 | # Parse out a single character from a range of characters 388 | def char(context, _.._ = rangeMatcher), do: char(context, [rangeMatcher]) 389 | 390 | # Parse out a single character from a list of acceptable characters and/or ranges 391 | def char(context, characterMatchers) when is_list(characterMatchers) do 392 | if !valid_context?(context) do 393 | context 394 | else 395 | case skip(context) do 396 | %{rest: <>} = matched_context -> 397 | if char_charrangelist_matches(c, characterMatchers) do 398 | %{matched_context | 399 | result: c, 400 | rest: rest, 401 | position: matched_context.position + byte_size(<>), 402 | column: if(c===?\n, do: 1, else: matched_context.column+1), 403 | line: matched_context.line + if(c===?\n, do: 1, else: 0), 404 | } 405 | else 406 | %{matched_context | 407 | error: %ExSpirit.Parser.ParseException{message: "Tried parsing out any of the the characters of `#{inspect characterMatchers}` but failed due to the input character not matching", context: context}, 408 | } 409 | end 410 | bad_context -> 411 | %{bad_context | 412 | error: %ExSpirit.Parser.ParseException{message: "Tried parsing out any of the the characters of `#{inspect characterMatchers}` but failed due to end of input", context: context}, 413 | } 414 | end 415 | end 416 | end 417 | 418 | def char_charrangelist_matches(c, matchers, defaultValue \\ false) 419 | def char_charrangelist_matches(c, c, _defaultValue), do: true 420 | def char_charrangelist_matches(c, d, _defaultValue) when is_integer(d), do: d<0 and -c !== d 421 | def char_charrangelist_matches(c, first..last, _defaultValue) when first<=last and c>=first and c<=last, do: true 422 | def char_charrangelist_matches(c, first..last, _defaultValue) when first>=last and c<=first and c>=last, do: true 423 | def char_charrangelist_matches(c, first..last, _defaultValue) when first<=last and -c>=first and -c<=last, do: false 424 | def char_charrangelist_matches(c, first..last, _defaultValue) when first>=last and -c<=first and -c>=last, do: false 425 | def char_charrangelist_matches(c, [], defaultValue), do: defaultValue 426 | def char_charrangelist_matches(c, [c | rest], _defaultValue), do: true 427 | def char_charrangelist_matches(c, [first..last | rest], _defaultValue) when first<=last and c>=first and c<=last, do: true 428 | def char_charrangelist_matches(c, [first..last | rest], _defaultValue) when first>=last and c<=first and c>=last, do: true 429 | def char_charrangelist_matches(c, [first..last | rest], _defaultValue) when first<=last and -c>=first and -c<=last, do: false 430 | def char_charrangelist_matches(c, [first..last | rest], _defaultValue) when first>=last and -c<=first and -c>=last, do: false 431 | def char_charrangelist_matches(c, [first..last | rest], _defaultValue), do: char_charrangelist_matches(c, rest, first<0) 432 | def char_charrangelist_matches(c, [d | rest], defaultValue) when -c===d, do: false 433 | def char_charrangelist_matches(c, [d | rest], _defaultValue), do: char_charrangelist_matches(c, rest, d<0) 434 | def char_charrangelist_matches(c, _d, defaultValue), do: defaultValue 435 | 436 | 437 | def chars_increment_while_matching(_matchers, _maximumChars, "", position, column, line, chars), do: {position, column, line, chars} 438 | def chars_increment_while_matching(_matchers, maximumChars, _rest, position, column, line, maximumChars), do: {position, column, line, maximumChars} 439 | def chars_increment_while_matching(matchers, maximumChars, <>, position, column, line, chars) do 440 | if char_charrangelist_matches(c, matchers) do 441 | if c == ?\n do 442 | chars_increment_while_matching(matchers, maximumChars, rest, position+byte_size(<>), 1, line+1, chars+1) 443 | else 444 | chars_increment_while_matching(matchers, maximumChars, rest, position+byte_size(<>), column+1, line, chars+1) 445 | end 446 | else 447 | {position, column, line, chars} 448 | end 449 | end 450 | 451 | 452 | 453 | defmacro chars(context_ast, matchers_ast, minimumChars \\ 1, maximumChars \\ -1) do 454 | quote location: :keep do 455 | original_context = unquote(context_ast) 456 | matchers = unquote(matchers_ast) 457 | minimumChars = unquote(minimumChars) 458 | if !valid_context?(original_context) do 459 | original_context 460 | else 461 | skipped_context = skip(original_context) 462 | {position, column, line, chars} = chars_increment_while_matching(matchers, unquote(maximumChars), skipped_context.rest, 0, skipped_context.column, skipped_context.line, 0) 463 | if chars < minimumChars do 464 | %{skipped_context | 465 | error: %ExSpirit.Parser.ParseException{message: "Tried parsing out characters of `#{inspect matchers}` but failed due to not meeting the minimum characters required of #{minimumChars}", context: skipped_context}, 466 | } 467 | else 468 | <> = skipped_context.rest 469 | %{skipped_context | 470 | result: result, 471 | rest: result_rest, 472 | position: skipped_context.position + position, 473 | column: column, 474 | line: line, 475 | } 476 | end 477 | end 478 | end 479 | end 480 | 481 | defmacro chars1(context_ast, firstMatcher_ast, matchers_ast, minimumChars \\ 1, maximumChars \\ -1) do 482 | quote location: :keep do 483 | context = unquote(context_ast) 484 | firstMatcher = unquote(firstMatcher_ast) 485 | matchers = unquote(matchers_ast) 486 | minimumChars = unquote(minimumChars) 487 | if !valid_context?(context) do 488 | context 489 | else 490 | case skip(context) do 491 | %{rest: <>} = first_matched_context -> 492 | if char_charrangelist_matches(c, firstMatcher) do 493 | {position, column, line, chars} = chars_increment_while_matching(matchers, unquote(maximumChars)-1, rest, byte_size(<>), if(c===?\n, do: 1, else: first_matched_context.column+1), first_matched_context.line + if(c===?\n, do: 1, else: 0), 1) 494 | if chars < minimumChars do 495 | %{context | 496 | error: %ExSpirit.Parser.ParseException{message: "Tried parsing out characters of `#{inspect matchers}` but failed due to not meeting the minimum characters required of #{minimumChars}", context: context}, 497 | } 498 | else 499 | <> = first_matched_context.rest 500 | %{first_matched_context | 501 | result: result, 502 | rest: result_rest, 503 | position: first_matched_context.position + position, 504 | column: column, 505 | line: line, 506 | } 507 | end 508 | else 509 | %{first_matched_context | 510 | error: %ExSpirit.Parser.ParseException{message: "Tried parsing out any of the characters of `#{inspect firstMatcher}` but failed due to the input character not matching", context: context}, 511 | } 512 | end 513 | bad_context -> 514 | %{bad_context | 515 | error: %ExSpirit.Parser.ParseException{message: "Tried parsing out any of the characters of `#{inspect firstMatcher}` but failed due to end of input", context: context}, 516 | } 517 | end 518 | end 519 | end 520 | end 521 | 522 | 523 | def symbols(context, %ExSpirit.TreeMap{root: root}) do 524 | if !valid_context?(context) do 525 | context 526 | else 527 | context = skip(context) 528 | %{context | result: nil} 529 | |> symbols_(root) 530 | end 531 | end 532 | 533 | defp symbols_(%{rest: ""} = context, map) do 534 | case map[[]] do 535 | nil -> 536 | %{context | 537 | error: %ExSpirit.Parser.ParseException{message: "Tried matching out symbols and got the end of the line but failed to find a value in `#{inspect map}`", context: context}, 538 | } 539 | parser when is_function(parser, 1) -> 540 | context |> parser.() 541 | value -> 542 | %{context | 543 | result: value, 544 | } 545 | end 546 | end 547 | 548 | defp symbols_(%{rest: <>} = context, map) do 549 | case map[c] do 550 | nil -> 551 | case map[[]] do 552 | nil -> 553 | %{context | 554 | error: %ExSpirit.Parser.ParseException{message: "Tried matching out symbols and got to `#{<>}` but failed to find it in `#{inspect map}`", context: context}, 555 | } 556 | parser when is_function(parser, 1) -> 557 | parser.(context) 558 | value -> 559 | %{context | result: value} 560 | end 561 | submap -> 562 | %{context | 563 | rest: rest, 564 | position: context.position + byte_size(<>), 565 | column: if(c===?\n, do: 1, else: context.column+1), 566 | line: context.line + if(c===?\n, do: 1, else: 0), 567 | } |> symbols_(submap) 568 | |> case do 569 | %{error: nil} = good_context -> good_context 570 | bad_context -> # Does this level have a value then? 571 | case map[[]] do 572 | nil -> 573 | %{context | 574 | error: %ExSpirit.Parser.ParseException{message: "Tried matching out symbols and got to `#{<>}` but failed to find a longest match in `#{inspect map}`", context: context}, 575 | } 576 | parser when is_function(parser, 1) -> 577 | %{context | 578 | position: context.position + byte_size(<>), 579 | column: if(c===?\n, do: 1, else: context.column+1), 580 | line: context.line + if(c===?\n, do: 1, else: 0), 581 | } |> parser.() 582 | value -> 583 | %{context | 584 | position: context.position + byte_size(<>), 585 | column: if(c===?\n, do: 1, else: context.column+1), 586 | line: context.line + if(c===?\n, do: 1, else: 0), 587 | result: value, 588 | } 589 | end 590 | end 591 | end 592 | end 593 | 594 | 595 | end 596 | end 597 | 598 | end 599 | -------------------------------------------------------------------------------- /lib/ex_spirit/parserx.ex: -------------------------------------------------------------------------------- 1 | defmodule ExSpirit.Parserx do 2 | @moduledoc """ 3 | 4 | ExSpirit.Parsers is the parsing section of ExSpirit, designed to parse out some 5 | kind of stream of data (whether via a binary, a list, or perhaps an actual 6 | stream) into a data structure of your own design. 7 | 8 | This parser differs from ExSpirit.Parser in that it uses an experimental Expression Template support. 9 | """ 10 | 11 | 12 | defmacro __using__([]) do 13 | quote do 14 | import ExSpirit.Parserx 15 | # Module.register_attribute(__MODULE__, :spirit_rules, accumulate: true) 16 | # @before_compile ExSpirit.Parserx 17 | 18 | # In-Module Helpers 19 | 20 | defp char_charrangelist_matches(c, matchers, defaultValue \\ false) 21 | defp char_charrangelist_matches(c, c, _defaultValue), do: true 22 | defp char_charrangelist_matches(c, d, _defaultValue) when is_integer(d), do: d<0 and -c !== d 23 | defp char_charrangelist_matches(c, first..last, _defaultValue) when first<=last and c>=first and c<=last, do: true 24 | defp char_charrangelist_matches(c, first..last, _defaultValue) when first>=last and c<=first and c>=last, do: true 25 | defp char_charrangelist_matches(c, first..last, _defaultValue) when first<=last and -c>=first and -c<=last, do: false 26 | defp char_charrangelist_matches(c, first..last, _defaultValue) when first>=last and -c<=first and -c>=last, do: false 27 | defp char_charrangelist_matches(c, [], defaultValue), do: defaultValue 28 | defp char_charrangelist_matches(c, [c | rest], _defaultValue), do: true 29 | defp char_charrangelist_matches(c, [first..last | rest], _defaultValue) when first<=last and c>=first and c<=last, do: true 30 | defp char_charrangelist_matches(c, [first..last | rest], _defaultValue) when first>=last and c<=first and c>=last, do: true 31 | defp char_charrangelist_matches(c, [first..last | rest], _defaultValue) when first<=last and -c>=first and -c<=last, do: false 32 | defp char_charrangelist_matches(c, [first..last | rest], _defaultValue) when first>=last and -c<=first and -c>=last, do: false 33 | defp char_charrangelist_matches(c, [first..last | rest], _defaultValue), do: char_charrangelist_matches(c, rest, first<0) 34 | defp char_charrangelist_matches(c, [d | rest], defaultValue) when -c===d, do: false 35 | defp char_charrangelist_matches(c, [d | rest], _defaultValue), do: char_charrangelist_matches(c, rest, d<0) 36 | defp char_charrangelist_matches(c, _d, defaultValue), do: defaultValue 37 | 38 | defp chars_increment_while_matching(_matchers, _maximumChars, "", position, column, line, chars), do: {position, column, line, chars} 39 | defp chars_increment_while_matching(_matchers, maximumChars, _rest, position, column, line, maximumChars), do: {position, column, line, maximumChars} 40 | defp chars_increment_while_matching(matchers, maximumChars, <>, position, column, line, chars) do 41 | if char_charrangelist_matches(c, matchers) do 42 | if c == ?\n do 43 | chars_increment_while_matching(matchers, maximumChars, rest, position+byte_size(<>), 1, line+1, chars+1) 44 | else 45 | chars_increment_while_matching(matchers, maximumChars, rest, position+byte_size(<>), column+1, line, chars+1) 46 | end 47 | else 48 | {position, column, line, chars} 49 | end 50 | end 51 | 52 | defp unquote(:'$repeat$')(_sep, _fun, _minimum, maximum, context, count) when count >= maximum, do: %{context | result: []} 53 | defp unquote(:'$repeat$')(sep, fun, minimum, maximum, context, count) do 54 | context = %{context | result: []} 55 | case fun.(context) do 56 | %{error: nil, result: result} = new_context -> 57 | case unquote(:'$repeat$')(nil, sep || fun, minimum, maximum, new_context, count+1) do 58 | %{error: nil, result: results} = last_context when is_list(results) -> %{last_context | result: [result | results]} 59 | %{error: nil, result: results} = last_context -> throw :whaaa 60 | error_context -> error_context 61 | end 62 | _ when minimum > count -> 63 | %{context | 64 | result: nil, 65 | error: %ExSpirit.Parser.ParseException{message: "Repeating over a parser failed due to not reaching the minimum amount of #{minimum} with only a repeat count of #{count}", context: context, extradata: count}, 66 | } 67 | _ -> context 68 | end 69 | end 70 | end 71 | end 72 | 73 | # defmacro __before_compile__(_env) do 74 | # quote do 75 | # def __spirit_rules__(), do: @spirit_rules 76 | # end 77 | # end 78 | 79 | 80 | ## Primary interface: `parse` 81 | 82 | defmacro parse(rest, parser, opts \\ []) do 83 | filename = opts[:filename] || quote do "" end 84 | skipper = case opts[:skipper] do 85 | nil -> nil 86 | skipper -> 87 | env = defrule_env_new(__CALLER__.module) 88 | {_env, skipper} = parser_to_ast(env, skipper) 89 | quote do fn context -> context |> unquote(skipper) end end 90 | # fun -> quote do fn context -> context |> unquote(fun) end end 91 | end 92 | 93 | env = defrule_env_new(__CALLER__.module) 94 | env = %{env | skipable: skipper !== nil} 95 | 96 | # {_env, body} = defrule_gen_body(env, parser) 97 | {_env, parser} = parser_to_ast(env, parser) 98 | 99 | # parser |> Macro.to_string |> IO.puts() 100 | 101 | quote location: :keep do 102 | case unquote(rest) do 103 | %ExSpirit.Parser.Context{}=context -> context |> unquote(parser) 104 | input -> 105 | %ExSpirit.Parser.Context{ 106 | filename: unquote(filename), 107 | skipper: unquote(skipper), 108 | rest: unquote(rest), 109 | } |> unquote(parser) 110 | end 111 | end 112 | end 113 | 114 | 115 | ## Primary interface: `defrule` 116 | 117 | # defmacro defrule(head, bodies) do 118 | # head = defrule_fixup_head(head) 119 | # name = defrule_head_name(head) 120 | # 121 | # do_body = bodies[:do] || throw "`defrule` must have a `do` body specified" 122 | # after_body = bodies[:after] || nil 123 | # else_body = bodies[:else] || nil # quote do context -> raise %ParseException{message: unquote("Rule `#{name}` "), context: context} end 124 | # 125 | # # quote do 126 | # # @spirit_rules {unquote(name), unquote(Macro.escape do_body), unquote(Macro.escape after_body), unquote(Macro.escape else_body)} 127 | # # end 128 | # quote do 129 | # def unquote(head) do 130 | # {name, unquote(do_body), unquote(after_body), unquote(else_body)} 131 | # end 132 | # end 133 | # end 134 | 135 | 136 | ## Primary interface: `defgrammar` 137 | 138 | defmacro defgrammar(head, bodies) do 139 | info = acquire_rule_information(head, bodies) 140 | context = Macro.var(:context, __MODULE__) 141 | quote do 142 | def unquote(info.name)(unquote(context), unquote_splicing(info.args)) do 143 | ExSpirit.Parserx.parse(unquote(context), unquote(info.do)) 144 | end 145 | end 146 | end 147 | 148 | ## Rule body grabber 149 | 150 | defp acquire_rule_information(head, bodies) do 151 | # head = defrule_fixup_head(head) 152 | # name = defrule_head_name(head) 153 | {name, _head_meta, args} = defrule_fixup_head(head) 154 | 155 | do_body = bodies[:do] || nil # throw "`defrule` must have a `do` body specified" 156 | after_body = bodies[:after] || nil 157 | else_body = bodies[:else] || nil # quote do context -> raise %ParseException{message: unquote("Rule `#{name}` "), context: context} end 158 | 159 | %{ 160 | name: name, 161 | args: args, 162 | do: do_body, 163 | after: after_body, 164 | else: else_body, 165 | } 166 | end 167 | 168 | ### Parser AST Generator 169 | 170 | # defp parser_to_ast(parser) do 171 | # env = defrule_env_new() 172 | # {_env, ast} = parser_to_ast(env, parser) 173 | # ast 174 | # end 175 | 176 | defp parser_to_ast(env, ast) 177 | defp parser_to_ast(env, {name, meta, scope}) when is_atom(name) and is_atom(scope) do 178 | parser_to_ast(env, {name, meta, []}) # Just delegating to the function call for now 179 | end 180 | defp parser_to_ast(%{commands: commands} = env, {name, _meta, args}) when is_atom(name) and is_list(args) do 181 | arity = length(args) 182 | case commands[{name, arity}] || commands[name] do 183 | nil -> throw {:INVALID_RULE_COMMAND, name, args, Map.keys(commands)} 184 | func when is_function(func, 1) -> func.(args) 185 | func when is_function(func, 2) -> func.(env, args) 186 | func when is_function(func, 3) -> func.(env, name, args) 187 | unknown -> throw {:INVALID_RULE_COMMAND_TYPE, name, args, unknown} 188 | end 189 | end 190 | defp parser_to_ast(_env, ast), do: throw {:INVALID_RULE, ast} 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | # defmacro defrules(head, bodies) do 207 | # 208 | # head = defrule_fixup_head(head) 209 | # name = defrule_head_name(head) 210 | # 211 | # do_body = bodies[:do] || throw "`defrule` must have a `do` body specified" 212 | # after_body = bodies[:after] || nil 213 | # else_body = bodies[:else] || nil # quote do context -> raise %ParseException{message: unquote("Rule `#{name}` "), context: context} end 214 | # 215 | # {_env, do_body} = defrule_gen_body(do_body) 216 | # 217 | # IO.puts("Do:") 218 | # Macro.to_string(do_body) |> IO.puts 219 | # IO.puts("After:") 220 | # Macro.to_string(after_body) |> IO.puts 221 | # IO.puts("Else:") 222 | # Macro.to_string(else_body) |> IO.puts 223 | # 224 | # quote do 225 | # @spirit_rules {unquote(name), unquote(Macro.escape do_body), unquote(Macro.escape after_body), unquote(Macro.escape else_body)} 226 | # end 227 | # 228 | # # throw {head, name, bodies, do_body, after_body, else_body, env} 229 | # end 230 | 231 | defp defrule_fixup_head(head) 232 | defp defrule_fixup_head({name, meta, scope}) when is_atom(name) and is_atom(scope) do 233 | defrule_fixup_head({name, meta, []}) 234 | end 235 | defp defrule_fixup_head({name, meta, args}) when is_atom(name) and is_list(args) do 236 | args = Enum.map(args, &defrule_fixup_head_arg/1) 237 | {name, meta, args} 238 | end 239 | defp defrule_fixup_head(head), do: throw {:INVALID_RULE_HEAD, head} 240 | 241 | defp defrule_fixup_head_arg(arg) 242 | defp defrule_fixup_head_arg({name, meta, scope}) when is_atom(name) and is_atom(scope) do 243 | {name, meta, scope} 244 | end 245 | defp defrule_fixup_head_arg(arg), do: throw {:INVALID_RULE_HEAD_ARG, arg} 246 | 247 | # defp defrule_head_name(head) 248 | # defp defrule_head_name({name, _meta, _args}), do: name 249 | # 250 | # defp defrule_gen_body(do_body) do 251 | # env = defrule_env_new() 252 | # defrule_gen_body(env, do_body) 253 | # end 254 | # defp defrule_gen_body(env, do_body) 255 | # defp defrule_gen_body(env, {name, meta, scope}) when is_atom(name) and is_atom(scope) do 256 | # case defrule_env_get_binding(env, name) do 257 | # nil -> defrule_perform_command(env, name, []) 258 | # x -> throw x 259 | # end 260 | # end 261 | # defp defrule_gen_body(env, {name, meta, args}) when is_atom(name) and is_list(args) do 262 | # defrule_perform_command(env, name, args) 263 | # end 264 | # defp defrule_gen_body(_env, do_body), do: throw {:INVALID_RULE_BODY_EXPRESSION, do_body} 265 | # 266 | # defp defrule_perform_command(env, name, args) 267 | # defp defrule_perform_command(%{commands: commands} = env, name, args) do 268 | # arity = length(args) 269 | # case commands[{name, arity}] || commands[name] do 270 | # nil -> throw {:INVALID_RULE_COMMAND, name, args, Map.keys(commands)} 271 | # func when is_function(func, 1) -> func.(args) 272 | # func when is_function(func, 2) -> func.(env, args) 273 | # func when is_function(func, 3) -> func.(env, name, args) 274 | # result -> throw {:INVALID_RULE_COMMAND_TYPE, name, args, result, Map.keys(commands)} 275 | # end 276 | # end 277 | 278 | # defp defrule_context_if(env, bodies) 279 | # defp defrule_context_if(%{failable: false} = env, bodies) do 280 | # context = defrule_env_get_context(env) 281 | # case bodies[:do] do 282 | # nil -> quote do case do unquote(context) -> unquote(context) end 283 | # body -> quote do case do unquote(context) -> unquote(context) |> unquote(body) end 284 | # end 285 | # end 286 | # defp defrule_context_if(env, bodies) do 287 | # # env = defrule_env_incr_context(env) 288 | # context = defrule_env_get_context(env) 289 | # do_body = bodies[:do] || quote do case do unquote(context) -> unquote(context) end end 290 | # else_body = bodies[:else] || quote do case do unquote(context) -> unquote(context) end end 291 | # ast = 292 | # quote do 293 | # case do 294 | # %{error: nil} = unquote(context) -> unquote(context) |> unquote(do_body) 295 | # %{} = unquote(context) -> unquote(context) |> unquote(else_body) 296 | # end 297 | # end 298 | # {env, ast} 299 | # end 300 | 301 | # defmacrop defrule_context_if_skipper(%{failable: true, skipable: true} = env, bodies) do 302 | # quote bind_quoted: [env: env, bodies: bodies] do 303 | # context = defrule_env_get_context(env) 304 | # do_body = bodies[:do] || quote do case do unquote(context) -> unquote(context) end end 305 | # else_body = bodies[:else] || quote do case do unquote(context) -> unquote(context) end end 306 | # ast = 307 | # quote do 308 | # case do 309 | # %{error: nil, skipper: skipper} = unquote(context) -> 310 | # case skipper do 311 | # nil -> unquote(context) 312 | # skipper -> unquote(context) |> skipper.() 313 | # end |> unquote(do_body) 314 | # unquote(context) -> unquote(context) |> unquote(else_body) 315 | # end 316 | # end 317 | # {env, ast} 318 | # end 319 | # end 320 | # defmacrop defrule_context_if_skipper(%{failable: true, skipable: false} = env, bodies) do 321 | # quote bind_quoted: [env: env, bodies: bodies] do 322 | # context = defrule_env_get_context(env) 323 | # do_body = bodies[:do] || quote do case do unquote(context) -> unquote(context) end end 324 | # else_body = bodies[:else] || quote do case do unquote(context) -> unquote(context) end end 325 | # ast = 326 | # quote do 327 | # case do 328 | # %{error: nil, skipper: skipper} = unquote(context) -> 329 | # case skipper do 330 | # nil -> unquote(context) 331 | # skipper -> unquote(context) |> skipper.() 332 | # end |> unquote(do_body) 333 | # unquote(context) -> unquote(context) |> unquote(else_body) 334 | # end 335 | # end 336 | # {env, ast} 337 | # end 338 | # end 339 | # defmacrop defrule_context_if_skipper(%{failable: false, skipable: true} = env, bodies) do 340 | # quote bind_quoted: [env: env, bodies: bodies] do 341 | # context = defrule_env_get_context(env) 342 | # do_body = bodies[:do] || quote do case do unquote(context) -> unquote(context) end end 343 | # ast = 344 | # quote do 345 | # case do 346 | # %{skipper: skipper} = unquote(context) -> 347 | # case skipper do 348 | # nil -> unquote(context) 349 | # skipper -> 350 | # %{unquote(context) | 351 | # 352 | # } 353 | # end 354 | # end |> unquote(do_body) 355 | # end 356 | # {env, ast} 357 | # end 358 | # end 359 | 360 | 361 | defp defrule_env_new(name), do: %{ 362 | failable: true, 363 | skipable: true, 364 | context_level: 0, 365 | bindings: %{}, 366 | commands: default_commands(), 367 | defps: [], 368 | name: name, 369 | counter: 0, 370 | scope: __MODULE__, 371 | } 372 | 373 | defp defrule_env_get_context(env, level_delta \\ 0) 374 | defp defrule_env_get_context(%{scope: scope, context_level: level}, delta) do 375 | Macro.var(String.to_atom("context" <> Integer.to_string(level + delta)), scope) 376 | end 377 | 378 | # defp defrule_env_incr_context(%{context_level: context_level} = env, delta \\ 1) do 379 | # %{env | context_level: context_level + delta} 380 | # end 381 | # 382 | # defp defrule_env_get_binding(env, name) do 383 | # env.bindings[name] 384 | # end 385 | 386 | 387 | 388 | 389 | 390 | 391 | defmacrop context_if(env, bodies) do 392 | quote bind_quoted: [env: env, bodies: bodies] do 393 | context = defrule_env_get_context(env) 394 | do_body = bodies[:do] || quote do case do unquote(context) -> unquote(context) end end 395 | else_body = bodies[:else] || quote do case do unquote(context) -> unquote(context) end end 396 | ast = 397 | case env do 398 | %{failable: true} -> 399 | quote do 400 | case do 401 | %{error: nil} = unquote(context) -> unquote(context) |> unquote(do_body) 402 | unquote(context) -> unquote(context) |> unquote(else_body) 403 | end 404 | end 405 | _ -> 406 | quote do 407 | unquote(do_body) 408 | end 409 | end 410 | {env, ast} 411 | end 412 | end 413 | 414 | defmacrop skip_do(env) do 415 | quote bind_quoted: [env: env] do 416 | context = defrule_env_get_context(env) 417 | context1 = defrule_env_get_context(env, 1) 418 | ast = 419 | case env do 420 | %{skipable: true} -> 421 | quote do 422 | case do 423 | %{skipper: skipper} = unquote(context) -> 424 | case skipper do 425 | nil -> unquote(context) 426 | skipper -> 427 | %{unquote(context) | 428 | skipper: nil, 429 | } |> skipper.() 430 | |> case do 431 | %{error: nil} = unquote(context1) -> 432 | %{unquote(context1) | 433 | skipper: unquote(context).skipper, 434 | result: unquote(context).result, 435 | } 436 | _bad_skipper -> unquote(context) 437 | end 438 | end 439 | end 440 | end 441 | _ -> 442 | quote do 443 | case do 444 | unquote(context) -> unquote(context) 445 | end 446 | end 447 | end 448 | {env, ast} 449 | end 450 | end 451 | 452 | defmacrop context_skip_if(env, bodies) do 453 | quote bind_quoted: [env: env, bodies: bodies] do 454 | context = defrule_env_get_context(env) 455 | {env, skipper} = skip_do(env) 456 | do_body = 457 | case bodies[:do] do 458 | nil -> skipper 459 | body -> quote do unquote(skipper) |> unquote(body) end 460 | end 461 | context_if(env, [do: do_body]++bodies) 462 | end 463 | end 464 | 465 | 466 | 467 | def default_commands(), do: %{ 468 | {:char, 0} => &cmd_char_0/2, 469 | :char => &cmd_char_n/2, 470 | :chars => &cmd_chars_n/2, 471 | :lit => &cmd_lit_n/2, 472 | {:|>, 2} => &cmd_seq_2/2, 473 | :seq => &cmd_seq_n/2, 474 | {:||, 2} => &cmd_alt_2/2, 475 | :alt => &cmd_alt_n/2, 476 | {:skip, 0} => &cmd_skip_0/2, 477 | {:skip, 2} => &cmd_skip_2/2, 478 | {:no_skip, 1} => &cmd_no_skip_1/2, 479 | {:lexeme, 1} => &cmd_lexeme_1/2, 480 | {:ignore, 1} => &cmd_ignore_1/2, 481 | {:repeat, 1} => &cmd_repeat_1_4/2, 482 | {:repeat, 2} => &cmd_repeat_1_4/2, 483 | {:repeat, 3} => &cmd_repeat_1_4/2, 484 | {:repeat, 4} => &cmd_repeat_1_4/2, 485 | } 486 | 487 | 488 | 489 | defp cmd_char_0(env, []) do 490 | context_skip_if(env) do 491 | quote do 492 | case do 493 | %{rest: <>} = context -> 494 | %{context | 495 | result: c, 496 | rest: rest, 497 | position: context.position + byte_size(<>), 498 | column: if(c===?\n, do: 1, else: context.column+1), 499 | line: context.line + if(c===?\n, do: 1, else: 0), 500 | } 501 | bad_context -> 502 | %{bad_context | 503 | error: %ExSpirit.Parser.ParseException{message: "Tried parsing out a character but the end of input was reached", context: bad_context}, 504 | } 505 | end 506 | end 507 | end 508 | end 509 | 510 | defp cmd_char_n(env, matchers) do 511 | c_ast = Macro.var(:c, __MODULE__) 512 | context_skip_if(env) do 513 | quote do 514 | case do 515 | %{rest: <>} = matched_context -> 516 | if unquote(cmd_char_n_matchers(c_ast, matchers)) do 517 | %{matched_context | 518 | result: c, 519 | rest: rest, 520 | position: matched_context.position + byte_size(<>), 521 | column: if(c===?\n, do: 1, else: matched_context.column+1), 522 | line: matched_context.line + if(c===?\n, do: 1, else: 0), 523 | } 524 | else 525 | %{matched_context | 526 | error: %ExSpirit.Parser.ParseException{message: "Tried parsing out any of the the characters of `#{inspect unquote(matchers)}` but failed due to the input character not matching", context: matched_context}, 527 | } 528 | end 529 | bad_context -> 530 | %{bad_context | 531 | error: %ExSpirit.Parser.ParseException{message: "Tried parsing out any of the the characters of `#{inspect unquote(matchers)}` but failed due to end of input", context: bad_context}, 532 | } 533 | end 534 | end 535 | end 536 | end 537 | 538 | defp cmd_char_n_matcher(c_ast, matcher) 539 | defp cmd_char_n_matcher(c_ast, matcher) when is_integer(matcher) and matcher>=0 do 540 | {true, quote do unquote(c_ast) === unquote(matcher) end} 541 | end 542 | defp cmd_char_n_matcher(c_ast, matcher) when is_integer(matcher) and matcher<0 do 543 | {false, quote do not(unquote(c_ast) === unquote(matcher)) end} 544 | end 545 | defp cmd_char_n_matcher(c_ast, {:-, _, [matcher]}) when is_integer(matcher) and matcher>0 do 546 | {false, quote do not(unquote(c_ast) === unquote(matcher)) end} 547 | end 548 | defp cmd_char_n_matcher(c_ast, {:.., _meta, [l, r]}) when is_integer(l) and is_integer(r) and l>=0 and r>=0 do 549 | {true, quote do unquote(c_ast) >= unquote(l) and unquote(c_ast) <= unquote(r) end} 550 | end 551 | defp cmd_char_n_matcher(c_ast, {:.., _meta, [l, r]}) when is_integer(l) and is_integer(r) and l<0 and r<0 do 552 | {false, quote do not(unquote(c_ast) >= unquote(l) and unquote(c_ast) <= unquote(r)) end} 553 | end 554 | defp cmd_char_n_matcher(c_ast, {:.., _meta, [{:-, _, [l]}, {:-, _, [r]}]}) when is_integer(l) and is_integer(r) and l>0 and r>0 do 555 | {false, quote do not(unquote(c_ast) >= unquote(l) and unquote(c_ast) <= unquote(r)) end} 556 | end 557 | defp cmd_char_n_matcher(_c_ast, [matcher | _rest]), do: throw {:INVALID_CHAR_MATCHER_TYPE, matcher} 558 | 559 | defp cmd_char_n_matchers(c_ast, matchers) when is_list(matchers) do 560 | matchers 561 | |> Enum.sort_by(fn 562 | l.._r -> l 563 | c -> c 564 | end, &>/2) 565 | |> Enum.reduce(nil, fn 566 | (matcher, nil) -> 567 | {_is_pos, left} = cmd_char_n_matcher(c_ast, matcher) 568 | left 569 | (matcher, left) -> 570 | case cmd_char_n_matcher(c_ast, matcher) do 571 | {true, ast} -> quote do unquote(left) or unquote(ast) end 572 | {false, ast} -> quote do unquote(left) and unquote(ast) end 573 | end 574 | end) 575 | end 576 | 577 | 578 | defp cmd_chars_n(env, matchers) do 579 | {matchers, opts} = Enum.split_while(matchers, ¬(Keyword.keyword?(&1))) 580 | opts = List.flatten(opts) 581 | minimumChars = opts[:min] || 1 582 | maximumChars = opts[:max] || -1 583 | context_skip_if(env) do 584 | quote do 585 | case do 586 | %{rest: rest} = matched_context -> 587 | {position, column, line, chars} = chars_increment_while_matching(unquote(matchers), unquote(maximumChars), matched_context.rest, 0, matched_context.column, matched_context.line, 0) 588 | if chars < unquote(minimumChars) do 589 | %{matched_context | 590 | error: %ExSpirit.Parser.ParseException{message: "Tried parsing out characters of `#{inspect unquote(matchers)}` but failed due to not meeting the minimum characters required of #{unquote(minimumChars)}", context: matched_context}, 591 | } 592 | else 593 | case matched_context.rest do 594 | <> -> 595 | %{matched_context | 596 | result: result, 597 | rest: result_rest, 598 | position: matched_context.position + position, 599 | column: column, 600 | line: line, 601 | } 602 | end 603 | end 604 | end 605 | end 606 | end 607 | end 608 | 609 | 610 | defp cmd_lit_n(env, orig_lits) do 611 | lits = 612 | orig_lits 613 | |> Enum.map(fn # Turn single characters into a binary 614 | c when is_integer(c) -> <> 615 | s -> s 616 | end) 617 | |> Enum.sort_by(&String.length/1, &>/2) # Sort to match longest first 618 | |> Enum.uniq() 619 | |> Enum.flat_map(fn s -> # flat_map'ing because `->` returns wrapped in a list because of Elixir oddness 620 | quote do 621 | %{rest: unquote(s)<>rest} = context -> %{context | rest: rest, result: nil} 622 | end 623 | end) 624 | lits = 625 | lits ++ quote do # `->` already gets returned as a list... 626 | context -> 627 | %{context | 628 | error: %ExSpirit.Parser.ParseException{message: "Failed matching out a literal of one of `#{inspect unquote(orig_lits)}`", context: context}, 629 | } 630 | end 631 | context_skip_if(env) do 632 | {:case, [], [[do: lits]]} 633 | end 634 | end 635 | 636 | 637 | defp cmd_seq_2(env, seq) do 638 | seq = cmd_seq_2_flatten({:|>, [], seq}) 639 | cmd_seq_n(env, seq) 640 | end 641 | 642 | defp cmd_seq_2_flatten(ast) 643 | defp cmd_seq_2_flatten({:|>, _, [left, right]}) do 644 | left = List.wrap(cmd_seq_2_flatten(left)) 645 | right = List.wrap(cmd_seq_2_flatten(right)) 646 | left ++ right 647 | end 648 | defp cmd_seq_2_flatten(ast), do: List.wrap(ast) 649 | 650 | defp cmd_seq_n(env, seqs) do 651 | [first_seq | rest_seq] = Enum.flat_map(seqs, &cmd_seq_2_flatten/1) 652 | {env, inner} = cmd_seq_n_expand(env, first_seq, rest_seq) 653 | context_skip_if(env) do 654 | inner 655 | end 656 | end 657 | 658 | defp cmd_seq_n_expand(env, this_ast, [next_ast | rest_ast]) do 659 | {env, this_ast} = parser_to_ast(env, this_ast) 660 | {env, inner} = cmd_seq_n_expand(env, next_ast, rest_ast) 661 | context_skip_if(env) do 662 | quote do 663 | unquote(this_ast) 664 | |> case do 665 | %{result: result} = context -> {result, context |> unquote(inner)} 666 | end 667 | |> case do 668 | {nil, %{error: nil} = context} -> context 669 | {result, %{error: nil, result: nil} = context} -> %{context | result: result} 670 | # {result, %{error: nil, result: results} = context} when is_tuple(results) -> %{context | result: :erlang.append_element(results, result)} # backwards 671 | # {result, %{error: nil, result: results} = context} -> %{context | result: {result, results}} 672 | {result, %{error: nil, result: results} = context} when is_list(results) -> %{context | result: [result | results]} 673 | {result, %{error: nil, result: results} = context} -> %{context | result: [result, results]} 674 | {_result, context} -> context 675 | end 676 | end 677 | end 678 | end 679 | defp cmd_seq_n_expand(env, this_ast, []) do 680 | parser_to_ast(env, this_ast) 681 | end 682 | 683 | 684 | defp cmd_alt_2(env, alt) do 685 | alt = cmd_alt_2_flatten({:||, [], alt}) 686 | cmd_alt_n(env, alt) 687 | end 688 | 689 | defp cmd_alt_2_flatten(ast) 690 | defp cmd_alt_2_flatten({:||, _, [left, right]}) do 691 | left = List.wrap(cmd_alt_2_flatten(left)) 692 | right = List.wrap(cmd_alt_2_flatten(right)) 693 | left ++ right 694 | end 695 | defp cmd_alt_2_flatten(ast), do: List.wrap(ast) 696 | 697 | defp cmd_alt_n(env, choices) do 698 | [first_choice | rest_choices] = Enum.flat_map(choices, &cmd_alt_2_flatten/1) 699 | context_binding = Macro.var(:original_context_alt, __MODULE__) 700 | {env, inner} = cmd_alt_n_expand(env, context_binding, first_choice, rest_choices) 701 | context_skip_if(env) do 702 | quote do 703 | case do 704 | unquote(context_binding) -> unquote(context_binding) |> unquote(inner) 705 | end 706 | end 707 | end 708 | end 709 | 710 | defp cmd_alt_n_expand(env, original_context_ast, this_ast, [next_ast | rest_ast]) do 711 | {env, this_ast} = parser_to_ast(env, this_ast) 712 | {env, inner} = cmd_alt_n_expand(env, original_context_ast, next_ast, rest_ast) 713 | ast = 714 | quote location: :keep do 715 | unquote(this_ast) |> case do 716 | %{error: nil} = good_context -> good_context 717 | %{error: %ExSpirit.Parser.ExpectationFailureException{}} = bad_context -> bad_context 718 | _bad_context -> unquote(original_context_ast) |> unquote(inner) 719 | end 720 | end 721 | {env, ast} 722 | end 723 | defp cmd_alt_n_expand(env, _context_ast, this_ast, []) do 724 | parser_to_ast(env, this_ast) 725 | end 726 | 727 | 728 | defp cmd_skip_0(env, []) do 729 | context_skip_if(env) do 730 | quote do 731 | case do context -> 732 | %{context | result: nil} 733 | end 734 | end 735 | end 736 | end 737 | 738 | 739 | defp cmd_skip_2(env, [parser, skipper]) do 740 | {env, parser} = parser_to_ast(env, parser) 741 | {env, skipper} = parser_to_ast(env, skipper) 742 | skipper_fn = quote do fn context -> context |> unquote(skipper) end end 743 | context_if(env) do 744 | quote do 745 | case do 746 | %{skipper: original_skipper} = context -> 747 | %{context | skipper: unquote(skipper_fn)} 748 | |> unquote(parser) 749 | |> case do 750 | context -> %{context | skipper: original_skipper} 751 | end 752 | end 753 | end 754 | end 755 | end 756 | 757 | 758 | defp cmd_no_skip_1(env, [parser]) do 759 | {env, parser} = parser_to_ast(env, parser) 760 | context_if(env) do 761 | quote do 762 | case do 763 | %{skipper: skipper} = context -> 764 | %{context | skipper: nil} 765 | |> unquote(parser) 766 | |> case do 767 | context -> %{context | skipper: skipper} 768 | end 769 | end 770 | end 771 | end 772 | end 773 | 774 | 775 | defp cmd_lexeme_1(env, [parser]) do 776 | {env, parser} = parser_to_ast(env, parser) 777 | context_skip_if(env) do 778 | quote do 779 | case do 780 | %{position: position} = context -> 781 | case context |> unquote(parser) do 782 | %{error: nil, position: new_position} = good_context -> 783 | bytes = new_position - position 784 | case context.rest do 785 | <> -> 786 | %{good_context | 787 | result: parsed, 788 | rest: rest, 789 | } 790 | _ -> 791 | %{good_context | 792 | result: nil, 793 | error: %ExSpirit.Parser.ParseException{message: "Lexeme failed, length needed is #{bytes} but available is only #{byte_size(context.rest)}", context: context, extradata: good_context}, 794 | } 795 | end 796 | context -> context 797 | end 798 | end 799 | end 800 | end 801 | end 802 | 803 | 804 | defp cmd_ignore_1(env, [parser]) do 805 | {env, parser} = parser_to_ast(env, parser) 806 | context_if(env) do 807 | quote do 808 | case do 809 | context -> %{(context |> unquote(parser)) | result: nil} 810 | end 811 | end 812 | end 813 | end 814 | 815 | 816 | defp cmd_repeat_1_4(env, opts) do 817 | {parser, opts} = 818 | case opts do 819 | [parser] -> {parser, []} 820 | [parser, opts] -> {parser, List.flatten(opts)} 821 | end 822 | minimum = opts[:min] || 1 823 | maximum = opts[:max] || :infinite 824 | sep = opts[:sep] || nil 825 | 826 | {env, ast_parser} = parser_to_ast(env, parser) 827 | fparser = quote do fn context -> context |> unquote(ast_parser) end end 828 | {env, fsep} = 829 | case sep do 830 | nil -> {env, nil} 831 | sep -> 832 | {env, sep} = parser_to_ast(env, quote do ignore(unquote(sep)) |> unquote(parser) end) 833 | f_ast = quote do fn context -> context |> unquote(sep) end end 834 | {env, f_ast} 835 | end 836 | 837 | context_if(env) do 838 | quote do 839 | case do 840 | context -> unquote(:'$repeat$')(unquote(fsep), unquote(fparser), unquote(minimum), unquote(maximum), context, 0) 841 | end 842 | end 843 | end 844 | end 845 | 846 | end 847 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExSpirit.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :ex_spirit, 6 | version: "0.4.0", 7 | elixir: "~> 1.4", 8 | description: description(), 9 | package: package(), 10 | docs: [ 11 | #logo: "path/to/logo.png", 12 | extras: ["README.md"], 13 | main: "readme", 14 | ], 15 | build_embedded: Mix.env == :prod, 16 | start_permanent: Mix.env == :prod, 17 | deps: deps()] 18 | end 19 | 20 | def application do 21 | [extra_applications: []] 22 | end 23 | 24 | def description do 25 | """ 26 | Spirit-style PEG-like parsing library for Elixir. 27 | """ 28 | end 29 | 30 | def package do 31 | [ 32 | licenses: ["MIT"], 33 | name: :ex_spirit, 34 | maintainers: ["OvermindDL1"], 35 | links: %{"Github" => "https://github.com/OvermindDL1/ex_spirit"} 36 | ] 37 | end 38 | 39 | defp deps do 40 | [ 41 | {:ex_doc, "~> 0.17", only: [:dev]}, 42 | {:stream_data, "0.3.0", only: [:test]}, 43 | {:cortex, "~> 0.2.0", only: [:dev, :test]} 44 | ] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"cortex": {:hex, :cortex, "0.2.1", "8a13be1bf570cdc5f6f83855f6c5ba8c4a92bc32cf8da042fa1519537ae24315", [:mix], [{:file_system, "~> 0.2", [hex: :file_system, optional: false]}]}, 2 | "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], []}, 3 | "ex_doc": {:hex, :ex_doc, "0.17.1", "39f777415e769992e6732d9589dc5846ea587f01412241f4a774664c746affbb", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, 4 | "file_system": {:hex, :file_system, "0.2.1", "c4bec8f187d2aabace4beb890f0d4e468f65ca051593db768e533a274d0df587", [:mix], []}, 5 | "stream_data": {:hex, :stream_data, "0.3.0", "cbfc8e3212f64683615657ea27804126a42ded634adfdfee258bf087ee605d46", [:mix], []}} 6 | -------------------------------------------------------------------------------- /test/ex_spirit/parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExSpirit.Parser.SymbolTest do 2 | use ExUnit.Case 3 | use ExSpirit.Parser, text: true 4 | use ExUnitProperties 5 | alias ExSpirit.TreeMap 6 | 7 | defp example_tree_map() do 8 | TreeMap.new() 9 | |> TreeMap.add_text("x", "x") 10 | |> TreeMap.add_text("yy", "yy") 11 | |> TreeMap.add_text("zzz", "zzz") 12 | |> TreeMap.add_text("olá", "olá") 13 | |> TreeMap.add_text("こんにちは", "こんにちは") 14 | end 15 | 16 | defp tree_map_from_strings(strings) do 17 | Enum.reduce strings, TreeMap.new(), fn string, tree_map -> 18 | tree_map |> TreeMap.add_text(string, string) 19 | end 20 | end 21 | 22 | test "`symbols` parser return context with right position (ASCII test case)" do 23 | # rest == "" 24 | assert %{result: "x", position: 1, rest: ""} = parse "x", symbols(example_tree_map()) 25 | assert %{result: "yy", position: 2, rest: ""} = parse "yy", symbols(example_tree_map()) 26 | assert %{result: "zzz", position: 3, rest: ""} = parse "zzz", symbols(example_tree_map()) 27 | # rest != "" 28 | assert %{result: "x", position: 1, rest: "1"} = parse "x1", symbols(example_tree_map()) 29 | assert %{result: "yy", position: 2, rest: "1"} = parse "yy1", symbols(example_tree_map()) 30 | assert %{result: "zzz", position: 3, rest: "1"} = parse "zzz1", symbols(example_tree_map()) 31 | end 32 | 33 | test "`symbols` parser return context with right position (UTF8 test case)" do 34 | pt_pos = byte_size("olá") 35 | jp_pos = byte_size("こんにちは") 36 | # rest == "" 37 | assert %{result: "olá", position: ^pt_pos, rest: ""} = parse "olá", symbols(example_tree_map()) 38 | assert %{result: "こんにちは", position: ^jp_pos, rest: ""} = parse "こんにちは", symbols(example_tree_map()) 39 | # rest != "" 40 | assert %{result: "olá", position: ^pt_pos, rest: "1"} = parse "olá1", symbols(example_tree_map()) 41 | assert %{result: "こんにちは", position: ^jp_pos, rest: "1"} = parse "こんにちは1", symbols(example_tree_map()) 42 | end 43 | 44 | property "`symbols` parser return context with right position" do 45 | check all word <- string(:printable), 46 | word != "", 47 | rest <- string(:printable) do 48 | 49 | tree_map = tree_map_from_strings([word]) 50 | expected_position = byte_size(word) 51 | 52 | assert %{result: ^word, position: ^expected_position, rest: ^rest} = 53 | parse (word <> rest), symbols(tree_map) 54 | end 55 | end 56 | 57 | test "`symbols` parser composes with `lexeme` (ASCII test case)" do 58 | # rest == "" 59 | assert parse("x", symbols(example_tree_map())) == parse("x", lexeme(symbols(example_tree_map()))) 60 | assert parse("yy", symbols(example_tree_map())) == parse("yy", lexeme(symbols(example_tree_map()))) 61 | assert parse("zzz", symbols(example_tree_map())) == parse("zzz", lexeme(symbols(example_tree_map()))) 62 | # rest != "" 63 | assert parse("x1", symbols(example_tree_map())) == parse("x1", lexeme(symbols(example_tree_map()))) 64 | assert parse("yy1", symbols(example_tree_map())) == parse("yy1", lexeme(symbols(example_tree_map()))) 65 | assert parse("zzz1", symbols(example_tree_map())) == parse("zzz1", lexeme(symbols(example_tree_map()))) 66 | end 67 | 68 | test "`symbols` parser composes with `lexeme` (UTF8 test case)" do 69 | # rest == "" 70 | assert parse("olá", symbols(example_tree_map())) == parse("olá", lexeme(symbols(example_tree_map()))) 71 | assert parse("こんにちは", symbols(example_tree_map())) == parse("こんにちは", lexeme(symbols(example_tree_map()))) 72 | # rest != "" 73 | assert parse("olá1", symbols(example_tree_map())) == parse("olá1", lexeme(symbols(example_tree_map()))) 74 | assert parse("こんにちは1", symbols(example_tree_map())) == parse("こんにちは1", lexeme(symbols(example_tree_map()))) 75 | end 76 | 77 | # Note: the success and failure cases are separated because it's probably quite rare 78 | # to have a success case with purely random strings, and that's the most interesting case. 79 | property "`symbols` parser composes with `lexeme` - in case of success" do 80 | check all word <- string(:printable), 81 | word != "", 82 | rest <- string(:printable) do 83 | 84 | tree_map = tree_map_from_strings([word]) 85 | input = word <> rest 86 | 87 | assert parse(input, lexeme(symbols(tree_map))) == parse(input, symbols(tree_map)) 88 | end 89 | end 90 | 91 | property "`symbols` parser composes with `lexeme` - in case of failure" do 92 | check all word <- string(:printable), 93 | word != "", 94 | prefix <- string(:printable), 95 | prefix != "", 96 | not String.starts_with?(prefix <> word, word), 97 | rest <- string(:printable) do 98 | 99 | tree_map = tree_map_from_strings([word]) 100 | input = prefix <> word <> rest 101 | 102 | assert parse(input, lexeme(symbols(tree_map))) == parse(input, symbols(tree_map)) 103 | end 104 | end 105 | 106 | property "`symbols` matches the longest string" do 107 | check all short_word <- string(:printable), 108 | short_word != "", 109 | suffix <- string(:printable), 110 | suffix != "", 111 | rest <- string(:printable) do 112 | 113 | long_word = short_word <> suffix 114 | assert byte_size(long_word) > byte_size(short_word) # self documenting 115 | tree_map = tree_map_from_strings([short_word, long_word]) 116 | expected_position = byte_size(long_word) 117 | input = long_word <> rest 118 | 119 | assert %{result: ^long_word, position: ^expected_position, rest: ^rest} 120 | = parse(input, symbols(tree_map)) 121 | end 122 | end 123 | 124 | property "`symbols` parser matches only at the begining of the input (trivial)" do 125 | check all word <- string(:printable), 126 | word != "", 127 | prefix <- string(:printable), 128 | prefix != "", 129 | not String.starts_with?(prefix <> word, word), 130 | rest <- string(:printable) do 131 | 132 | tree_map = tree_map_from_strings([word]) 133 | input = prefix <> word <> rest 134 | 135 | assert %{result: nil, position: 0, rest: ^input, error: error} = parse(input, symbols(tree_map)) 136 | assert error != nil 137 | end 138 | end 139 | end 140 | 141 | 142 | 143 | defmodule ExSpirit.ParserDocTests do 144 | use ExUnit.Case 145 | doctest ExSpirit.Parser 146 | doctest ExSpirit.Parser.Text 147 | end 148 | -------------------------------------------------------------------------------- /test/ex_spirit/parserx_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExSpirit.ParserxTest do 2 | use ExUnit.Case 3 | # doctest ExSpirit.Parserx 4 | 5 | use ExSpirit.Parserx 6 | alias ExSpirit.Parser.ParseException 7 | 8 | 9 | # defmodule TestParser do 10 | # use ExSpirit.Parserx 11 | # 12 | # defrule single_char0a, do: char 13 | # # defrule single_char0b, do: char() 14 | # # defrule single_char0c(), do: char 15 | # # defrule single_char0d(), do: char() 16 | # end 17 | 18 | test "parse: char" do 19 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char 20 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char() 21 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char(?a) 22 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char(?a, ?b) 23 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char(?b, ?a) 24 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char(?a..?b) 25 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char(-?b) 26 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char(-?b..-?z) 27 | assert %{error: %ParseException{}, result: nil, rest: "b"} = parse "b", char(?a) 28 | assert %{error: %ParseException{}, result: nil, rest: ""} = parse "", char() 29 | assert %{error: %ParseException{}, result: nil, rest: ""} = parse "", char(?a) 30 | end 31 | 32 | test "parse: chars" do 33 | assert %{error: nil, result: "a", rest: ""} = parse "a", chars(?a) 34 | assert %{error: nil, result: "aa", rest: ""} = parse "aa", chars(?a) 35 | assert %{error: nil, result: "aaa", rest: ""} = parse "aaa", chars(?a) 36 | assert %{error: nil, result: "a", rest: "b"} = parse "ab", chars(?a) 37 | assert %{error: nil, result: "aa", rest: "b"} = parse "aab", chars(?a) 38 | assert %{error: nil, result: "aaa", rest: "b"} = parse "aaab", chars(?a) 39 | assert %{error: nil, result: "a", rest: ""} = parse "a", chars(?a, ?b) 40 | assert %{error: nil, result: "a", rest: ""} = parse "a", chars(?b, ?a) 41 | assert %{error: nil, result: "aa", rest: ""} = parse "aa", chars(?a, min: 2) 42 | assert %{error: nil, result: "a", rest: "a"} = parse "aa", chars(?a, max: 1) 43 | assert %{error: nil, result: "a", rest: ""} = parse "a", chars(?a, min: 1, max: 2) 44 | assert %{error: nil, result: "aa", rest: ""} = parse "aa", chars(?a, min: 1, max: 2) 45 | assert %{error: nil, result: "aa", rest: "a"} = parse "aaa", chars(?a, min: 1, max: 2) 46 | assert %{error: %ParseException{}, result: nil, rest: ""} = parse "", chars(?a) 47 | assert %{error: %ParseException{}, result: nil, rest: "b"} = parse "b", chars(?a) 48 | assert %{error: %ParseException{}, result: nil, rest: "a"} = parse "a", chars(?a, min: 2) 49 | end 50 | 51 | test "parse: lit" do 52 | assert %{error: nil, result: nil, rest: ""} = parse "t", lit(?t) 53 | assert %{error: nil, result: nil, rest: ""} = parse "t", lit("t") 54 | assert %{error: nil, result: nil, rest: ""} = parse "test", lit("test") 55 | assert %{error: nil, result: nil, rest: ""} = parse "t", lit(?t, ?a) 56 | assert %{error: nil, result: nil, rest: ""} = parse "t", lit(?a, ?t) 57 | assert %{error: nil, result: nil, rest: ""} = parse "t", lit(?t, "a") 58 | assert %{error: nil, result: nil, rest: ""} = parse "t", lit("t", ?a) 59 | assert %{error: nil, result: nil, rest: ""} = parse "t", lit("t", "a") 60 | assert %{error: nil, result: nil, rest: ""} = parse "t", lit("t", ?t) 61 | assert %{error: nil, result: nil, rest: ""} = parse "t", lit(?t, "t") 62 | assert %{error: nil, result: nil, rest: ""} = parse "test", lit("te", "test") 63 | assert %{error: nil, result: nil, rest: ""} = parse "test", lit("test", "te") 64 | end 65 | 66 | test "parse: |>" do 67 | assert %{error: nil, result: 'ab', rest: "cd"} = parse "abcd", char(?a) |> char(?b) 68 | assert %{error: nil, result: 'abc', rest: "d"} = parse "abcd", char(?a) |> char(?b) |> char(?c) 69 | assert %{error: nil, result: 'abcd', rest: ""} = parse "abcd", char(?a) |> char(?b) |> char(?c) |> char(?d) 70 | assert %{error: %ParseException{}, result: nil, rest: "abcd"} = parse "abcd", char(?b) |> char(?c) 71 | end 72 | 73 | test "parse: seq" do 74 | assert %{error: nil, result: 'ab', rest: "cd"} = parse "abcd", seq(char(?a), char(?b)) 75 | assert %{error: nil, result: 'abc', rest: "d"} = parse "abcd", seq(char(?a), char(?b), char(?c)) 76 | assert %{error: nil, result: 'abcd', rest: ""} = parse "abcd", seq(char(?a), char(?b), char(?c), char(?d)) 77 | assert %{error: %ParseException{}, result: nil, rest: "abcd"} = parse "abcd", seq(char(?b), char(?c)) 78 | end 79 | 80 | test "parse: ||" do 81 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char(?a) || char(?b) 82 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char(?b) || char(?a) 83 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char(?a) || char(?b) || char(?c) 84 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char(?c) || char(?b) || char(?a) 85 | end 86 | 87 | test "parse: alt" do 88 | assert %{error: nil, result: ?a, rest: ""} = parse "a", alt(char(?a), char(?b)) 89 | assert %{error: nil, result: ?a, rest: ""} = parse "a", alt(char(?b), char(?a)) 90 | assert %{error: nil, result: ?a, rest: ""} = parse "a", alt(char(?a), char(?b), char(?c)) 91 | assert %{error: nil, result: ?a, rest: ""} = parse "a", alt(char(?c), char(?b), char(?a)) 92 | end 93 | 94 | test "parse: skipper" do 95 | assert %{error: nil, result: ?a, rest: ""} = parse "a", char, skipper: char(?\s) 96 | assert %{error: nil, result: ?a, rest: ""} = parse " a", char, skipper: char(?\s) 97 | assert %{error: nil, result: ?\s, rest: "a"} = parse " a", char, skipper: char(?\s) 98 | assert %{error: nil, result: ?a, rest: ""} = parse " a", char, skipper: chars(?\s) 99 | assert %{error: nil, result: 'aa', rest: ""} = parse "aa", char |> char, skipper: char(?\s) 100 | assert %{error: nil, result: 'aa', rest: ""} = parse "a a", char |> char, skipper: char(?\s) 101 | assert %{error: nil, result: 'a ', rest: "a"} = parse "a a", char |> char, skipper: char(?\s) 102 | 103 | # Explicit skip 104 | assert %{error: nil, result: 'aa', rest: ""} = parse "a a", char |> skip |> char, skipper: char(?\s) 105 | 106 | # Turn off skip 107 | assert %{error: nil, result: ?a, rest: ""} = parse "a", no_skip(char), skipper: char(?\s) 108 | assert %{error: nil, result: ?\s, rest: "a"} = parse " a", no_skip(char), skipper: char(?\s) 109 | 110 | # Change skip 111 | assert %{error: nil, result: ?a, rest: ""} = parse "ba", skip(char, char(?b)), skipper: char(?\s) 112 | end 113 | 114 | test "parse: lexeme" do 115 | assert %{error: nil, result: "a", rest: ""} = parse "a", lexeme(char) 116 | assert %{error: nil, result: "a", rest: "b"} = parse "ab", lexeme(char) 117 | assert %{error: nil, result: "ab", rest: ""} = parse "ab", lexeme(char |> char) 118 | end 119 | 120 | test "parse: ignore" do 121 | assert %{error: nil, result: nil, rest: ""} = parse "a", ignore(char) 122 | assert %{error: nil, result: nil, rest: "b"} = parse "ab", ignore(char) 123 | assert %{error: nil, result: nil, rest: ""} = parse "ab", ignore(char |> char) 124 | end 125 | 126 | test "parse: repeat" do 127 | assert %{error: nil, result: 'a', rest: ""} = parse "a", repeat(char) 128 | assert %{error: nil, result: 'aaa', rest: ""} = parse "aaa", repeat(char) 129 | assert %{error: nil, result: 'aa', rest: ""} = parse "aa", repeat(char) 130 | assert %{error: %ParseException{}, result: nil, rest: ""} = parse "", repeat(char) 131 | assert %{error: %ParseException{}, result: nil, rest: ""} = parse "a", repeat(char, min: 2) 132 | assert %{error: nil, result: 'aa', rest: ""} = parse "aa", repeat(char, min: 2) 133 | assert %{error: nil, result: 'aaa', rest: ""} = parse "aaa", repeat(char, max: 3) 134 | assert %{error: nil, result: 'aaa', rest: "a"} = parse "aaaa", repeat(char, max: 3) 135 | assert %{error: nil, result: 'a', rest: ""} = parse "a", repeat(char(-?,), sep: char(?,)) 136 | assert %{error: nil, result: 'a', rest: "a"} = parse "aa", repeat(char(-?,), sep: char(?,)) 137 | assert %{error: nil, result: 'aa', rest: ""} = parse "a,a", repeat(char(-?,), sep: char(?,)) 138 | assert %{error: nil, result: 'aaaa', rest: ""} = parse "a,a,a,a", repeat(char(-?,), sep: char(?,)) 139 | assert %{error: nil, result: 'aaaa', rest: "a"} = parse "a,a,a,aa", repeat(char(-?,), sep: char(?,)) 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /test/ex_spirit_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExSpiritTest do 2 | use ExUnit.Case 3 | doctest ExSpirit 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | # Documentation template 4 | _ = """ 5 | #### 6 | 7 | 8 | 9 | ##### Examples 10 | 11 | ```elixir 12 | 13 | ``` 14 | """ 15 | 16 | defmodule ExSpirit.Tests.Parser do 17 | use ExSpirit.Parser, text: true 18 | 19 | defrule testrule( 20 | seq([ uint(), lit(?\s), uint() ]) 21 | ) 22 | 23 | defrule testrule_pipe( 24 | seq([ uint(), lit(?\s), uint() ]) 25 | ), pipe_result_into: Enum.map(fn i -> i-40 end) 26 | 27 | defrule testrule_fun( 28 | seq([ uint(), lit(?\s), uint() ]) 29 | ), fun: (fn context -> %{context | result: {"altered", context.result}} end).() 30 | 31 | defrule testrule_context(context) do 32 | %{context | result: "always success"} 33 | end 34 | 35 | defrule testrule_context_arg(context, value) do 36 | %{context | result: value} 37 | end 38 | end 39 | --------------------------------------------------------------------------------