Lark-parser отступы DSL и многострочные строки документации

Я пытаюсь реализовать DSL определения записи, используя lark. Он основан на отступах, что немного усложняет ситуацию.

Жаворонок - отличный инструмент, но я столкнулся с некоторыми трудностями.

Вот фрагмент DSL, который я реализую:

record Order :
    """Order record documentation
    should have arbitrary size"""

    field1 Int
    field2 Datetime:
        """Attributes should also have
        multiline documentation"""

    field3 String "inline documentation also works"

и вот используемая грамматика:

?start: (_NEWLINE | redorddef)*

simple_type: NAME

multiline_doc:  MULTILINE_STRING _NEWLINE
inline_doc: INLINE_STRING

?element_doc:  ":" _NEWLINE _INDENT multiline_doc _DEDENT | inline_doc

attribute_name: NAME
attribute_simple_type: attribute_name simple_type [element_doc] _NEWLINE
attributes: attribute_simple_type+
_recordbody: _NEWLINE _INDENT [multiline_doc] attributes _DEDENT
redorddef: "record" NAME ":" _recordbody



MULTILINE_STRING: /"""([^"\\]*(\\.[^"\\]*)*)"""/
INLINE_STRING: /"([^"\\]*(\\.[^"\\]*)*)"/

_WS_INLINE: (" "|/\t/)+
COMMENT: /#[^\n]*/
_NEWLINE: ( /\r?\n[\t ]*/ | COMMENT )+

%import common.CNAME -> NAME
%import common.INT

%ignore /[\t \f]+/  // WS
%ignore /\\[\t \f]*\r?\n/   // LINE_CONT
%ignore COMMENT
%declare _INDENT _DEDENT

Он отлично работает для документов с многострочными строками для определения записи, отлично работает для определения встроенных атрибутов, но не работает для документа с многострочными строками атрибутов.

Код, который я использую для выполнения, таков:

import sys
import pprint

from pathlib import Path

from lark import Lark, UnexpectedInput
from lark.indenter import Indenter

scheman_data_works = '''
record Order :
        """Order record documentation
        should have arbitrary size"""

        field1 Int
        # field2 Datetime:
        #   """Attributes should also have
        #   multiline documentation"""

        field3 String "inline documentation also works"
'''

scheman_data_wrong = '''
record Order :
        """Order record documentation
        should have arbitrary size"""

        field1 Int
        field2 Datetime:
                """Attributes should also have
                multiline documentation"""

        field3 String "inline documentation also works"
'''
grammar = r'''

?start: (_NEWLINE | redorddef)*

simple_type: NAME

multiline_doc:  MULTILINE_STRING _NEWLINE
inline_doc: INLINE_STRING

?element_doc:  ":" _NEWLINE _INDENT multiline_doc _DEDENT | inline_doc

attribute_name: NAME
attribute_simple_type: attribute_name simple_type [element_doc] _NEWLINE
attributes: attribute_simple_type+
_recordbody: _NEWLINE _INDENT [multiline_doc] attributes _DEDENT
redorddef: "record" NAME ":" _recordbody



MULTILINE_STRING: /"""([^"\\]*(\\.[^"\\]*)*)"""/
INLINE_STRING: /"([^"\\]*(\\.[^"\\]*)*)"/

_WS_INLINE: (" "|/\t/)+
COMMENT: /#[^\n]*/
_NEWLINE: ( /\r?\n[\t ]*/ | COMMENT )+

%import common.CNAME -> NAME
%import common.INT

%ignore /[\t \f]+/  // WS
%ignore /\\[\t \f]*\r?\n/   // LINE_CONT
%ignore COMMENT
%declare _INDENT _DEDENT

'''

class SchemanIndenter(Indenter):
    NL_type = '_NEWLINE'
    OPEN_PAREN_types = ['LPAR', 'LSQB', 'LBRACE']
    CLOSE_PAREN_types = ['RPAR', 'RSQB', 'RBRACE']
    INDENT_type = '_INDENT'
    DEDENT_type = '_DEDENT'
    tab_len = 4

scheman_parser = Lark(grammar, parser='lalr', postlex=SchemanIndenter())
print(scheman_parser.parse(scheman_data_works).pretty())
print("\n\n")
print(scheman_parser.parse(scheman_data_wrong).pretty())

и результат:

redorddef
Order
multiline_doc """Order record documentation
        should have arbitrary size"""
attributes
    attribute_simple_type
    attribute_name    field1
    simple_type       Int
    attribute_simple_type
    attribute_name    field3
    simple_type       String
    inline_doc        "inline documentation also works"




Traceback (most recent call last):
File "schema_parser.py", line 83, in <module>
    print(scheman_parser.parse(scheman_data_wrong).pretty())
File "/Users/branquif/Dropbox/swf_projects/schema-manager/.venv/lib/python3.7/site-packages/lark/lark.py", line 228, in parse
    return self.parser.parse(text)
File "/Users/branquif/Dropbox/swf_projects/schema-manager/.venv/lib/python3.7/site-packages/lark/parser_frontends.py", line 38, in parse
    return self.parser.parse(token_stream, *[sps] if sps is not NotImplemented else [])
File "/Users/branquif/Dropbox/swf_projects/schema-manager/.venv/lib/python3.7/site-packages/lark/parsers/lalr_parser.py", line 68, in parse
    for token in stream:
File "/Users/branquif/Dropbox/swf_projects/schema-manager/.venv/lib/python3.7/site-packages/lark/indenter.py", line 31, in process
    for token in stream:
File "/Users/branquif/Dropbox/swf_projects/schema-manager/.venv/lib/python3.7/site-packages/lark/lexer.py", line 319, in lex
    for x in l.lex(stream, self.root_lexer.newline_types, self.root_lexer.ignore_types):
File "/Users/branquif/Dropbox/swf_projects/schema-manager/.venv/lib/python3.7/site-packages/lark/lexer.py", line 167, in lex
    raise UnexpectedCharacters(stream, line_ctr.char_pos, line_ctr.line, line_ctr.column, state=self.state)
lark.exceptions.UnexpectedCharacters: No terminal defined for 'f' at line 11 col 2

        field3 String "inline documentation also
^

Я понимаю, что грамматика с отступом более сложна, и жаворонок, кажется, упрощает ее, но не может найти здесь ошибку.

PS: я также пробовал pyparsing, но безуспешно с тем же сценарием, и мне было бы трудно перейти на PLY, учитывая объем кода, который, вероятно, потребуется.


person Branquif    schedule 13.02.2019    source источник
comment
Должен ли быть ':' после field2 DateTime?   -  person PaulMcG    schedule 13.02.2019
comment
Да. Я хочу подписать, что после ':' для этого элемента есть многострочная строка документа с отступом. Ответ Эреза сработал для меня.   -  person Branquif    schedule 13.02.2019


Ответы (2)


Ошибка возникает из-за неуместных терминалов _NEWLINE. Как правило, рекомендуется убедиться, что правила сбалансированы с точки зрения их роли в грамматике. Итак, вот как вы должны были определить element_doc:

?element_doc:  ":" _NEWLINE _INDENT multiline_doc _DEDENT
            | inline_doc _NEWLINE

Обратите внимание на добавленную новую строку, которая означает, что независимо от того, какую из двух опций выберет синтаксический анализатор, он заканчивается в похожем состоянии с точки зрения синтаксиса (_DEDENT также соответствует новой строке).

Второе изменение, как следствие первого, заключается в следующем:

attribute_simple_type: attribute_name simple_type (element_doc|_NEWLINE)

Поскольку element_doc уже обрабатывает новые строки, мы не должны пытаться сопоставить его дважды.

person Erez    schedule 13.02.2019
comment
Спасибо Эрез! Не понял, что _DEDENT соответствует новой строке... хороший момент и в правилах балансировки. - person Branquif; 13.02.2019

Вы упомянули попытку pyparsing, иначе я бы оставил ваш вопрос в покое.

Синтаксический анализ с учетом пробелов не очень хорош с pyparsing, но в этом случае он прилагает усилия, используя pyparsing.indentedBlock. Есть некоторый уровень стресса в написании этого, но это можно сделать.

import pyparsing as pp

COLON = pp.Suppress(':')
tpl_quoted_string = pp.QuotedString('"""', multiline=True) | pp.QuotedString("'''", multiline=True)
quoted_string = pp.ungroup(tpl_quoted_string | pp.quotedString().addParseAction(pp.removeQuotes))
RECORD = pp.Keyword("record")
ident = pp.pyparsing_common.identifier()

field_expr = (ident("name")
              + ident("type") + pp.Optional(COLON)
              + pp.Optional(quoted_string)("docstring"))

indent_stack = []
STACK_RESET = pp.Empty()
def reset_indent_stack(s, l, t):
    indent_stack[:] = [pp.col(l, s)]
STACK_RESET.addParseAction(reset_indent_stack)

record_expr = pp.Group(STACK_RESET
                       + RECORD - ident("name") + COLON + pp.Optional(quoted_string)("docstring")
                       + (pp.indentedBlock(field_expr, indent_stack))("fields"))

record_expr.ignore(pp.pythonStyleComment)

Если ваш пример записан в переменную «sample», выполните:

print(record_expr.parseString(sample).dump())

И получить:

[['record', 'Order', 'Order record documentation\n    should have arbitrary size', [['field1', 'Int'], ['field2', 'Datetime', 'Attributes should also have\n        multiline documentation'], ['field3', 'String', 'inline documentation also works']]]]
[0]:
  ['record', 'Order', 'Order record documentation\n    should have arbitrary size', [['field1', 'Int'], ['field2', 'Datetime', 'Attributes should also have\n        multiline documentation'], ['field3', 'String', 'inline documentation also works']]]
  - docstring: 'Order record documentation\n    should have arbitrary size'
  - fields: [['field1', 'Int'], ['field2', 'Datetime', 'Attributes should also have\n        multiline documentation'], ['field3', 'String', 'inline documentation also works']]
    [0]:
      ['field1', 'Int']
      - name: 'field1'
      - type: 'Int'
    [1]:
      ['field2', 'Datetime', 'Attributes should also have\n        multiline documentation']
      - docstring: 'Attributes should also have\n        multiline documentation'
      - name: 'field2'
      - type: 'Datetime'
    [2]:
      ['field3', 'String', 'inline documentation also works']
      - docstring: 'inline documentation also works'
      - name: 'field3'
      - type: 'String'
  - name: 'Order'
person PaulMcG    schedule 13.02.2019
comment
Привет, Пол. Я очень рад, что вы ответили! Я инвестировал в pyparsing, но, как вы подчеркнули, грамматика с отступом не подходит для этого. Пытался манипулировать пробелами, использовал indentedBlock, но безуспешно. Глядя на ваш код, теперь это имеет смысл. Мне, как дилетанту в разборе, далековато разбираться одному. Спасибо за это! - person Branquif; 13.02.2019