Работа с грамматической неоднозначностью при синтаксическом анализе 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"))))'
.
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 ответ:
Если вы посмотрите на список операторов, вы увидите, что есть еще один случай, когда один и тот же символ используется как двоичный и унарный оператор, с разными прецедентами: унарный минус. Это довольно нормально в языках выражений, и 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
маловероятным кандидатом. Но, как вы говорите, это неофициально.