Pegjs: Реализовать параметризуемые правила

Созданный на 25 авг. 2011  ·  29Комментарии  ·  Источник: pegjs/pegjs

Было бы здорово иметь возможность параметризовать правила с помощью переменных;

   = '\"' parse_contents '\"' ->
   / '\'' parse_contents('\'') '\'' ->
   / '+' parse_contents('+') '+' -> /* sure why not :) */

parse_contents(terminator='\"')
    = ('\\' terminator / !terminator .)+ -> return stuff

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

Спасибо за ваше время в объяснении

У меня, вероятно, будет еще один вопрос к вам через десять лет. Хороших 2020-х

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

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

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

Кроме того, в чистой СУХОЙ логике при выполнении таких вещей, как «вещи, ограниченные этим символом с такой escape-последовательностью», лучше вызывать что-то вроде delimited('\'', '\\') , чем просто выполнять правило (и его действия!) три раза. .

Я должен был быть более ясным. Под "конкретным" я искал что-то вроде "Я работал над грамматикой языка X и там есть 5 правил, которые можно было бы объединить в одно, вот они: "То есть я хотел увидеть реальный мир вариант использования и реальный код. Из этого я могу лучше судить, в каких случаях эта функция будет полезна и для скольких людей.

Пожалуйста, не принимайте это, поскольку я против этой функции как таковой. Я просто обычно не хочу реализовывать функции, полезные только для небольшой части языков или разработчиков, из-за сложности и стоимости реализации. И в этом случае стоимость относительно высока.

Просто написав парсер для javascript, я мог бы получить string = delimited_by('\'') / delimited_by('\"') , а затем regexp = delimited_by('/') .

В последнее время я пишу парсер для своего собственного языка. У меня есть что-то подобное в структуре PEG, которую я написал для python:

LeftAssociative(op, subrule): l:subrule rest:(op subrule)* -> a_recursive_function_that_reverses_rest_and_makes_bintrees(l, op, subrule)

И тогда я могу написать:

...
exp_mult = LeftAssociative(/[\*\/%]/, exp_paren)
exp_add = LeftAssociative(/[\+-]/, exp_mult)

Поскольку у меня примерно столько же уровней приоритета, как и в C++ (все его операторы плюс еще несколько), я позволю вам представить, насколько полезным может быть in. Я еще не закончил анализировать выражения, но уже использую его 12 раз.

Это было бы здорово в сочетании с функцией «импорт».

require(CommaSeparatedList.pegjs)
require(StringLiteral.pegjs)
require(IntegerLiteral.pegjs)

...

Function
 = name:FuncName "(" args:CommaSeparatedList(Literal)  ")" 

Hash
= "{"   props:CommaSeparatedList(Prop)   "}"

Prop
= Key ":" Literal

Literal =
  StringLiteral / IntegerLiteral

(Это немного сложнее, чем запрос OP, но он казался слишком близким, чтобы оправдать свой собственный поток.)

Я создаю парсер схемы R5RS с помощью PEG.js. Все радужно, за исключением квазицитатов, которые требуют контекстно-зависимого разбора. Было бы полезно иметь возможность параметризовать правила для генерации правил на лету из шаблонов, избегая большого количества неудобной постобработки. Например, упрощенная грамматика квазицитирования может выглядеть так:

    quasiquotation = qq[1]
    qq[n] = "`" qq_template[n]
    qq_template[0] = expression
    qq_template[n] = simple_datum / list_qq_template[n] / unquotation[n]
    list_qq_template[n] = "(" qq_template[n]* ")" / qq[n+1]
    unquotation[n] = "," qq_template[n-1]

Я заинтересован в том, чтобы внести свой вклад в разработку этой функции, если есть желание добавить ее в инструмент.

Основной причиной для этого будет поддержка грамматик, чувствительных к контексту, которые, если я не ошибаюсь, являются наиболее популярными языками (я точно знаю, что C и python имеют контекстно-зависимые вещи). По словам Тревора Джима, Haskell также не является контекстно-свободным и утверждает, что большинство языков таковыми не являются:

http://trevorjim.com/haskell-is-not-context-free/
http://trevorjim.com/how-to-prove-that-a-programming-language-is-context-free/

Использование внешнего состояния в синтаксическом анализаторе, который может выполнять возврат (например, PEG), опасно и может привести к проблемам, подобным тем, которые можно увидеть в этом синтаксическом анализаторе:

{   var countCs = 0;
}

start = ((x/y) ws*)* { return countCs }

x = "ab" c "d"
y = "a" bc "e"

c = "c" { countCs++; }
bc = "bc" { countCs++; }

ws = " " / "\n"

Приведенное выше возвращает 2 вместо правильного ответа 1. Подобные проблемы могут быть трудными для понимания, могут создавать коварные трудно обнаруживаемые ошибки, а когда они обнаружены, их вообще очень сложно обойти, не говоря уже о том, чтобы сделать это элегантно. . Мне непонятно, как это сделать без постобработки данных, возвращаемых PEG. Если каким-то образом ваш парсер сам нуждается в подсчете, ему просто не повезло.

В настоящее время (опасно) использование внешнего состояния является единственным способом разбора грамматики, чувствительной к контексту. С параметризованными правилами синтаксический анализатор мог бы проанализировать это, не рискуя получить недопустимое состояние:

start = countCs:((x<0>/y<0>) ws*)* { return countCs.reduce(function(a,b){return a+b[0];}, 0); }

x<count> = "ab" theCount:c<count> "d" { return theCount; }
y<count> = "a" theCount:bc<count> "e" { return theCount; }

c<count> = "c" { return count++; }
bc<count> = "bc" { return count++; }

ws = " " / "\n"

Дэвид, вы спросили о реальных ситуациях, и синтаксис отступов с пробелами в python явно является примером здесь. Я хочу сделать аналогичный синтаксис пробельного отступа в Lima (язык программирования, который я делаю с PEG). Но я бы не хотел реализовывать что-то подобное, когда я мог бы непреднамеренно создать недопустимое состояние, которое снесет все к чертям. Я мог бы назвать любую конструкцию синтаксического анализа, требующую контекста, например x* y в языке C (является ли x умноженным на y или y определяется как указатель на значение типа x?).

Обратите внимание, что для того, чтобы грамматики, чувствительные к контексту, можно было анализировать, обязательно необходимо передать информацию, возвращаемую из уже сопоставленных подвыражений, в параметризованное правило, иначе синтаксический анализатор не сможет фактически использовать какой-либо контекст. Вот реальный пример типа строки, который я рассматриваю для Lima, который работает только в том случае, если доступен параметризованный синтаксический анализ и может получить доступ (как переменные) к меткам ранее сопоставленных выражений:

literalStringWithExplicitLength = "string[" n:number ":" characters<n> "]"
number = n:[0-9]* {return parseInt(n.join(''));}
characters<n> = c:. { // base case
  if(n>0) return null; // don't match base case unless n is 0
  else return c;
}
/ c:. cs:characters<n-1> {
  ret c+cs
}

Это позволит проанализировать строку типа string[10:abcdefghij] . Вы не можете сделать это с хорошим чистым PEG.js в его нынешнем виде. Вы сделали что-то ужасное, например:

{ var literalStringLengthLeft=undefined;
}
literalStringWithExplicitLength = "string[" n:specialNumber ":" characters "]"
specialNumber = n:number {
  literalStringLengthLeft = n;
  return n;
}
number = n:[0-9]* {return parseInt(n.join(''));}
characters = c:character cs:characters? {
  return c + cs
}
character = c:. {
  if(literalStringLengthLeft > 0) {
    literalStringLengthLeft--;
    return c;
  } else {
    literalStringLengthLeft = undefined;
    return null; // doesn't match
  }
}

Многие протоколы нуждаются в подобном анализе — например, в пакетах IPv4 есть поле, описывающее его общую длину. Вам нужен этот контекст, чтобы правильно проанализировать остальную часть пакета. То же самое верно для IPv6, UDP и, возможно, любого другого пакетного протокола. Большинству протоколов, использующих TCP, также потребуется что-то подобное, поскольку нужно иметь возможность передавать несколько совершенно отдельных объектов, используя один и тот же концептуальный поток символов.

В любом случае, я надеюсь, что привел несколько хороших примеров и причин, почему я считаю, что это не только приятная функция, не только мощная функция, но и действительно важная функция, которой не хватает многим парсерам (включая, на данный момент, PEG.js). ).

Pegasus (проект, в котором большая часть синтаксиса совпадает с peg.js) решает эту проблему, используя выражение #STATE{} , которое дает возможность изменять словарь состояний. Этот словарь состояния откатывается, когда откатываются правила. Это позволяет ему поддерживать синтаксический анализ значительных пробелов (подробности см. В вики-записи о значительных пробелах ).

Кроме того, возвращая состояние вместе с курсором синтаксического анализа, мемоизацию можно выполнять и для правил с отслеживанием состояния.

Я думаю, что Peg.js может легко сделать то же самое.

Как Pegasus управляет состоянием возврата, когда правила возвращаются? Я могу представить, что вы могли бы сохранить моментальный снимок всего измененного состояния программы и вернуть его обратно, но это было бы дорого. Я мог бы представить себе сохранение моментального снимка только тех переменных, которые изменились, но это потребовало бы либо от пользователя указать его, что усложнило бы создание синтаксических анализаторов, либо потребовало бы, чтобы синтаксический анализатор каким-то образом вычислил все измененное состояние в каком-то фрагменте кода. Ни один из них не кажется идеальным, так как же Pegasus это делает?

Теоретически синтаксический анализатор может избежать недопустимо выполненных действий, если A. действия поставлены в очередь в замыканиях и выполняются только после полного завершения синтаксического анализа, и B. поскольку они выполняются после завершения синтаксического анализа, они не могут отменить соответствие правилу. Возможно, эта схема была бы более оптимальной, чем возврат состояния в pegasus?

Кроме того, исправление проблемы недопустимого состояния действительно очень приятно, но оно не решает проблему выразимости, которую я поднял, связанную со строковым литералом, таким как string[10:abcdefghij], но мне определенно интересно, как это работает

Он не отменяет состояние всей программы. Он поддерживает неизменяемый словарь состояний. Он сохраняет текущий словарь состояния вместе с курсором, и всякий раз, когда курсор возвращается назад, словарь состояния возвращается вместе с ним. Словарь неизменен везде, кроме действий #STATE{} , и КОПИРУЕТСЯ непосредственно перед каждым изменением состояния.

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

В JavaScript нет (насколько мне известно) возможности сделать объект неизменяемым, но это было в основном из соображений безопасности. Peg.js просто нужно будет скопировать словарь состояния перед обработкой каждого блока кода #STATE{} и вернуться к нему всякий раз, когда курсор возвращается.

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

Я только что написал форк, который предоставляет среду, доступную с помощью переменной env : https://github.com/tebbi/pegjs
Это то же самое, что и объект #STATE{} , предложенный выше.
Это быстрый хак, использующий (пакетную) глобальную переменную, которая восстанавливается всякий раз, когда остается функция синтаксического анализа. Копирование env выполняется с помощью Object.create().

Вот пример грамматики, которая использует его для анализа блоков, определенных пробелами, а-ля Python:

{
  env.indLevel = -1
}

block =
  empty
  ind:ws* &{
    if (ind.length <= env.indLevel) return false;
    env.indLevel = ind.length;
    return true;
  }
  first:statement? rest:indStatement*
  {
    if (first) rest.unshift(first);
    return rest;
  }

indStatement =
  "\n" empty ind:ws* &{ return env.indLevel === ind.length; }
  stm:statement
  {return stm; }

statement =
    id:identifier ws* ":" ws* "\n"
    bl:block { return [id, bl]; }
  / identifier

identifier = s:[a-z]* { return s.join(""); }

empty = (ws* "\n")*

ws = [ \t\r]

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

b:
   c
   d:
       e
   f
g

У меня сложилось впечатление, что PEG.js не поддерживает какие-либо параметры в правилах, что удивительно. Эта функция очень важна для меня.

То, что мне нужно, проще, чем запрос ОП - ОП хочет изменить саму грамматику в зависимости от параметра, но как минимум мне нужно просто передать целое число в правило. По сути, я хочу перевести правило LLLPG , которое выглядит следующим образом (где PrefixExpr — это выражение с высоким приоритетом, такое как префиксное выражение, например -x , или идентификатор...):

@[LL(1)]
rule Expr(context::Precedence)::LNode @{
    {prec::Precedence;}
    e:PrefixExpr(context)
    greedy
    (   // Infix operator
        &{context.CanParse(prec=InfixPrecedenceOf(LT($LI)))}
        t:(NormalOp|BQString|Dot|Assignment)
        rhs:Expr(prec)
        { ... }
    |   // Method_calls(with arguments), indexers[with indexes], generic!arguments
        &{context.CanParse(P.Primary)}
        e=FinishPrimaryExpr(e)
    |   // Suffix operator
        ...
    )*
    {return e;}
};
// Helper rule that parses one of the syntactically special primary expressions
@[private] rule FinishPrimaryExpr(e::LNode)::LNode @{
(   // call(function)
    "(" list:ExprList(ref endMarker) ")"
    { ... }
    |   // ! operator (generic #of)
        "!" ...
    |   // Indexer / square brackets
        {var args = (new VList!LNode { e });}
        "[" args=ExprList(args) "]"
        { ... }
    )
    {return e;}
};

В моем языке 25 уровней приоритета, и с помощью этих правил я свернул почти все из них для обработки одним правилом (вы можете думать о Precedence как обертке для пары целых чисел, описывающих приоритет оператор). Более того, мой язык имеет бесконечное количество операторов (в основном любую последовательность знаков препинания), и приоритет оператора определяется после его разбора. Хотя _технически_ можно было бы анализировать язык обычным способом, с отдельным правилом для каждого из 25 видов операторов, это был бы ужасный способ сделать это.

Также вы можете видеть здесь, что внутреннее правило FinishPrimaryExpr строит синтаксическое дерево, которое включает параметр, переданный из включающего правила.

Итак... есть ли способ передать параметры правилу PEG.js?

+1! В моем случае я просто хочу сгенерировать парсер для синтаксиса, где некоторые разделители настраиваются глобально. В этом случае я могу добиться этого, заменив литералы-разделители на выражения match-any в сочетании с предикатом, но было бы намного элегантнее (и эффективнее), если бы match-Everything можно было просто заменить переменной.

+1, есть ли шанс увидеть это реализованным в обозримом будущем?

Другой вариант использования. Это из вашего примера javascript.pegjs :

(...)

RelationalExpression
  = head:ShiftExpression
    tail:(__ RelationalOperator __ ShiftExpression)*
    { return buildBinaryExpression(head, tail); }

RelationalOperator
  = "<="
  / ">="
  / $("<" !"<")
  / $(">" !">")
  / $InstanceofToken
  / $InToken

RelationalExpressionNoIn
  = head:ShiftExpression
    tail:(__ RelationalOperatorNoIn __ ShiftExpression)*
    { return buildBinaryExpression(head, tail); }

RelationalOperatorNoIn
  = "<="
  / ">="
  / $("<" !"<")
  / $(">" !">")
  / $InstanceofToken

(...)

  (...)
  / ForToken __
    "(" __
    init:(ExpressionNoIn __)? ";" __
    test:(Expression __)? ";" __
    update:(Expression __)?
    ")" __
    body:Statement
  (...)

(...)

Все эти правила ...NoIn (а их МНОГО ) необходимы просто из-за оператора for in . Не лучше ли было бы подойти к этому примерно так:

(...)

RelationalExpression<allowIn>
  = head:ShiftExpression
    tail:(__ RelationalOperator<allowIn> __ ShiftExpression)*
    { return buildBinaryExpression(head, tail); }

RelationalOperator<allowIn>
  = "<="
  / ">="
  / $("<" !"<")
  / $(">" !">")
  / $InstanceofToken
  / &{ return allowIn; } InToken
    {return "in";}

(...)

  (...)
  / ForToken __
    "(" __
    init:(Expression<false> __)? ";" __
    test:(Expression<true> __)? ";" __
    update:(Expression<true> __)?
    ")" __
    body:Statement
  (...)

(...)

Это очень похоже на то, как, например, указывается грамматика JavaScript: https://tc39.github.io/ecma262/#prod -IterationStatement (обратите внимание на ~In )

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

Есть ли альтернативный способ добиться этого без дублирования правил?

+1, есть ли шанс увидеть это реализованным в обозримом будущем?

Для этой проблемы назначена веха (после 1.0.0). Текущая версия PEG.js — 0.10.0. Очевидно, что проблемы после 1.0.0 будут решены после выпуска 1.0.0, что произойдет в какой-то момент после выпуска 0.11.0 в соответствии с дорожной картой .

Это должно ответить на ваш вопрос. Лучший способ ускорить весь процесс — помочь с проблемами, предназначенными для 0.11.0 и 1.0.0 .

Есть ли альтернативный способ добиться этого без дублирования правил?

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

Есть два типа аргументов, которые могут быть переданы парсерам:

  1. Ценности. Грамматика для таких языков, как Python, Nim и Haskell (а также Scheme по-другому) зависит от «глубины» выражения внутри дерева. Написание правильной грамматики требует как-то передать этот контекст.
  2. Парсеры. leftAssociative(element, separator) , escapedString(quote) и withPosition(parser) являются хорошими примерами этого.

Должен быть способ как-то отметить, является ли аргумент парсером или значением. Когда я пытался найти правильный подход, я в конечном итоге использовал глобальные переменные для контекста, и это, очевидно, тупик. У кого-нибудь есть идеи по этому поводу?

Как насчет макросов ?

Данный:

Add <Expression, Add>
  = left:Expression _ '+' _ right:Add
    { return { type: 'add', left, right } }
  / Expression

Когда:

  = Add <MyExpression, MyAdd>

MyExpression
  = [0-9]+

Потом:

  = left:MyExpression _ '+' _ right:MyAdd
    { return { type: 'add', left, right } }
  / MyExpression

MyExpression
  = [0-9]+

Это позволяет нам строить правила снизу вверх :smirk:

Согласен, рекомендую разработчикам добавить эту функцию :)

Мне действительно нужна эта функция для обновленной грамматики JavaScript, которую я пишу, так что она занимает первое место в моем списке пожеланий. Попробую и посмотрю, как получится.

@samvv Я наткнулся на это совсем другим путем и еще не прочитал всю ветку.
Однако в #572, на который я ссылался здесь, я показываю технику, с помощью которой вы можете имитировать параметризованные правила.

То есть, по сути: возвращать функции как промежуточные результаты синтаксического анализа.

Этот «трюк» ни в коем случае не является моим изобретением и, вероятно, довольно неуклюжим для ваших целей. Но это может быть обходным путем для вас. Я имею в виду до "post v1.0"... :)

@meisl Круто, спасибо за совет! Попробую, когда найду время.

@samvv Ох, ах ... Боюсь, я упустил из виду кое-что довольно важное:

Имеет большое значение, хотите ли вы, чтобы параметризованное правило

  • только иметь возможность производить значения , которые зависят от параметра
  • или (также) чтобы его решения по синтаксическому анализу зависели от параметра

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

Однако даже для последнего есть обходной путь, хотя и БОЛЕЕ неуклюжий.
И часть «зависимых решений» не имеет ничего общего с возвращаемыми функциями...

Я добавляю пример для вас, чтобы попробовать в https://pegjs.org/online

Основная идея такова: используйте глобальное состояние, чтобы запомнить текущий «терминатор». Это настоящий хак, по общему признанию, и повторяющийся.
Но: добавление еще одного разделителя, скажем, | означало бы просто добавление еще одной альтернативы str :

  / (t:'|' {term = t}) c:conts t:.&{ return isT(t) }  { return c }

который отличается от остальных только тем самым символом |


{
  var term;
  function isT(ch) { return ch === term }
  function isE(ch) { return ch === '\\' }
}
start = str*

str
  = (t:'\"' {term = t}) c:conts t:.&{ return isT(t) }  { return c }
  / (t:'\'' {term = t}) c:conts t:.&{ return isT(t) }  { return c }

conts
  = c:(
        '\\' t:.&{ return isT(t) || isE(t) } { return t }
      /      t:.!{ return isT(t)           } { return t }
    )*
    { return c.join('') }

... на входах

  • "abc" -> ["abc"]
  • "a\"bc" -> ["a\"bc"]
  • "a\\bc" -> ["a\bc"]
  • "a\\b\"c"'a\\b\'' -> ["a\b\"c", "a\b'c"]

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

@ceymard - я понимаю, что это десять лет спустя, но мне любопытно, чем это отличается от № 36.

Вау, мне потребовалось время, чтобы вспомнить. 10 лет !

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

В #36 правила указаны вне самой грамматики. Таким образом, сама грамматика параметризуется.

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

Таким образом, чтобы злоупотреблять терминологией C++, совершенно неправильно, первые являются статикой шаблона, а вторые - вызовами конструктора?

Я предполагаю, что эта аналогия несколько работает, да.

Спасибо за ваше время в объяснении

У меня, вероятно, будет еще один вопрос к вам через десять лет. Хороших 2020-х

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

Была ли эта страница полезной?
0 / 5 - 0 рейтинги