aboutsummaryrefslogtreecommitdiff
path: root/src/parcom.cr
blob: 5bb6aeef634a502963eb8a59c352ee40a4ed89e6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
require "./parcom/*"

module Parcom
  VERSION = "0.3.0"

  # A ParserFail exception should be raised by `Parser#parse` when
  # a parse attempt is unsuccessful.
  class ParserFail < Exception
  end

  # Provides a more convenient syntax for combining parsers via `Parser#and_then`.
  # The first and second arguments are types used for the parser's type.
  # The thirs argument is a string literal used for the name of the parser.
  # This is followed by any number of 2-tuples containing a variable name and
  # an expression resolving to a `Parser(t.class, _)`, whose success value will
  # be stored in the aformentioned variable. The `make` named argument is an
  # expression that resolves to a `Parser(t.class, u.class)`. This parser is
  # the final step of the parser chain. The `pure` named argument is an
  # expression that resolves to a `u.class`. This expression is passed to a
  # call to `Parser(t.class, u.class).pure`, which will be the final step
  # of the parser chain.
  #
  # Example:
  # ```
  # any_word = Parser(Char, Char).satisfy(&.letter?).some.map(&.join)
  # ws = Parser(Char, Array(Char)).satisfy(&.whitespace?).many
  # two_of_same_word = parser_chain Char, String, "two words",
  #   {word, any_word},
  #   {_,    ws},
  #   make: Parser.token_sequence(word.chars).map(&.join)
  #
  # tokens = Tokens.from_string("foo   foo")
  # result = two_of_same_word.parse(tokens)
  # result.value # => "foo"
  #
  # # The above definition of `two_of_same word`
  # # is an alternative way of doing this:
  # two_of_same_word = any_word.and_then do |word|
  #   ws.and_then do |_|
  #     Parser.token_sequence(word.chars).map(&.join)
  #   end
  # end.named("two words")
  # ```
  #
  # This macro is based on Haskell's do-notation.
  macro parser_chain(t, u, name, *steps, make = nil, pure = nil )
    {% if make.nil? && pure.nil? %}
      raise ArgumentError.new("Expected exactly one of 'make' and 'pure', but got neither")
    {% elsif !make.nil? && !pure.nil? %}
      raise ArgumentError.new("Expected exactly one of 'make' and 'pure', but got both")
    {% else %}
      Parser({{t}}, {{u}}).new({{name}}) do |tokens|
        {% for tup, index in steps %}
          {{tup.last}}.and_then do |{{tup.first}}|
        {% end %}
        {% if !make.nil? %}
          {{make}}
        {% else %}
          Parser({{t}}, {{u}}).pure({{pure}})
        {% end %}
        {% for _, _ in steps %}
          end
        {% end %}
        .parse(tokens)
      end
    {% end %}
  end
end