(E)BNF Как выполнить сопоставление до следующего нетерминального правила?
Я пытаюсь написать грамматику для контента в формате RIS с помощью nearley
Пример файла:
TY - JOUR
KW - foo
KW - bar
ER -
А *.ris
файл всегда начинается с тега TY
и заканчивается тегом ER
. Между ними может быть много других тегов, напримерKW
(ключевое слово).
В спецификации сказано, что один KW
Оператор может занимать несколько строк.
Итак, это:
TY - JOUR
KW - foo
bar
baz
KW - bat
ER -
Эквивалентно:
TY - JOUR
KW - foo bar baz
KW - bat
ER -
Я изо всех сил пытаюсь придумать грамматику, которая говорила бы что-то вроде:
Ключевое слово начинается с
KW
с последующим-
за которым следует либо:
- буквы до конца строки
- буквы до конца строки и любые другие строки до следующего ключевого слова
Что бы я ни пытался, в конечном итоге все остальные операторы "проглатываются", например, первое многострочное ключевое слово захватывает все остальное после него.
Как бы вы написали это правило? Я не обязательно заинтересован в nearley конкретного ответа. Все, что вызывает у меня момент "Ага", подойдет!
1 ответ
Я определенно не очень хорошо разбираюсь в грамматике (вы, наверное, догадались), но это вызвало у меня Ага- момент:
Многие люди указывали мне, что писать грамматику для Nearley сложно. Дело в том, что писать грамматики вообще очень сложно. Не помогает то, что некоторые проблемы, связанные с грамматикой, доказуемо неразрешимы.
См. https://nearley.js.org/docs/how-to-grammar-good
А также:
Использование токенизатора дает много преимуществ. Это…
- … Часто делает ваш синтаксический анализатор быстрее более чем на порядок.
- … Позволяет писать более чистые и удобные в обслуживании грамматики.
- … В некоторых случаях помогает избежать неоднозначной грамматики. [...]
См. https://nearley.js.org/docs/tokenizers.
Я знаю, что Nearley рекомендует использовать moo-lexer:
nearley поддерживает и рекомендует Moo, сверхбыстрый лексер.
См. https://nearley.js.org/docs/tokenizers.
Итак, я погуглил и нашел на YouTube этот замечательный учебник, который определенно разблокировал меня. Большое спасибо @airportyh!
Сначала я думал, что это был путь слишком сложным для моего случая использования, но оказалось, что с помощью лексера на самом деле сделал вещи возможно и проще!
Для простоты я предоставлю решение с усеченным файлом RIS:
sample.ris
KW - foo
bar
baz
KW - bat
Этот файл должен дать ['foo bar baz', 'bat']
после разбора.
Сначала давайте установим кое-что
yarn add nearley
yarn add moo
Теперь давайте определим наш лексер
lexer.js
const moo = require('moo');
const lexer =
moo.compile
( { NL: {match: /[\n]/, lineBreaks: true}
, KW: 'KW'
, SEPARATOR: " - "
, CONTENT: /[a-z]+/
}
);
module.exports = lexer;
Мы определили четыре токена:
- Символ новой строки
NL
- В
KW
ключевое слово... ключевое слово! - В
SEPARATOR
между тегом и его содержимым - В
CONTENT
тега
Теперь давайте определим нашу грамматику
grammar.ne
@{% const lexer = require('./lexer.js'); %}
@lexer lexer
@builtin "whitespace.ne"
RECORD -> _ KW:+ {% ([, keywords]) => [].concat(...keywords) %}
KW -> %KW %SEPARATOR LINE:+ {% ([,,lines]) => lines.join(' ') %}
LINE -> %CONTENT __ {% ([{value}]) => value %}
Примечание: посмотрите, как мы можем ссылаться на токены, определенные в лексере, с помощью префикса %
!
Теперь нам нужно скомпилировать нашу грамматику
Nearley поставляется с компилятором:
yarn -s nearleyc grammar.ne > grammar.js
Вы также можете определить compile
сценарий в вашем package.json
:
{
...
"scripts": {
"compile": "nearleyc grammar.ne > grammar.js",
}
...
}
Наконец, давайте создадим парсер и воспользуемся им!
const nearley = require('nearley');
const grammar = require('./grammar.js');
module.exports =
str => {
const parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar));
parser.feed(str);
return parser.results[0];
};
Примечание: для этого требуется скомпилированная грамматика, т.е.grammar.js
Добавим в него текст:
const parser = require('./parser.js');
parser(`
KW - foo
bar
baz
KW - bat
`);
//=> [ 'foo bar baz', 'bat' ]
Последний совет: вы также можете проверить свою грамматику с помощью nearley-test
:
cat sample.ris | yarn -s nearley-test -- -q grammar.js