include :peg
include 'peg/ast'

#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.squish grammar }

  set :line seq(any_ref(:rule :comment) any(ref(:line_break) no(anything)))

  set :comment seq(str("#") kleene(seq no(str("\n")) anything))

  #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.squish rule_def }

  #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.squish rule_list }

  #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.squish rule_seq }

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

  #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 seq(str("(") ref(:rule_list) str(")"))

  #Matches a string literal
  set :str_literal action seq(str('"') label(:content kleene(any str('\\\\') str('\"') seq(no(str '"') anything))) str('"')) { my.squish str_lit }

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

  #Matches a regular expression
  set :reg_literal action seq(str('/') label(:content kleene(any str('\\/') seq(no(str '/') anything))) str('/')) { my.squish regex_rule }

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

  #Matches a label
  set :label action seq(label(:label_name ref(:rule_name)) str(":") label(:exp ref(:rule_exp))) { my.squish rule_label }

  #Matches an optional rule:
  #  some_rule?
  set :maybe action seq(ref(:simple_rule_exp) str("?")) { my.squish maybe_rule }

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

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

  #Matches a negative lookahead:
  #  !some_rule
  set :not action seq(str("!") ref(:simple_rule_exp)) { my.squish not_rule }

  #Matches positive lookahead:
  #  &some_rule
  set :and action seq(str("&") ref(:simple_rule_exp)) { my.squish and_rule }

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

  #Matches an object squish-in appended to a rule:
  #  some_rule <cool_thing>
  set :squish action reg(/<(\w+)>/) { my.squish set_squish }
}

#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_syntax, :peg_parser