docs: def addRule(self, doc, func, _preprocess=True):
          """Add a grammar rules to _self.rules_, _self.rule2func_,
              and _self.rule2name_
  
          Comments, lines starting with # and blank lines are stripped from
          doc. We also allow limited form of * and + when there it is of
          the RHS has a single item, e.g.
                 stmts ::= stmt+
          """
          fn = func
  
          # remove blanks lines and comment lines, e.g. lines starting with "#"
          doc = os.linesep.join([s for s in doc.splitlines() if s and not re.match("^\s*#", s)])
  
          rules = doc.split()
  
          index = []
          for i in range(len(rules)):
              if rules[i] == '::=':
                  index.append(i-1)
          index.append(len(rules))
  
          for i in range(len(index)-1):
              lhs = rules[index[i]]
              rhs = rules[index[i]+2:index[i+1]]
              rule = (lhs, tuple(rhs))
  
              if _preprocess:
                  rule, fn = self.preprocess(rule, func)
  
              # Handle a stripped-down form of *, +, and ?:
              #   allow only one nonterminal on the right-hand side
              if len(rule[1]) == 1:
  
                  if rule[1][0] == rule[0]:
                      raise TypeError("Complete recursive rule %s" % rule2str(rule))
  
                  repeat = rule[1][-1][-1]
                  if repeat in ('*', '+', '?'):
                      nt = rule[1][-1][:-1]
  
                      if repeat == '?':
                          new_rule_pair = [rule[0], list((nt,))]
                          self.optional_nt.add(rule[0])
                      else:
                          self.list_like_nt.add(rule[0])
                          new_rule_pair = [rule[0], [rule[0]] + list((nt,))]
                      new_rule = rule2str(new_rule_pair)
                      self.addRule(new_rule, func, _preprocess)
                      if repeat == '+':
                          second_rule_pair = (lhs, (nt,))
                      else:
                          second_rule_pair = (lhs, tuple())
                      new_rule = rule2str(second_rule_pair)
                      self.addRule(new_rule, func, _preprocess)
                      continue
  
              if lhs in self.rules:
                  if rule in self.rules[lhs]:
                      if 'dups' in self.debug and self.debug['dups']:
                          self.duplicate_rule(rule)
                      continue
                  self.rules[lhs].append(rule)
              else:
                  self.rules[lhs] = [ rule ]
              self.rule2func[rule] = fn
              self.rule2name[rule] = func.__name__[2:]
              self.ruleschanged = True
  
              # Note: In empty rules, i.e. len(rule[1] == 0, we don't
              # call reductions on explicitly. Instead it is computed
              # implicitly.
              if self.profile_info is not None and len(rule[1]) > 0:
                  rule_str = self.reduce_string(rule)
                  if rule_str not in self.profile_info:
                      self.profile_info[rule_str] = 0
              pass
          return