Pegjs: Поддержка синтаксического анализа языков на основе отступов

Созданный на 16 окт. 2013  ·  34Комментарии  ·  Источник: pegjs/pegjs

Я использовал несколько языков на основе отступов, таких как CoffeeScript, Jade, и хочу сам создавать DSL.
Я нахожу некоторые уловки с сохранением отступов в pegjs путем поиска, мне было интересно, есть ли согласованное решение:
http://stackoverflow.com/questions/11659095/parse-indentation-level-with-peg-js
http://stackoverflow.com/questions/4205442/peg-for-python-style-indentation
https://gist.github.com/jakubkulhan/3192844
https://groups.google.com/forum/#!searchin/pegjs/indent/pegjs/RkbAB4rPlfU/xxafrY5wGCEJ
Но будет ли Pegjs поддерживать эту функцию?

Самый полезный комментарий

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

Это то, что я делаю сейчас, и я рекомендую вам сделать это: предварительно обработать входной файл и вставить свои собственные токены отступа / отступа. Я использую соответственно {{{{и}}}}. Тогда ваша грамматика не зависит от контекста и может быть проанализирована обычным образом. Это может испортить ваши значения строки / столбца, но вы можете исправить это в постпроцессоре.

Все 34 Комментарий

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

Это то, что я делаю сейчас, и я рекомендую вам сделать это: предварительно обработать входной файл и вставить свои собственные токены отступа / отступа. Я использую соответственно {{{{и}}}}. Тогда ваша грамматика не зависит от контекста и может быть проанализирована обычным образом. Это может испортить ваши значения строки / столбца, но вы можете исправить это в постпроцессоре.

Если вам не нужно настраивать таргетинг на javascript, Pegasus , мой клон pegjs для C #, поддерживает состояние push / popping. Вот статья вики о том, как делать именно то, что вы хотите: https://github.com/otac0n/Pegasus/wiki/Significant-Whitespace-Parsing

Я хотел бы предложить pegjs использовать мой синтаксис в качестве отправной точки для синтаксического анализа на основе состояния.

Возможность безопасно нажимать и открывать состояние - это приятно. Я бы использовал это, если бы он был основан на Javascript. Просто не стоит интегрировать CLR только для разбора.

Я так и подумал. Я думаю, что в этом случае мне, вероятно, следует попытаться перенести мои улучшения в pegjs.

Однако я не обязательно хочу делать это без разговора с @dmajda.

@ otac0n Приятно. Я не пишу на C #. JavaScript для меня намного лучше.

Языки, основанные на отступах, важны. Я хочу посмотреть на упрощение их синтаксического анализа после 1.0.0.

Я думаю, что эту проблему лучше всего решить, разрешив состояние в целом, как это делает Pegasus и как предложено в # 285. Вот идея (ниже приводится значимая грамматика пробелов Pegasus, переведенная на pegjs и с добавленной моей идеей синтаксиса):

{var indentation = 0}

program
  = s:statements eof { return s }

statements
  = line+

line
  = INDENTATION s:statement { return s }

statement
  = s:simpleStatement eol { return s }
  / "if" _ n:name _? ":" eol INDENT !"bar " s:statements UNDENT {
      return { condition: n, statements: s }
    }
  / "def" _ n:name _? ":" eol INDENT s:statements UNDENT {
      return { name: n, statements: s }
    }

simpleStatement
  = a:name _? "=" _? b:name { return { lValue: a, expression: b } }

name
  = [a-zA-Z] [a-zA-Z0-9]* { return text() }

_ = [ \t]+

eol = _? comment? ("\r\n" / "\n\r" / "\r" / "\n" / eof)

comment = "//" [^\r\n]*

eof = !.

INDENTATION
  = spaces:" "* &{ return spaces.length == indentation }

INDENT
  = #STATE{indentation}{ indentation += 4 }

UNDENT
  = #STATE{indentation}{ indentation -= 4 }

Обратите внимание на блоки #STATE{indentation} внизу (очевидно, вдохновленные Пегасом). Я называю это государственными блоками. Идея состоит в том, чтобы разрешить блокировку состояний перед действиями. Вот более сложный блок состояний:

#STATE{a, b, arr: {arr.slice()}, obj: {shallowCopy(obj)}, c}

Это сокращение для:

#STATE{a: {a}, b: {b}, arr: {arr.slice()}, obj: {shallowCopy(obj)}, c: {c}}

Другими словами, после применения сокращенного расширения содержимое блока состояния представляет собой список identifier ":" "{" code "}" . Добавление блока состояния перед действием сообщает pegjs, что это действие изменит перечисленные идентификаторы, и, если правило выполняется с возвратом, эти идентификаторы должны быть сброшены на код между фигурными скобками.

Вот скомпилированные функции для INDENT и UNDENT из приведенной выше грамматики с добавлением сброса переменной indentation :

    function peg$parseINDENT() {
      var s0, s1, t0;

      s0 = peg$currPos;
      t0 = indentation;
      s1 = [];
      if (s1 !== peg$FAILED) {
        peg$reportedPos = s0;
        s1 = peg$c41();
      } else {
        indentation = t0;
      }
      s0 = s1;

      return s0;
    }

    function peg$parseUNDENT() {
      var s0, s1, t0;

      s0 = peg$currPos;
      t0 = indentation;
      s1 = [];
      if (s1 !== peg$FAILED) {
        peg$reportedPos = s0;
        s1 = peg$c42();
      } else {
        indentation = t0;
      }
      s0 = s1;

      return s0;
    }

А вот немного того, как можно скомпилировать «сложный блок состояний» сверху:

s0 = peg$currPos;
t0 = a;
t1 = b;
t2 = arr.slice();
t3 = shallowCopy(obj);
t4 = c;
// ...
if (s1 !== peg$FAILED) {
  // ...
} else {
  peg$currPos = s0;
  a = t0;
  b = t1;
  arr = t2;
  obj = t3;
  c = t4;
}

Что вы думаете об этой идее:

  • Сообщите pegjs, какие переменные с состоянием будут изменены действием.
  • Предоставьте код, необходимый для хранения этих переменных, если их нужно сбросить. (Включая сокращенный синтаксис для простого случая, когда переменная является примитивным значением.)

А что ты думаешь о синтаксисе?

Изменить: вот предлагаемая грамматика синтаксиса (просто для удовольствия):

diff --git a/src/parser.pegjs b/src/parser.pegjs
index 08f6c4f..09e079f 100644
--- a/src/parser.pegjs
+++ b/src/parser.pegjs
@@ -116,12 +116,31 @@ ChoiceExpression
     }

 ActionExpression
-  = expression:SequenceExpression code:(__ CodeBlock)? {
+  = expression:SequenceExpression code:((__ StateBlock)? __ CodeBlock)? {
       return code !== null
-        ? { type: "action", expression: expression, code: code[1] }
+        ? {
+            type:       "action",
+            expression: expression,
+            code:       code[2],
+            stateVars:  (code[0] !== null ? code[0][1] : [])
+          }
         : expression;
     }

+StateBlock "state block"
+  = "#STATE{" __ first:StateBlockItem rest:(__ "," __ StateBlockItem)* __ "}" {
+      return buildList(first, rest, 3);
+    }
+
+StateBlockItem
+  = varName:Identifier expression:(__ ":" __ CodeBlock)? {
+      return {
+        type:       "stateVar",
+        name:       varName,
+        expression: expression !== null ? expression[3] : varName
+      };
+    }
+
 SequenceExpression
   = first:LabeledExpression rest:(__ LabeledExpression)* {
       return rest.length > 0

Привет, народ,
Чтобы уточнить, правильно ли я считаю, что лучше не использовать PEG.js (с обходными путями, указанными в начале этой проблемы) с языками на основе отступов, пока эта проблема не будет закрыта?
Спасибо.

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

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

Разрешите прояснить ситуацию: в PEG.js возможен парсинг языков на основе отступов. Существуют различные решения, упомянутые выше, и я просто создал еще одно, пытаясь «почувствовать» это (это грамматика простого языка с двумя операторами, одно из которых может содержать подвыполнения с отступом - аналогично, например, if в Python).

Общим для всех решений является то, что им нужно вручную отслеживать состояние отступа (потому что PEG.js не может этого сделать). Это означает, что есть два ограничения:

  1. Вы не можете безопасно скомпилировать грамматику с кешированием (потому что синтаксический анализатор может использовать кешированные результаты вместо выполнения кода манипулирования состоянием).
  2. Вы не можете возвращаться через уровни отступа (потому что в настоящее время нет возможности развернуть состояние при возврате). Другими словами, вы не можете проанализировать язык, в котором есть две допустимые конструкции, неоднозначность которых может быть устранена только после изменения уровня отступа и новой строки.

Ограничение 1 в некоторых случаях может вызвать проблемы с производительностью, но я не думаю, что существует много языков, для которых ограничение 2 было бы проблемой.

Я согласен с этим состоянием до 1.0.0, и я планирую вернуться к этой теме позже. Первым уровнем улучшения может быть устранение ограничения 2 с использованием более явного отслеживания состояния (как предложено выше) или путем предоставления ловушки с возвратом (чтобы можно было правильно развернуть состояние). Второй уровень - избавиться от необходимости вручную отслеживать состояние отступа, предоставив для этого декларативный способ. Это может помочь с ограничением 1.

H, я написал (крошечный, хакерский) патч для PEG.js, который поддерживает правильное отслеживание с возвратом, как я объяснил здесь: https://github.com/pegjs/pegjs/issues/45

извините за шишку 😜

Я как раз собирался создать парсеры CSON и YAML для языка, который разрабатываю, и пока искал способы создать парсер на основе отступов с помощью PEG.js, я придумал простой метод, который:

1) не зависит от состояния push / pop
2) утверждение уровней отступа через код в действиях

Мне пришло в голову, что любое из двух вышеупомянутых решений фактически добавляет проблемы с производительностью сгенерированным синтаксическим анализаторам. Дополнительно на мой взгляд:

1) зависимость от состояния не только добавляет уродливый синтаксис PEG.js, но также может влиять на тип парсеров, которые могут быть сгенерированы, поскольку они должны будут поддерживать передачу состояния на основе действий.
2) иногда добавление некоторого кода в действия приводит к правилу, зависящему от языка, и для некоторых разработчиков это означает, что они не могут использовать плагины для генерации парсеров для других языков, таких как C или PHP, не прибегая к дополнительным плагинам для обработки действий по правилам, которые просто означает более крупную систему сборки для поддержки 1 или 2 изменений.

Через некоторое время я начал создавать свой вариант парсера PEG.js и подумал: почему бы просто не использовать префиксные операторы инкремента («++») и декремента («-») (__ ++ выражение__ и __-- выражение__ ) для обработки результатов выражений соответствия (__expression * __ или __expression + __).

Ниже приводится пример грамматики, основанной на простом языке , основанном на намерениях @dmajda , переписанном для использования нового выражения __ ++ expression__ и __-- expression__ вместо __ & {predicate} __:

Start
  = Statements

Statements
  = Statement*

Statement
  = Indent* statement:(S / I) { return statement; }

S
  = "S" EOS {
      return "S";
    }

I
  = "I" EOL ++Indent statements:Statements --Indent { return statements; }
  / "I" EOS { return []; }

Indent "indent"
  = "\t"
 / !__ "  "

__ "white space"
 = " \t"
 / " "

EOS
  = EOL
  / EOF

EOL
  = "\n"

EOF
  = !.

Гораздо приятнее для глаз, не так ли? Легче понять, как для людей, так и для программного обеспечения.

Как это работает? просто:

1) Indent* сообщает парсеру, что мы хотим 0 или более того, что возвращает Indent
2) ++Indent указывает синтаксическому анализатору увеличить минимальное количество совпадений, необходимых для Indent
3) Теперь каждый раз, когда синтаксический анализатор собирается вернуть совпадения для Indent , он сначала ожидает, что это будет __1 more__ match, чем раньше, в противном случае выдается _peg $ SyntaxError_.
4) --Indent указывает синтаксическому анализатору уменьшить минимальное количество совпадений, необходимых для Indent
5) Теперь каждый раз, когда синтаксический анализатор ищет Indent и возвращает совпадения, которые он ожидает __1 меньше__ совпадений, чем раньше, в противном случае выдается _peg $ SyntaxError_.

Это решение - лучший способ добавить поддержку «Анализ значимых пробелов» без добавления уродливого синтаксиса в грамматики PEG.js или блокировки сторонних генераторов.

Вот измененные правила для добавления поддержки синтаксического анализа в _src / parser.pegjs_:

{
  const OPS_TO_PREFIXED_TYPES = {
    "$": "text",
    "&": "simple_and",
    "!": "simple_not",
    "++": "increment_match",
    "--": "decrement_match"
  };
}

PrefixedOperator
  = "$"
  / "&"
  / "!"
  / "++"
  / "--"

SuffixedOperator
  = "?"
  / "*"
  / "+" !"+"

Правильно ли я предполагаю, что для его поддержки со стороны компилятора / генератора нам придется:

1) добавьте проход компилятора, который гарантирует, что __ ++ выражение__ или __-- выражение__ используются только в __expression * __ или __expression + __, где __expression__ должно иметь типы: выбор, последовательность или правило_ref
2) добавьте проверку на основе кеша в сгенерированный синтаксический анализатор для __expression * __ или __expression + __, который утверждает, что минимально необходимое совпадение выполнено, прежде чем возвращать совпадения
3) необязательно добавить вспомогательный метод для сгенерированных синтаксических анализаторов для реализации, который возвращает количество совпадений, необходимых для данного правила, например. nMatches( name: String ): Number

@futagoza , это чисто и умно. Мне это нравится. Я работаю над синтаксическим анализатором, который обрабатывает состояние, но единственное состояние, которое нам действительно нужно, - это уровни отступа. Я могу использовать эту идею и отдать вам должное. Отслеживание уровня отступа по-прежнему эффективно требует состояния нажатия / выталкивания, и поэтому оно все еще может препятствовать некоторым оптимизациям, но семантика этого очень приятная.

Если вы добавляете операторы в грамматику, я также рекомендую добавить оператор префикса @. Его цель - просто извлечь результат единственного правила из последовательности. Используя это, образец грамматики становится еще чище. Больше никаких тривиальных действий {return x}.

Start
  = Statements

Statements
  = Statement*

Statement
  = Indent* @(S / I)

S
  = "S" EOS {
      return "S";
    }

I
  = "I" EOL ++Indent <strong i="8">@Statements</strong> --Indent
  / "I" EOS { return []; }

Indent "indent"
  = "\t"
 / !__ "  "

__ "white space"
 = " \t"
 / " "

EOS
  = EOL
  / EOF

EOL
  = "\n"

EOF
  = !.

@kodyjking, что ты об этом думаешь?

@futagoza У вас есть вилка / ветка с включенным патчем отступов и небольшой образец грамматики?

Я работаю над этим отступом вилки / ветки

@krinye "Я также рекомендую добавить оператор префикса @ . Его цель - просто извлечь результат одного правила из последовательности"

Не мог бы кто-нибудь из вас взглянуть и сделать комментарий или пиар с исправлением. Спасибо :)

Readme: изменения вилки

Я отлаживал ... increment_match-not-found

Ах, не заметил предостережения:

Правильно ли я предполагаю, что для его поддержки со стороны компилятора / генератора нам придется:

  • добавить проход компилятора, который гарантирует, что выражение ++ или - выражение используются только в выражении * или выражении +, где выражение должно иметь типы: выбор, последовательность или ссылка_правила
  • добавить проверку на основе кеша в сгенерированный синтаксический анализатор для выражения * или выражения +, который утверждает, что минимальное требуемое совпадение выполнено, прежде чем возвращать совпадения
  • необязательно добавить вспомогательный метод для сгенерированных синтаксических анализаторов для реализации, который возвращает количество совпадений, необходимых для данного правила, например. nMatches (имя: Строка): Число

Просто ради удовольствия попробовал добавить это в visitor.js

      increment_match: visitExpression,
      decrement_match: visitExpression,

Теперь я получаю Invalid opcode: undefined.

@kristianmandrup относительно оператора @ для извлечения отдельных значений из последовательностей, у меня есть вилка с этой функцией, добавленной в PegJS, доступную здесь:

https://github.com/krisnye/pegjs

Это довольно простое дополнение.

@krisnye +1 за реализацию на основе грамматики, красиво и просто. Если вы не возражаете, я добавлю это в свой вариант PEG.js 😄

@kristianmandrup увидимся, что вы

@futagoza Пожалуйста, сделайте это.

Я обсуждал логику отступов с коллегой, и мы предлагаем следующие синтаксические элементы

// увеличивает указанную переменную состояния на единицу
идентификатор ++
// уменьшает указанную переменную состояния
идентификатор--

// повторяем постоянное количество или переменную состояния (с нулевым по умолчанию, если переменная еще не увеличена)
правило {целое | идентификатор}
// повторяем мин / макс, используя константы или переменные состояния
правило {целое | идентификатор, целое число | идентификатор}

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

Большое спасибо, ребята! Если это так просто сделать, почему бы не сделать «специальный» форк, в котором все, что вы упомянули, «просто работает»;)
Ваше здоровье!

@krisnye

  1. Использование __identifier ++ __ может легко вызвать неприятную ошибку, если разработчик имел в виду __identifier + __, поэтому я решил использовать __ ++ identifier__ и, для согласованности, __-- identifier__
  2. Как упоминалось в другом вопросе о диапазонах, rule{ STATE_REPEAT / RANGE } можно спутать с rule{ ACTION } , особенно если вы создаете подсветку синтаксиса для PEG.js, поэтому этот подход был отклонен @dmajda

@kristianmandrup _ (OFF TOPIC) _ Я хорошо

@futagoza Спасибо, приятель :)

Кто-то еще из этого списка указал мне на другие решения для построения / генерации парсеров, которые я тоже мог бы изучить. Держи меня в курсе! Ваше здоровье!

@kristianmandrup Это не включено, насколько я могу судить, но @dmajda сказал 3 года назад, что он рассмотрит это после выпуска PEG.js v1, но, насколько я могу судить, этого не будет в течение следующих 2 лет, если он не планирует выпустить дополнительные минорные версии PEG.js v0 (_0.12_, _0.13_, _etc_)

Я имел в виду, если вы уже включили поддержку отступов в ePEG или в дорожную карту?

@kristianmandrup , это в дорожной карте. Я некоторое время не обновлял репозиторий ePEG.js и совсем недавно решил превратить его в полную версию PEG.js вместо плагина.

@futagoza согласился с ++ / - в качестве предварительных операций, и я забыл о синтаксисе действий, мы изменили его на [min, max]

Так

++identifier
--identifier
rule[integer | identifier]
rule[integer | identifier, integer | identifier]

@krisnye [ ... ] используется для классов символов, см. https://github.com/pegjs/pegjs#characters

То, что я делаю в ePEG.js, - это добавление диапазонов (также в дорожной карте) для достижения того, что, как я думаю, вы описываете:

space = [ \t]*
rule = range|expression
  • __expression__ может быть любым из следующих: __ ++ space__, __-- space__ или __space__
  • __range__ может быть __ min.. __, __ min..max __, __ ..max __ или __ exact __
  • __min__, __max__ или __exact__ может быть только целым числом __ без знака__
  • использование __range__ с __expression__ (например, 2 | expression) может позволить нам установить общее количество __expression__, необходимое для успешного синтаксического анализа __rule__.
  • использование диапазона __exact__ с __ ++ выражение__ или __-- выражение__ (например, 3 | ++ выражение) может позволить нам установить количество __integer__ для __ ++ __ или __ - __, которое по умолчанию равно __1__
  • использование диапазона __min__ или __max__ с __ ++ выражение__ или __-- выражение__ вызывает синтаксическую ошибку.

Я не использую переменные состояния, так как это просто сбивает с толку идентификаторы правил.

Используя комбинацию __range__, __ ++ __, __ - __ или __ @__, я надеюсь создать файлы грамматики PEG, которые меньше полагаются на правила __action__ для их результата, что должно увеличить время разработки пробелов (например, отступы на основе, ASCII art и т. д.) языков, поскольку разработчикам и / или разработчикам языка не нужно беспокоиться о попытках подтвердить, было ли проанализировано правильное количество пробелов.
Это также должно позволить разработчикам плагинов создавать генераторы синтаксических анализаторов, которые могут оптимизировать, не опасаясь того, какой у нас язык __action __ (по умолчанию JavaScript, но с помощью плагина можно изменить его на CoffeeScript, PHP и т. Д.).

Так что кажется, что сегодня все еще невозможно разобрать Python с помощью PEG.js из коробки, верно?

Если нет, то скоро ли это будет? Есть ли набор задач, необходимых для выполнения этой работы, в которую люди могли бы внести свой вклад?

У меня есть проект, в котором я хотел бы иметь возможность получить Python AST в JS, изменить его, а затем преобразовать обратно в исходный код с тем же форматированием, поэтому мне было бы интересно сделать это, если есть четкое дорожная карта.

@mindjuice Пока нет, планируется публикация 1.0.

Если вы не можете ждать, мы с моим племянником создали парсер, написанный на TypeScript, который использует тот же синтаксис и обрабатывает отступы. Это еще не задокументировано, но довольно просто. Работа еще продолжается, поскольку мы используем его в качестве парсера для нового языкового дизайна.

https://github.com/krisnye/pegs

Вы также можете попробовать другой генератор парсеров с поддержкой, например chevrotain

Пример отступа Python

Я считаю, что очень возможно проанализировать отступы, подобные Python, с помощью PEGjs.
В приведенном ниже примере используется только отступ на основе четырех пробелов, но его можно расширить, чтобы охватить произвольно расположенные табуляции и другие символы пробелов.
Фактически, язык, над которым я работаю, имеет немного более сложную историю отступов, чем Python, и эта грамматика хорошо подходит для него.

{
    let prevIndentCount = 0;
    function print(...s) { console.log(...s); }
}

Indent 'indent'
    = i:("    "+) { 
        let currentIndentCount = i.toString().replace(/,/g, "").length;
        if (currentIndentCount === prevIndentCount + 4) { 
            // DEBUG //
            print("=== Indent ===");
            print("    current:"+currentIndentCount); 
            print("    previous:"+prevIndentCount);
            print("    lineNumber:"+location().start.line); 
            // DEBUG //
            prevIndentCount += 4;
            return "[indent]";
        }
        error("error: expected a 4-space indentation here!")
    } // 4 spaces 

Samedent 'samedent'
    = s:("    "+ / "") &{
        let currentIndentCount = s.toString().replace(/,/g, "").length;
        if (currentIndentCount === prevIndentCount) {
            print("=== Samedent ===");
            return true;
        }
        return false;
    }

Dedent 'dedent'
    = d:("    "+ / "") {
        let currentIndentCount = d.toString().replace(/,/g, "").length;
        if (currentIndentCount < prevIndentCount) {
            // DEBUG //
            print("=== Dedent ===");
            print("    current:"+currentIndentCount); 
            print("    previous:"+prevIndentCount);
            print("    lineNumber:"+location().start.line); 
            // DEBUG //
            prevIndentCount -= 4;
            return "[dedent]";
        }
        error("error: expected a 4-space dedentation here!");
    }

С помощью приведенной выше грамматики вы можете создать правило блока с отступом следующим образом:

FunctionDeclaration 
    = 'function' _ Identifier _ FunctionParameterSection _ ":" _ FunctionBody

FunctionBody
    = Newline Indent FunctionSourceCode (Newline Samedent FunctionSourceCode)* Dedent 
Была ли эта страница полезной?
0 / 5 - 0 рейтинги