Как стать автором
Обновить

ISPA Parser Generator

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров296

Что это

Не будем разбирать что такое парсер, но в целом это код, который разбирает ваш текст на структуру из массивов и обьектов (ключ-значение) или на классы с наследованием. Соответственно я создаю программу, которая генерирует такой код автоматически на основе грамматики (что когда в тексте должно встречаться).

Зачем

Хочеться иметь парсер генератор с максимальной гибкостью да бы в большинстве случаях не пришлось писать парсер вручную. Моя цель - сделать инструмент, который автоматизирует эту работу, сохраняя удобство, мощь и скорость разработки

Преимущества

Этот парсер генератор имеет следующие преимущества (не всё реализовано, но многое уже работает).

  1. Поддержка нескольких видов парсеров (LL(*), LR(1), LALR, LR(*)). Сейчас я работаю над LL(*) парсером, LR и т.д реализованы, прошли тесты, но не готовы для полного использования т.к. требуют доработки.

  2. Поддержка нескольких языков. Пока что только С++, как только все реализую планирую TypeScript и Python.

  3. Полная, автоматическая конструкция AST + возможность определить специфичную для языка структуру (только автоматическая конструкция AST реализована).

  4. CLL (Common Language Logic) - общая логика языка, дает возможность вставлять в парсер семантические действия при этом не делая парсер генерируемым на конкретный язык. Она более абстрактная ну и вообще синтаксически красивее. Сейчас больше реализовано теоретически чем практически (осталось протестировать ну и готово).

  5. Вложенные правила, инкапсуляция. Можно определять правило (или токен) внутри другого правила (или токена). Полностью реализовано.

  6. Встроенная библиотека, шаблоны, наследование. Почему я всё перечислил в одном пункте? Потому-что шаблоны и наследование сделаны только для создания стандартной библиотеки (или даже сторонних библиотек). Суть шаблонов в том, что можно определить имя и место него позже подставить какое-нибуть правило или токен. Что вобщем то присутствует во многих языках. Суть наследвствия в том, что можно видоизменить уже существующее правило. Вспомните наследствие классов, это похожая штука. Таким образом можно спокойно создавать парсинг библиотеки. Пока-что ничто из этого не реализовано.

  7. Context Sensitive Lexing - Работаю над этим. Вообще лексинг использует DFA, и этот генератор тоже, но может генерировать Advanced DFA (я так назвал), что дает возможность добавлять функции или ссылки на другие DFA. То-есть оптимизация на высоком уровне, возможности не ограничены. Сейчас реализованно на где-то 70%, а завтра уже может быть сделано). Видел человек упоминал это в посте Почему я не использую парсер генераторы.

  8. Modifiers - по сути возможность обозначить правило как inline - не создавать для правила отдельную структуру, а все его данные напрямую вставить при ссылке на него или skip - пропускать токен если он встречается. Позже могут быть другие modifiers.

  9. Автоматические токены - много генераторов делают автоматические токены если они встречаются в правиле. Этот тоже.

  10. Возможность добавлять правила восстановления при ошибках и делать кастомные сообщения ошибок (реализован только IR)

  11. Автоматический пропуск пробелов.

Как выглядит "real world" правило

rule:
    @ ID ':' @ #member+ @ #data_block? @ #nested_rule* ';'

    @{ name, rule, data_block, nested_rules }

    #member:

        @(keyvalue | value)? @ name | group | CSEQUENCE | STRING | HEX | BIN | NOSPACE | ESCAPED | DOT | OP | LINEAR_COMMENT | cll @ quantifier?

        @{ prefix, val, quantifier }
    ;
    #name:
        @ '#'? @ ID (DOT @ ID)*

        @{ is_nested, name, nested_name }
    ;
    #group:
        '(' @ member* ')'

        {@}
    ;
    #keyvalue:
        AT (\s0 @ ID)?
        {@}
    ;
    #value:
        '&' @ ID
        {@}
    ;
    #nested_rule: 
        '#' \s0 @ rule
        {@}
    ;
    #data_block:
        @ #templated_datablock | #regular_datablock
        {@}
        #regular_datablock:
            '{' @ cll.expr | #key+ '}'
            {@}
            #key:
                @ ID '=' @ cll.expr
                @{name, dt}
            ;
        ;
        #templated_datablock:
            AT '{' (@ ID (',' @ ID)*)? '}'
            @{ first_name, second_name }
        ;
    ;
    #OP:
        '|'
    ;
    #quantifier:
        @ QUESTION_MARK | PLUS | MULTIPLE
        {@}
    ;

    #CSEQUENCE:
        '[' @ '^'? @ ( #DIAPASON | #ESCAPE | #SYMBOL )* ']'
        @{_not, val}
        #SYMBOL:
            @ '\\' | [^\]]
            {@}
        ;
        #ESCAPE:
            '\\' \s0 @ .
            {@}
        ;
        #DIAPASON:
            ( @ SYMBOL \s0 '-' \s0 @ SYMBOL)
            @{from, to}
        ;
    ;
    #NOSPACE:
        '\\s0'
    ;
    #ESCAPED:
        '\\' \s0 @ . \s0
        {@}
    ;
    #HEX:
        '0x' @[0-9A-Fa-f]+
        {@}
    ;
    #BIN:
        '0b' @[01]+
        {@}
    ;
;

Это и другие правила можно найти здесь. Удивительно, но большую часть занимает СLL.

Это граматика правила или токена.
rule: - обьявление правила rule
@ - обозначение, что это нужно добавить в AST
ID - ссылка на другое правило. По сути это правило должно встречаться в текущем правиле
#member - ссылка на вложенное правило текущего правила. Для вложенных правил вложенного правила другие вложенные правила стают глобальными. Да, звучит сложно. По сути для СSEQUENCE #memb

er уже глобальный и # перед вызовом не требуеться
?, +, \* - quantifiers. ? означает найти 0 или 1 раз, + означает найти 1+ раз, \* означает найти 0+ раз
';' - "здесь должна быть эта строка"
() - группа. К группе также можно применять ?, +, \*
| - найти один из вариантов
Много из этого чистый regex.
\s0 - специальная конструкция означающая не пропускать здесь пробел

Конструкция AST

Часть правила можно сохранить в переменную, которую потом можно задействовать в СLL или для построения финальной структуры. Создания части дерева выглядит так:
@{key1, key2}
сокращение для
{
key1: @
key2: @
}

1. Для сохранения значения можно напрямую определить ключ посредством @Name. Если вы хотите дать имя ключу ниже, то оставляете просто @ (с пробелом обязательно если следующей идет ссылка на другое правило).
2. Для построения дерева (если не дали имя ключу при присвоении значения) используеться конструкция
@{a, b, c} если все что вам нужно - присвоить имена сохраненным значениям
{
a: 1 + b
c: d
}

если вы хотите настроить ключи с выражением СLL.
{<выражение>} - если значение только одно и вам не нужно создавать структуру ключ-значение. Например {@}, или {a + 1}

Подробное обьяснение как определить и использовать кастомную структуру могу описать в следующем посте если вам будет интересно

CLL

Тут небольшой пример, много обьяснять не буду т.к на это лучше выделить отдельный пост

rule:
  $var a = 10;
  $var b = 20;
  $if (a + b == 30) {
    some_rule 
  }
  {
    a: a
    b: b
  }

Это не практичный пример, т.к. СLL в своей грамматике не применял. Но вообще штука очень полезная, особенно для парсинга зависимых от контекста языков как С++.

Шаблоны, унаследования

Планирую сделать что-то типа такого:

json<NUMBER, STRING, ARRAY, OBJECT>:  NUMBER | STRING | ARRAY | OBJECT;

NUMBER: [0-9]+
// ...
// вызов этого правила
my_json = json<NUMBER, STRING, ARRAY, OBJECT>;
some_rule: my_json;

Унаследование смотрите в этом файле (не забудьте переводчик ;) ). Единственное что синтаксис унаследования будет использовать -> а не ':'

Modifiers

[inline]
expr: 
  NUMBER '+' NUMBER
;

[skip]
WHITESPACE: [ ]+;

Восстановление при ошибках

function_body:
  '(' ID %cf (',' ID)* ')'

  fail cf(comma, identifier) {
    if (identifier == ')') {
        rethrow "Trailing comma"
    } else {
        rethrow "Parameter name expected"
    }
  }
;

С помощью % выделяеться место, для которого создаються кастомные ошибки. Далее fail блок. Состоит из fail <имя> (параметры) { инструкции }.
Параметры - это все символы которые находяться в группе которую вы выделили. Если это не группа, то можно обьявить не больше одного параметра. Инструкции это соответственно логика для вывода кастомных ошибок и восстановления

Подсуммирование

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

Если тебе интересно присоединиться к разработке и внести свой вклад — буду рад сотрудничеству. Пиши или открывай issue на GitHub. Собственно github.

Теги:
Хабы:
+3
Комментарии0

Публикации

Ближайшие события