include :peg

#This is a PEG for parsing PEGs
peg_syntax = peg.new {
  #A grammar is a list of rules
  set :grammar action many(ref :line) {
    my.output = { name |
      "include :peg\n#{name} = peg.new\n" <<
      matched.map({ e | e.output }).join("\n  ") <<
      "\n\n" <<
      "export #{name}, \"#{name}_parser\""
    }
  }

  set :line action seq(any_ref(:rule :comment) any(ref(:line_break) no(anything))) {
    my.output = matched.first.output
  }

  set :comment action seq(str("#") kleene(seq no(str("\n")) anything)) {
    my.output = ""
  }

  set :error action seq(str("%") many(seq no(str("%")) anything) str("%")) {
    my.output = { "parse_error(\"#{matched[1].text}\")" }
  }

  #Optional spaces
  set :opt_spaces reg /\s*/

  #Required spaces
  set :spaces reg /( )+/

  #Multiple line breaks
  set :line_break many(str("\n"))

  #A rule looks like:
  #  name = list of rule items
  set :rule action seq(ref(:rule_name) ref(:opt_spaces) str("=") ref(:opt_spaces) ref(:rule_list) reg(/( )*/)) {
    my.output = { "brat.with_this { set(:#{matched.first.text}, #{matched[-2].output}) }" }
  }

  #A rule name can only contain word characters
  set :rule_name reg /\w+/

  #A rule list is a sequence of optional rules, separated by |
  set :rule_list action seq(ref(:rule_seq) kleene(seq ref(:opt_spaces) str("|") ref(:opt_spaces) ref(:rule_seq))) {
    my.output = {
      first = matched.first
      rest = matched.last

      true? rest.elements.empty?
        {
          first.output
        }
        {
          options = rest.elements.map { r |
            r.elements.last.output
          }

          "any(#{first.output}, #{options.join ','})"
        }
    }
  }

  #Matches a sequence of basic matchers separated by spaces
  set :rule_seq action seq(ref(:rule_exp) kleene(seq_ref :spaces :rule_exp) maybe(seq ref(:spaces) any_ref(:action :squish))) {
    my.output = {
      first = matched.first
      rest = matched[1]
      act = false? matched[2].matched == ""
              { matched[2].matched.elements.last }
              { null }

      out = true? rest.elements.empty?
            {
              first.output
            }
            {
              "seq(#{first.output}, #{rest.elements.map({ e | e.elements.last.output }).join(',')})"
            }

      null? act
        { out }
        { "action(#{out}, #{act.output})" }
    }
  }

  #A rule expression is an actual matcher or grouping
  set :rule_exp any_ref(:maybe :many :kleene :not :and :simple_rule_exp :error)

  #Simple rules (i.e., not suffixed rules like + or *)
  set :simple_rule_exp any_ref(:label :str_literal :rule_ref :reg_literal :anything :paren_rule)

  #Matches parentheses, which denote priority
  set :paren_rule action seq(str("(") ref(:rule_list) str(")")) {
    my.output = matched[1].output
  }

  #Matches a string literal
  set :str_literal action seq(str('"') kleene(any str('\\\\') str('\"') seq(no(str '"') anything)) str('"')) {
    my.output = "str(#{text})"
  }

  #References the name of another rule
  set :rule_ref action label(:name ref(:rule_name)) {
    my.output = "ref(:#{name.text})"
  }

  #Matches a regular expression
  set :reg_literal action seq(str('/') label(:inner kleene(any str('\\/') seq(no(str '/') anything))) str('/')) {
    my.output = "reg(/\\G#{inner.text}/)"
  }

  #Matches a period
  set :anything action seq(str(".") maybe(ref :spaces)) {
    my.output = "anything()"
  }

  #Matches a label (but labels aren't really working currently)
  set :label action seq(label(:label_name ref(:rule_name)) str(":") label(:exp ref(:rule_exp))) {
    my.output = "label(:#{label_name.text} #{exp.output})"
  }

  #Matches an optional rule:
  #  some_rule?
  set :maybe action seq(ref(:simple_rule_exp) str("?")) {
    my.output = "maybe(#{matched.first.output})"
  }

  #Matches one or more of a rule:
  #  some_rule+
  set :many action seq(ref(:simple_rule_exp) str("+")) {
    my.output = "many(#{matched.first.output})"
  }

  #Matches zero or more of a rule:
  #  some_rule*
  set :kleene action seq(ref(:simple_rule_exp) str("*")) {
    my.output = "kleene(#{matched.first.output})"
  }

  #Matches a negative lookahead:
  #  !some_rule
  set :not action seq(str("!") ref(:simple_rule_exp)) {
    my.output = "no(#{matched.last.output})"
  }

  #Matches positive lookahead:
  #  &some_rule
  set :and action seq(str("&") ref(:simple_rule_exp)) {
    my.output = {
      "and(#{matched.last.output})"
    }
  }

  #Matches an action appended to a rule:
  #  some_rule { ... }
  set :action action seq(str("{") kleene(any(reg(/[^{}]+/) ref(:action))) str("}")) {
    my.output = my.text
  }

  #Matches an object squish-in appended to a rule:
  #  some_rule <cool_thing>
  set :squish action reg(/<(\w+)>/) {
    my.output = "{ my.squish #{my.matched[1]} }"
  }
}

#Generates a parser with the given name from the PEG syntax passed in
peg.make_parser = { name, grammar, *included |
  result = peg_syntax.parse grammar
  include_files = true? included.empty?
                    { "" }
                    { "includes #{included.map({ i | "'#{i}'" }).join(' ')}\n" }

  true? result
    { true? result.matched_all?
      { include_files << result.output(name) }
      {
        last_line = result.text.split("\n").last
        p "Error parsing around: #{last_line}"
      }
    }
    { p "Could not parse." }
}

export peg, :peg