Работа с грамматической неоднозначностью при синтаксическом анализе HTSQL


Я пишу грамматику для синтаксического анализа HTSQL и застрял с тем, как иметь дело с повторным использованием символа / как для операторов сегмента, так и для операторов деления. Описанная грамматика не слишком формальна, поэтому я следил за точным выводом реализации Python, которая с первого взгляда кажется рукописным синтаксическим анализатором, а не генератором синтаксического анализатора - для справки генератор синтаксического анализатора, который я в настоящее время использую, является CL-YACC с помощью CL-LEX. (FWIW полная вещь здесь , хотя, вероятно, немного устарела.)

Одна двусмысленность, с которой я борюсь, возникает из-за того, что "/1" разбирается как '(:COLLECT (:INTEGER "1"))', но "/1/2" разбирается как '(:COLLECT (:OPERATOR / (:INTEGER "1") (:INTEGER "2")))', то есть один является разделителем сегментов, другой-делением; "/1//2" снова является '(:COLLECT (:OPERATOR / (:INTEGER "1") (:COLLECT (:INTEGER "2"))))'.

Таким образом, вопрос заключается в том, как я могу справиться с этим в спецификации грамматики, не прибегая к переключению на ручной синтаксический анализатор? Переключился бы на другой класс генератора синтаксического анализатора (вместо LALR (1)) помогите? До сих пор я пробовал различные варианты грамматики, однако тот факт, что прецеденты фиксируются для всей грамматики, также мешает обеим интерпретациям косой черты. Другой способ, который я пытался, состоял в том, чтобы устранить двусмысленность в лексере, т. е. по-разному трактовать первый Слэш (в каждой "группе") и возвращать другой символ, например DIV - однако я не мог найти хорошее правило и почему-то сомневался, что оно существует чисто, глядя на лексический структура. Наконец, я испытываю искушение решить эту проблему, полностью отойдя от данного синтаксического анализатора, чтобы облегчить себе жизнь. Считаете ли вы это более желательным в том смысле, что наличие предсказуемой грамматики легче понять?

Пример 1, скрипт Python для изучения дерева синтаксического анализа:

import htsql


application = htsql.HTSQL("sqlite:///htsql_demo.sqlite")


global y
y = None


def p(string):
    global y
    with application:
        y = htsql.core.syn.parse.parse(string)
        return y


def l(name):
    result = []
    for c in name:
        if c.isupper() and result:
            result.append("-")
        result.append(c)
    return "".join(result)


def keyword(name):
    return ":{}".format(name.upper())


def n(expression):
    name = expression.__class__.__name__
    name = name[:name.find("Syntax")]
    return keyword(l(name))


def t(expression):
    arguments = [n(expression)]
    d = expression.__dict__
    if "identifier" in d:
        arguments.append(t(expression.identifier))
    if "text" in d:
        arguments.append(""{}"".format(expression.text))
    if "symbol" in d:
        if not isinstance(expression, (htsql.core.syn.syntax.ProjectSyntax, htsql.core.syn.syntax.FilterSyntax, htsql.core.syn.syntax.CollectSyntax, htsql.core.syn.syntax.DetachSyntax)):
            arguments.append(expression.symbol)
    if "arm" in d:
        arguments.append(t(expression.arm))
    if "larm" in d:
        arguments.append(t(expression.larm))
    if "rarm" in d:
        arguments.append(t(expression.rarm))
    if "arms" in d:
        arguments.extend(t(x) for x in expression.arms)
    if "rarms" in d:
        arguments.extend(t(x) for x in expression.rarms)
    return "({})".format(" ".join(arguments))


# t(p("/school"))
# '(:COLLECT (:IDENTIFIER "school"))

# t(p("/'school'"))
# '(:COLLECT (:STRING "school"))

Пример 2, мой текущий синтаксический анализатор, который не справляется с этим правильно:

(defpackage #:cl-htsql
  (:use #:cl #:alexandria #:cl-lex #:yacc)
  (:import-from #:arnesi #:with-collector))

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defun maybe-intern (name &optional (package NIL package-p))
    "If NAME is a SYMBOL, return it, otherwise INTERN it."
    (cond
      ((symbolp name)
       name)
      (package-p
       (intern name package))
      (T
       (intern name))))

  (defmacro define-lexer (name &body patterns)
    "Shortcut for DEFINE-STRING-LEXER."
    `(define-string-lexer ,name
       ,@(mapcar
          (lambda (pattern)
            (etypecase pattern
              ((or symbol string)
               (let ((symbol (maybe-intern pattern))
                     (pattern (string pattern)))
                 `(,pattern (return (values ',symbol ',symbol)))))
              (list
               (destructuring-bind (pattern &optional symbol value) pattern
                 (let* ((symbol (or symbol (intern pattern)))
                        (value (or value symbol)))
                   (etypecase symbol
                     (list
                      `(,pattern ,symbol))
                     (symbol
                      `(,pattern (return (values ',symbol ',value))))))))))
          patterns))))

;; parser are results are to be treated immutable
(define-lexer string-lexer
  /
  ("\|" |)
  ("\&" &)
  <=
  >=
  ==
  =
  !==
  !=
  !~
  !
  ~
  <
  >
  @
  ("\?" ?)
  ("\." .)
  ("\(" ()
  ("\)" ))
  ("\+" +)
  -
  ("\*" *)
  :
  ("-?0|[1-9][0-9]*(\.[0-9]*)?([eE][+-]?[0-9]+)?"
   (return (cond
             ((find #e $@)
              (values 'float $@))
             ((find #. $@)
              (values 'decimal $@))
             (T
              (values 'integer $@)))))
  ("([^"\.\?~'=<>\(\)@\|\&/:])+" (return (values 'name $@)))
  ("'([^\']|\.)*?'" (return (values 'string (string-trim "'" $@))))
  (""([^\"]|\.)*?"" (return (values 'string (string-trim """ $@)))))

(define-parser *expression-parser*
  (:muffle-conflicts (44 0))
  (:start-symbol query)
  (:terminals (||| #+(or)div & ! |.| ? / = != !== !~ ~ < > == <= >= ( ) + - * @ name integer decimal float string))
  (:precedence ((:left @) (:left ~) (:left |.|) (:left + -) (:left * div) (:left = != == !== ~ !~ < <= > >=) (:left !) (:left &) (:left |||) (:left ?) (:left /)))

  (query
   segment)

  (segment
   (/ segment (lambda (x y) (declare (ignore x)) (if (eq y :skip) '(:skip) `(:collect ,y))))
   skip
   group)

  (skip
   ((constantly :skip)))

  (group
   (( segment ) (lambda (x y z) (declare (ignore x z)) `(:group ,y)))
   sieve)

  (sieve
   (segment ? segment (lambda (x y z) (declare (ignore y)) `(:filter ,x ,z)))
   or)

  (or
   (segment ||| segment (lambda (x y z) `(:operator ,y ,x ,z)))
   and)

  (and
   (segment & segment (lambda (x y z) `(:operator ,y ,x ,z)))
   not)

  (not
   (! segment (lambda (x y) `(:prefix ,x ,y)))
   comparison)

  (comparison
   (segment = segment (lambda (x y z) `(:operator ,y ,x ,z)))
   (segment != segment (lambda (x y z) `(:operator ,y ,x ,z)))
   (segment ~ segment (lambda (x y z) `(:operator ,y ,x ,z)))
   (segment !~ segment (lambda (x y z) `(:operator ,y ,x ,z)))
   (segment == segment (lambda (x y z) `(:operator ,y ,x ,z)))
   (segment !== segment (lambda (x y z) `(:operator ,y ,x ,z)))
   (segment < segment (lambda (x y z) `(:operator ,y ,x ,z)))
   (segment <= segment (lambda (x y z) `(:operator ,y ,x ,z)))
   (segment > segment (lambda (x y z) `(:operator ,y ,x ,z)))
   (segment >= segment (lambda (x y z) `(:operator ,y ,x ,z)))
   addition)

  (addition
   (segment + segment (lambda (x y z) `(:operator ,y ,x ,z)))
   (segment - segment (lambda (x y z) `(:operator ,y ,x ,z)))
   multiplication)

  (multiplication
   (segment * segment (lambda (x y z) `(:operator ,y ,x ,z)))
   (segment / segment (lambda (x y z) (declare (ignore y)) `(:operator / ,x ,z)))
   composition)

  (composition
   (segment |.| segment (lambda (x y z) (declare (ignore y)) `(:compose ,x ,z)))
   attach)

  (attach
   (segment @ segment (lambda (x y z) (declare (ignore y)) `(:attach ,x ,z)))
   detach)

  (detach
   (@ segment (lambda (x y) (declare (ignore x)) `(:detach ,y)))
   term)

  (term
   (name (lambda (x) `(:identifier ,x)))
   (string (lambda (x) `(:string ,x)))
   (number (lambda (x) `(:integer ,x)))
   (integer (lambda (x) `(:integer ,x)))
   (decimal (lambda (x) `(:decimal ,x)))
   (float (lambda (x) `(:float ,x)))))

(defun make-lexer-for-source (source)
  "Make a lexer for the SOURCE, either a STRING or a STREAM."
  (etypecase source
    (string (string-lexer source))
    (stream
     (flet ((ignore (c)
              (declare (ignore c))))
       (stream-lexer #'read-line #'string-lexer #'ignore #'ignore)))))

(defun lex-source (source)
  "Debug helper to lex a SOURCE into a list of tokens."
  (let ((lexer (make-lexer-for-source source)))
    (loop
      for (x y) = (multiple-value-list (funcall lexer))
      while x
      collect (list x y))))

(define-condition htsql-parse-error (simple-error) ())

(defun translate-yacc-error (error)
  (make-condition
   'htsql-parse-error
   :format-control "Couldn't parse HTSQL query: ~A."
   :format-arguments (list error)))

(defun parse-htsql-query (source)
  "Parse SOURCE into a syntax tree.  The SOURCE may be either a STRING or
a STREAM."
  (handler-case
      (parse-with-lexer
       (make-lexer-for-source source)
       *expression-parser*)
    (yacc-parse-error (error)
      (error (translate-yacc-error error)))))

;; > (parse-htsql-query "/1/")
;; (:OPERATOR / (:COLLECT (:INTEGER "1")) :SKIP)
;; > (parse-htsql-query "/1/2")
;; (:OPERATOR / (:COLLECT (:INTEGER "1")) (:INTEGER "2"))
1 4

1 ответ:

Если вы посмотрите на список операторов, вы увидите, что есть еще один случай, когда один и тот же символ используется как двоичный и унарный оператор, с разными прецедентами: унарный минус. Это довольно нормально в языках выражений, и yacc и большинство производных yacc предлагают решение: явные объявления приоритета для продукций. В yacc вы можете написать

%token UNARY_MINUS UNARY_SLASH

%left UNARY_SLASH
%left '-' '+'
%left '/' '*'
%left UNARY_MINUS
%%
expr: '/' expr %prec UNARY_SLASH
    | expr '*' expr
    | expr '/' expr
    | expr '+' expr
    | expr '-' expr
    | '-' expr %prec UNARY_MINUS

Я не знаю, предлагает ли CL-YACC эквивалент; я ничего не нашел в документации.

Вы находитесь под нет обязанность использовать объявления о приоритете, и я даже не уверен, что это хорошая идея. Это только немного сложнее, чтобы просто сказать, что вы имеете в виду в недвусмысленном виде:

term: ID | NUMBER | ... | '(' expr0 ')'
expr0: '/' expr0
     | expr1
expr1: expr1 '+' expr2
     | expr1 '-' expr2
     | expr2
expr2: expr2 '/' expr3
     | expr2 '*' expr3
     | expr3
expr3: '-' expr3
     | term

Вышесказанное является недвусмысленным и вообще не требует объявления приоритета.


(неформальная) грамматика HTSQL говорит, что синтаксис оператора "сегмент" - это / T, а не / expr, но я не вижу никаких указаний на то, что такое T. На мой взгляд, T - это термин или, возможно, lexpr (T := expr), что делает 1 / 2 маловероятным кандидатом. Но, как вы говорите, это неофициально.