Pegjs: Implemente regras parametrizáveis

Criado em 25 ago. 2011  ·  29Comentários  ·  Fonte: pegjs/pegjs

Seria ótimo poder parametrizar regras com variáveis;

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

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

Comentários muito úteis

Obrigado pelo seu tempo na explicação

Provavelmente terei outra pergunta para você em dez anos. Tenha um bom 2020

Todos 29 comentários

Você tem um caso de uso específico em que isso economizaria uma quantidade significativa de trabalho ou tornaria possível algo atualmente impossível?

Isso torna a análise dos níveis de recuo muito mais fácil chamando regras que têm o nível passado como um argumento.

Além disso, em uma lógica DRY pura, ao fazer coisas como "coisas delimitadas por este caractere com sequência de escape tal", é melhor chamar algo como delimited('\'', '\\') do que apenas fazer a regra (e suas ações!) três vezes .

Eu deveria ter sido mais claro. Por "específico" eu estava procurando algo como "Eu estava trabalhando em uma gramática da linguagem X e existem 5 regras ali que poderiam ter sido combinadas em uma, aqui estão elas:" Ou seja, eu queria ver o mundo real caso de uso e código do mundo real. A partir disso posso julgar melhor em quais casos esse recurso seria útil e para quantas pessoas.

Por favor, não tome isso, pois me oponho a esse recurso em si. Eu geralmente não quero implementar recursos úteis apenas para uma pequena fração de linguagens ou desenvolvedores por causa da complexidade e custo de implementação. E neste caso o custo é relativamente alto.

Apenas escrevendo um analisador para javascript, eu poderia ter string = delimited_by('\'') / delimited_by('\"') e depois regexp = delimited_by('/') .

Ultimamente, tenho escrito um analisador para uma linguagem própria. Eu tenho algo assim em um framework PEG que escrevi para python:

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

E posso então escrever:

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

Como tenho tantos níveis de prioridade quanto em C++ (todos os seus operadores e mais alguns), vou deixar você imaginar o quão útil pode ser. Ainda não terminei as expressões de análise, mas já uso 12 vezes.

Isso seria ótimo se combinado com um recurso de 'importação'

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

(Isso é um pouco mais complicado do que o pedido do OP, mas parecia muito próximo para justificar seu próprio segmento.)

Estou construindo um analisador de esquema R5RS com a ajuda de PEG.js. Tudo é cor-de-rosa, exceto por quase cotações, que exigem análise sensível ao contexto. Seria útil poder parametrizar as regras para gerar regras em tempo real a partir de modelos, evitando uma grande quantidade de pós-processamento complicado. Por exemplo, uma gramática simplificada de quase aspas pode ter a seguinte aparência:

    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]

Estou interessado em contribuir para o desenvolvimento deste recurso caso haja interesse em adicioná-lo à ferramenta.

A principal razão para fazer isso seria suportar gramáticas sensíveis ao contexto, que, se não me engano, são as linguagens mais populares (eu sei com certeza que C e python têm coisas específicas de contexto). De acordo com Trevor Jim, Haskell também não é livre de contexto e afirma que a maioria dos idiomas não é:

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

Usar o estado externo em um analisador que pode retroceder (como o PEG pode) é perigoso e pode produzir problemas como os que podem ser vistos neste analisador:

{   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"

O acima retorna 2 em vez da resposta correta de 1. Problemas como esse podem ser difíceis de raciocinar, podem criar bugs insidiosos difíceis de encontrar e, quando encontrados, podem ser muito difíceis de contornar, muito menos fazê-lo elegantemente . Não está claro para mim como fazer isso sem fazer o pós-processamento de dados retornados pelo PEG. Se de alguma forma o seu próprio analisador precisar da contagem, está simplesmente sem sorte.

Atualmente, (perigosamente) usar o estado externo é a única maneira de analisar a gramática que é sensível ao contexto. Com regras parametrizadas, um analisador pode analisar isso sem arriscar um estado inválido:

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"

David, você pediu situações reais, e a sintaxe de recuo de espaço em branco do python é claramente um exemplo aqui. Eu quero fazer uma sintaxe de recuo de espaço em branco semelhante em Lima (a linguagem de programação que estou fazendo com o PEG). Mas eu não gostaria de implementar nada assim quando eu pudesse inadvertidamente criar um estado inválido que leva tudo para o inferno. Eu poderia nomear qualquer construção de análise que exija contexto, como x* y de C (é x vezes y ou y sendo definido como um ponteiro para um valor de tipo x?).

Observe que para gramáticas sensíveis ao contexto serem analisáveis, seria necessário necessariamente passar informações retornadas de subexpressões já correspondidas em uma regra parametrizada - caso contrário, o analisador não pode realmente usar nenhum contexto. Aqui está um exemplo real de um tipo de string que estou considerando para Lima que só funciona se a análise parametrizada estiver disponível e puder acessar (como variáveis) rótulos de expressões correspondidas anteriormente:

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
}

Isso seria capaz de analisar uma string como string[10:abcdefghij] . Você não pode fazer isso com o PEG.js puro e bom como está. Você fez algo horrível como:

{ 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
  }
}

Muitos protocolos têm esse tipo de necessidade de análise - por exemplo, os pacotes IPv4 têm um campo que descreve seu comprimento total. Você precisa desse contexto para analisar corretamente o restante do pacote. O mesmo vale para IPv6, UDP e provavelmente qualquer outro protocolo baseado em pacotes. A maioria dos protocolos que usam TCP também vai precisar de algo assim, já que é preciso ser capaz de transmitir vários objetos completamente separados usando o mesmo fluxo de caracteres conceituais.

De qualquer forma, espero ter dado alguns bons exemplos e razões pelas quais acho que este não é apenas um bom recurso, não apenas um recurso poderoso, mas realmente um recurso essencial que muitos analisadores estão perdendo (incluindo, no momento, PEG.js ).

Pegasus (um projeto que compartilha a maior parte de sua sintaxe com peg.js) resolve isso com uma expressão #STATE{} que tem a capacidade de alterar um dicionário de estado. Este dicionário de estado é retrocedido quando as regras são retrocedidas. Isso permite que ele suporte a análise de espaço em branco significativo (consulte a entrada do wiki sobre Espaço em branco significativo para obter detalhes).

Além disso, ao retroceder o estado junto com o cursor de análise, a memoização também pode ser realizada para regras com estado.

Peg.js poderia facilmente fazer o mesmo, eu acho.

Como a Pegasus gerencia o estado de retrocesso quando as regras retrocedem? Posso imaginar que você poderia manter um instantâneo de todo o estado do programa que mudou e revertê-lo, mas isso seria caro. Eu poderia imaginar manter um instantâneo apenas das variáveis ​​que foram alteradas, mas isso exigiria que o usuário a especificasse, o que adicionaria complexidade à criação de analisadores, ou exigiria que o analisador descobrisse de alguma forma todo o estado alterado em algum pedaço de código. Nada disso parece ideal, então como Pegasus faz isso?

Teoricamente, o analisador poderia evitar ações executadas de forma inválida se A. ações são enfileiradas em encerramentos e executadas apenas quando o analisador estiver totalmente concluído, e B. porque são executadas após a conclusão do analisador, não podem cancelar uma correspondência de regra. Talvez esse esquema seja mais ideal do que o retrocesso do estado feito em pegasus?

Além disso, corrigir o problema do estado inválido é muito bom, mas não resolve o problema de expressibilidade que trouxe relacionado a uma string literal como string[10:abcdefghij], mas definitivamente estou interessado em como funciona

Não retrocede o estado de todo o programa. Ele mantém um dicionário imutável de estado. Ele salva o dicionário de estado atual junto com o cursor e sempre que o cursor é retrocedido, o dicionário de estado é retrocedido com ele. O dicionário é imutável em qualquer lugar fora das ações #STATE{} e é COPIADO imediatamente antes de cada mudança de estado.

Há uma pequena penalidade de desempenho para definir uma variável extra toda vez que você avança o cursor, mas isso é superado pela capacidade de memorizar regras com estado. Além disso, isso não leva a toneladas de alocação de memória, porque a natureza imutável do dicionário de estado permite que ele seja compartilhado até que seja alterado. Por exemplo, se você não tivesse estado em seu analisador, haveria apenas uma alocação: um único dicionário de estado (vazio).

O JavaScript não (que eu saiba) tem a capacidade de tornar um objeto imutável, mas isso era principalmente um recurso de segurança. O Peg.js precisaria apenas copiar um dicionário de estado antes de processar cada bloco de código #STATE{} e retroceder sempre que o cursor for retrocedido.

Ah, ok, então o usuário basicamente precisa especificar o estado que está mudando. Isso é bem legal. Mas ainda acho que não cobre os mesmos benefícios que a parametrização. Parece que provavelmente é útil por si só para outras coisas.

Acabei de escrever um fork que fornece um ambiente, acessível usando a variável env : https://github.com/tebbi/pegjs
Este é o mesmo que o objeto #STATE{} sugerido acima.
É um hack rápido, usando uma variável (package-)global, que é restaurada sempre que uma função de análise é deixada. A cópia de env é realizada com Object.create().

Aqui está um exemplo de gramática que o usa para analisar blocos definidos por espaços em branco à la 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]

Aqui está um exemplo de entrada para o analisador resultante:

b:
   c
   d:
       e
   f
g

Tenho a impressão de que o PEG.js não suporta parâmetros de nenhum tipo nas regras - o que é surpreendente. Esse recurso é muito importante para mim.

O que eu preciso é mais simples do que o pedido do OP - o OP quer modificar a própria gramática dependendo do parâmetro, mas no mínimo eu só preciso passar um inteiro para uma regra. Basicamente, quero traduzir uma regra LLLPG que se pareça com isso (onde PrefixExpr é uma expressão de alta precedência, como uma expressão de prefixo como -x ou um identificador...):

@[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;}
};

Minha linguagem tem 25 níveis de precedência, e com essas regras eu recolhi quase todas elas para serem processadas por uma única regra (você pode pensar em Precedence como um wrapper em torno de alguns inteiros que descrevem a precedência de um operador). Além disso, minha linguagem tem um número infinito de operadores (basicamente qualquer sequência de pontuação) e a precedência de um operador é decidida depois que ele é analisado. Embora seja _tecnicamente_ possível analisar a linguagem da maneira usual, com uma regra separada para cada um dos 25 tipos de operador, essa seria uma maneira horrível de fazer isso.

Além disso, você pode ver aqui que a regra interna FinishPrimaryExpr constrói uma árvore de sintaxe que incorpora um parâmetro passado da regra de inclusão.

Então... existe alguma maneira de passar parâmetros para uma regra PEG.js?

+1! No meu caso, eu simplesmente quero gerar um analisador para uma sintaxe, onde alguns delimitadores são configuráveis ​​globalmente. Nesse caso, posso conseguir isso substituindo os literais delimitadores por expressões match any combinadas com um predicado, mas seria muito mais elegante (e também mais eficiente) se match-everything pudesse ser simplesmente substituído por uma variável.

+1, alguma chance de ver isso implementado em um futuro próximo?

Outro caso de uso. Isto é do seu exemplo 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
  (...)

(...)

Todas essas regras ...NoIn (e existem muitas delas) são necessárias simplesmente por causa da instrução for in . Não seria uma abordagem muito melhor para isso ser algo como:

(...)

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
  (...)

(...)

Isso parece muito semelhante a como, por exemplo, a gramática JavaScript é especificada: https://tc39.github.io/ecma262/#prod -IterationStatement (observe o ~In )

Uma linguagem que estou desenvolvendo atualmente tem esse problema exato: gostaria de desabilitar/habilitar algumas regras apenas em determinados pontos. Eu gostaria muito de evitar duplicar todas as regras afetadas como você fez para a gramática JavaScript.

Existe alguma maneira alternativa de conseguir isso sem duplicar as regras?

+1, alguma chance de ver isso implementado em um futuro próximo?

Este problema tem um marco atribuído (pós-1.0.0). A versão atual do PEG.js é 0.10.0. Obviamente, os problemas pós-1.0.0 serão resolvidos após o lançamento do 1.0.0, o que acontecerá em algum momento após o lançamento do 0.11.0 de acordo com o roteiro .

Isso deve responder à sua pergunta. A melhor maneira de acelerar todo o processo é ajudar com problemas direcionados para 0.11.0 e 1.0.0 .

Existe alguma maneira alternativa de conseguir isso sem duplicar as regras?

Uma maneira possível é rastrear o estado manualmente e, em seguida, usar predicados semânticos. Mas essa abordagem tem problemas com o retrocesso e eu não a recomendaria (em outras palavras, quando tivesse a opção entre duplicação de regras e rastreamento manual de estado, eu escolheria duplicação).

Existem dois tipos de argumentos que podem ser passados ​​para analisadores:

  1. Valores. Gramáticas para linguagens como Python, Nim e Haskell (e também Scheme de uma maneira diferente) dependem da "profundidade" da expressão dentro da árvore. Escrever uma gramática correta requer de alguma forma passar por esse contexto.
  2. Analisadores. leftAssociative(element, separator) , escapedString(quote) e withPosition(parser) são bons exemplos disso.

Deve haver uma maneira de marcar de alguma forma se o argumento é um analisador ou um valor. Quando tentei descobrir a abordagem correta, acabei usando variáveis ​​globais para contexto, e isso é obviamente um beco sem saída. Alguém tem alguma ideia sobre isso?

Que tal macros ?

Dado:

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

Quando:

  = Add <MyExpression, MyAdd>

MyExpression
  = [0-9]+

Então:

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

MyExpression
  = [0-9]+

Isso nos permite construir regras de baixo para cima :smirk:

Concordo, recomendo que os desenvolvedores adicionem esse recurso :)

Eu realmente preciso desse recurso para uma gramática JavaScript atualizada que estou escrevendo, então isso está no topo da minha lista de desejos. Vai dar uma chance e ver como funciona.

@samvv Eu me deparei com isso de uma rota muito diferente e ainda não li todo o tópico.
No entanto, no #572, do qual me referi aqui, estou mostrando uma técnica com a qual você pode simular regras parametrizadas.

Ou seja, em essência: funções de retorno como resultados intermediários de análise.

Esse "truque" não é de forma alguma minha invenção, e provavelmente bastante desajeitado para o seu propósito, eu acho. Mas pode ser uma solução para você. Quero dizer até "post v1.0"... :)

@meisl Legal, obrigada pela dica! Vou experimentá-lo quando eu encontrar algum tempo.

@samvv Ooh, ah... Receio ter esquecido algo bastante importante:

Faz muita diferença se você deseja que a regra parametrizada

  • só pode produzir valores , que dependem do parâmetro
  • ou (também) ter suas decisões de análise dependentes do parâmetro

O que eu estava propondo apenas ajuda com o primeiro - enquanto o último é o problema real do OP ...
Desculpe por isso.

No entanto, existe uma solução alternativa mesmo para o último, embora ainda mais desajeitado.
E a parte das "decisões dependentes" não tem nada a ver com o retorno de funções ...

Estou anexando um exemplo para você experimentar em https://pegjs.org/online

A ideia básica é: use o estado global para lembrar o "terminador" atual. Isso é bastante hack, reconhecidamente, e repetitivo.
Mas: adicionar mais um delimitador, digamos | significaria apenas adicionar mais uma alternativa a str :

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

que difere dos outros apenas nesse mesmo caractere |


{
  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('') }

... nas entradas

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

ps: isso realmente NÃO é algo que alguém gostaria de escrever à mão, eu sei. Mas ei, imagine que seria gerado para você... Acho que em princípio é assim .

@ceymard - Percebo que são dez anos depois, mas estou curioso para saber como isso é diferente do #36

Uau, demorei um pouco para lembrar. 10 anos!

Neste PR, as regras recebem argumentos e podem ser parametrizadas. Isso deve ser usado pela própria gramática para evitar a repetição de regras semelhantes, mas diferentes.

Em #36, as regras são especificadas fora da própria gramática. A própria gramática é assim parametrizada.

Eu acho que o escopo é diferente, embora se possa argumentar que uma gramática é em si uma regra e, portanto, esta é a mesma questão. Eu acho que não é, pois o número 36 provavelmente significaria algumas pequenas mudanças na API, enquanto este PR não.

Então, para abusar da terminologia C++ ish de uma maneira profundamente incorreta, as primeiras são estáticas de modelo, enquanto as últimas são chamadas de construtor?

Acho que essa analogia funciona um pouco, sim.

Obrigado pelo seu tempo na explicação

Provavelmente terei outra pergunta para você em dez anos. Tenha um bom 2020

Seria realmente útil para remover a redundância da minha definição de analisador. Eu tenho uma gramática personalizada que é de propósito muito relaxada, e algumas regras precisam ser aplicadas em contextos ligeiramente diferentes.

Esta página foi útil?
0 / 5 - 0 avaliações