Pegjs: Implemente uma maneira mais simples de expressar listas com um separador

Criado em 21 set. 2012  ·  25Comentários  ·  Fonte: pegjs/pegjs

Quando temos uma recursão à direita, temos que fazer algo assim:

Statements
  = head:Statement tail:(__ Statement)* {
      var result = [head];
      for (var i = 0; i < tail.length; i++) {
        result.push(tail[i][1]);
      }
      return result;
    }

Eu poderia achar útil poder fazer o mesmo da seguinte forma:

Statements
  = st:Statement (__ st:Statement)* { return st; /*where st is an array*/ }
feature

Todos 25 comentários

Relacionado: #69

Não se trata realmente de um auxiliar de recursão à direita, mas de listas de itens separados por um separador. Este é um padrão bastante comum em gramáticas de linguagem e, como tal, provavelmente merece uma simplificação. A questão é como fazê-lo.

Não gosto da solução proposta na descrição do problema. Isso é apenas magia negra e não está de acordo com as regras de escopo do rótulo, conforme descrito no nº 69. Em vez disso, atualmente estou pensando nas duas soluções a seguir:

Sintaxe especial

Imagine algo assim:

Args = args:Arg % ","

O significado seria "uma lista de Arg s separados por "," s. A variável args conteria uma matriz de tudo o que Arg produz, os separadores seriam ficar esquecido.

Uma questão é como distinguir listas separadas que permitem zero ou mais itens daquelas que permitem um ou mais itens. A experiência mostra que ambos são necessários. Uma resposta possível é anexar ou preceder * ou + ao operador % :

Args0 = args:Arg %* ","
Args1 = args:Arg %+ ","
Args0 = args:Arg *% ","
Args1 = args:Arg +% ","

O operador % poderia até ser implementado como um modificador dos operadores * e + existentes:

Args0 = args:Arg* % ","
Args1 = args:Arg+ % ","

Prós: Simples de implementar, não introduz novos conceitos.
Contras: Solução específica para um problema específico, não genérico.

Regras paramétricas

A segunda solução é usar regras paramétricas, já propostas em #45. Minha ideia atual sobre a sintaxe:

// Template definition
List<item, separator> = head:item tail:(separator item)* { ... boilerplate ... }

// Template use
Args = List<Arg, ",">

Dessa forma, o código clichê seria repetido no máximo uma vez na gramática. O problema com dois tipos de listas pode ser resolvido por dois modelos. Esses modelos podem até ser incorporados.

Prós: Genérico, pode eliminar outros tipos de clichê também.
Contras: Complexo de implementar, introduz um novo conceito.


Ainda não decidi qual caminho seguir. Eu adoraria ouvir quaisquer pensamentos/sugestões/propostas alternativas.

A definição de modelo parece ser o caminho a seguir para isso, especialmente se houver alguns modelos internos (opcionais), como list.

Eu tenho um projeto irmão para peg.js (otac0n/pegasus, é basicamente uma porta para C#) que já usa colchetes nessa posição para o tipo de dados da regra, mas parece que eu poderia descobrir algo se você fosse com isso.

Apenas meus dois centavos:

Sintaxe especial

Essa sintaxe também deve ser capaz de distinguir listas que permitem dois ou mais itens. É um padrão bastante comum que, quando a lista tem dois ou mais itens, você deseja envolvê-la dentro de um nó de contêiner, enquanto ela tem apenas um item, você apenas retorna esse item.

Além disso, o separador nem sempre pode ser descartado:

complexSelector = simpleSelectors:simpleSelector % (ws* [>+~] ws* / ws+)

Este é um exemplo de seletor de CSS, no qual você provavelmente deseja saber quais combinadores são usados.

Regras paramétricas

Eu gosto dessa ideia, mas parece que o que o template oferece ainda é bem limitado: apenas expressões podem ser parametrizadas. Se você quiser apenas trocar * por + , você deve usar um modelo diferente. É claro que você pode aninhar modelos como este:

AbstractList<head, tail> = head:head tail:tail { tail.unshift(head); return tail;  }
List<item, separator> = AbstractList<item, (separator item)*>
List2<item, separator> = AbstractList<item, (separator item)+>

mas:

  • modelos de nomenclatura é difícil

    • o nome List2 diz pouco sobre sua característica, e com sua definição sendo abstraída, a situação se agrava

    • você certamente não quer usar ListWithTwoOrMoreItems

  • é realmente melhor do que:

{ var list = function(head, tail) { tail.unshift(head); return tail; } } args = head:arg tail:(',' a:arg {return a})* { return list(head, tail) } args2 = head:arg tail:(',' a:arg {return a})+ { return list(head, tail) }

Pessoalmente, acho isso mais explícito e, portanto, mais legível.

  • mesmo que as expressões possam ser parametrizadas, a ação provavelmente assumirá que elas possuem certas propriedades. Por exemplo, se head for uma matriz, AbstractList falhará. Portanto, é provável que, embora as expressões em um modelo correspondam à regra atual, você não a use.

O verdadeiro problema

O que esse problema realmente trata é que os usuários desejam que o pegjs mescle valores de expressões para eles, para que não precisem fazer isso manualmente em ações.

Eu estou querendo saber se os rótulos são reutilizados assim, torna óbvio que os usuários querem que certos valores sejam mesclados?

args = args:arg args:(',' a:arg {return a})* { // args is an array of "arg"s }
args2 = args:arg args:((',' / ';') a:arg)* { //args is an array of "arg"s and separators "," or ";"

Observe que a segunda regra nivela o segundo valor de args

A primeira regra implica que o pegjs precisa testar os tipos de valores

items = items:item1 items:item2

Se nenhum de item1 e item2 for uma matriz, items será [item1, item2] , caso contrário, items será a concatenação dos dois.

A segunda regra, no entanto, implica em algum comportamento estranho que pode precisar ser alterado.

items = items:item1 items:item2

Se item1 e item2 for uma matriz de matrizes, ela precisa ser achatada, mas permaneça como está quando seu rótulo for exclusivo

items = items:item1 other:item2

Mas como quando os usuários usam um rótulo para duas expressões, é provável que suas mentes já tenham ativado o "modo de mesclagem", portanto, pode ser menos confuso do que parece.

marca curva, não concordo com seus contras com marcadores para regras paramétricas. Parece que seus dois primeiros marcadores derivam da ideia de que a modularidade e a abstração de alguma forma tornam as coisas mais difíceis de ler ou entender. Isso é claramente falso, como mostraram 50 anos de ciência da computação. As abstrações são a raiz de todo poder na programação

Se você tiver problemas para nomear variáveis, isso não é culpa da linguagem. Esperar que alguém leia as partes internas de sua regra para entender o que suas variáveis ​​mal nomeadas fazem não é uma solução, é uma brincadeira. Eu diria que você certamente quer nomes como "ListWithTwoOrMoreItems" - ele documenta seu código, o que significa que você não precisa escrever um comentário dizendo o que "List2" significa.

"é realmente melhor do que ..." - sim, é muito mais limpo, mais fácil de ler e mais fácil de manter. A situação fica ainda mais clara com regras ainda _levemente_ mais complicadas

David, não entendo a necessidade de sintaxe especial aqui. Este:
Args = args:Arg % ","
poderia ser feito assim:
Args = args:(Arg ",")* { return args.map(function(v){v[0]}) }

Map (e, claro, reduzir também) é uma função super útil que usei em alguns lugares ao usar PEG.js . Sim, é um pouco mais longo do que a sintaxe especial, mas A. É muito mais flexível, e B. não exige que ninguém aprenda a nova sintaxe PEG. Certamente não há necessidade de criar loops curtos e feios para um comportamento como esse.

Parece que seus dois primeiros marcadores derivam da ideia de que a modularidade e a abstração de alguma forma tornam as coisas mais difíceis de ler ou entender.

não, não foi isso que eu quis dizer. O PEG já contém um fantástico mecanismo de abstração chamado regras

    = number operator number

neste caso, tanto number quanto operator são abstrações, e eu certamente adoro isso.

A sintaxe do template, por sua vez, tenta abstrair essa regra parametrizando expressões. Mas lembre-se, PEG é uma linguagem declarativa, a parte gramatical não tem conhecimento da natureza das expressões e a parte gramatical não permite sintaxe condicional. Parametrizar significa simplesmente substituir aqui. Comparando com as regras, isso dificilmente abstrai qualquer coisa.

Se o objetivo é reutilizar a estrutura da regra, você também pode escrever uma regra mais geral e reprová-la na ação.

A situação fica ainda mais clara com regras ainda um pouco mais complicadas

Você poderia fornecer alguns casos de uso do mundo real em que a sintaxe do modelo pode ser útil, exceto para listas com separadores ou strings com aspas diferentes, que lidam essencialmente com mesclagem de expressões e devem ter uma sintaxe mais direcionada?

David, não entendo a necessidade de sintaxe especial aqui.

Args = args:(Arg ",")* é diferente de Args = args:Arg % "," . O primeiro permite que a regra termine com , , o último não.

Parametrizar significa simplesmente substituir aqui.

Você não poderia dizer o mesmo para funções em qualquer linguagem de programação? Tenho certeza de que ambos concordamos que as funções são úteis, então por que não em um analisador?

Você poderia fornecer alguns casos de uso do mundo real

Já escrevi alguns exemplos na edição principal aqui: https://github.com/dmajda/pegjs/issues/45

O primeiro permite que a regra termine com ,

Ah entendi, você tem razão. De qualquer forma, meu ponto ainda permanece que poderia ser feito assim:
Args = first:Arg rest:("," Arg)* { return [first].concat(rest.map(function(v){v[0]})) }

O bom das funções é que, se você está fazendo muito isso, pode criar uma função para ela e derrubá-la para:
Args = first:Arg rest:("," Arg)* { return yourFancyFunction(first,rest) }

E se você tivesse modelos de regras, poderia ficar ainda mais simples: Args = list<Arg,",">

Parece que você está falando de um recurso totalmente diferente.

A sintaxe proposta por David é usar os parâmetros na parte gramatical, e como eu disse, ela simplesmente substitui as coisas. Você não pode testar seu valor, não pode especificar diferentes estruturas gramaticais para diferentes valores de parâmetro.

O que você está sugerindo é usá-los na ação (se entendi corretamente), mas não tenho certeza de como funciona. O primeiro exemplo de "contagem" em #45 não diz de onde vem o valor inicial de count , ou você está apenas assumindo que todos os parâmetros têm um valor padrão 0 ?

Talvez você devesse abrir um novo tópico para isso.

@dmajda , estou pensando em outra sintaxe para resolver o problema de mesclagem, que se comporta muito como a sintaxe $ expression .

A sintaxe $ expression retorna a string correspondente, não importa como a expressão esteja estruturada. Da mesma forma, que tal introduzir uma, digamos, sintaxe # expression (ou qualquer coisa desse tipo), que retorne uma matriz de subexpressão correspondente, não importa como a expressão seja estruturada:

args = #(arg (',' a:arg {return a})*) // args is [arg, arg...]

args = #(arg (',' arg)*) // args is [arg, ',', arg, ',', ...]

No entanto, se você escrever dessa maneira

args = #(arg restArgs) // args is [arg, [',', arg, ',', arg, ...]]

restArgs = #(',' arg)* // restArgs is [',', arg, ',', arg, ...]

Não se comporta exatamente como $ expression

@curvedmark , ele mencionou em um e-mail para mim que sua proposta aqui correspondia ao que entendia ser minha proposta. Mas talvez você esteja certo. Independentemente disso, minha proposta não é "completamente diferente" - é uma generalização do que você acha que ele está propondo. Talvez @dmajda possa se esclarecer. Devo abrir uma nova questão para isso, David?

Você estava certo sobre o meu exemplo de contagem. Atualizei meu comentário para ter (espero) a estrutura correta. Obrigado.

Se valer a pena, eu ficaria feliz por qualquer uma das duas sugestões de David. As regras paramétricas são extremamente poderosas e seriam úteis para muitas coisas, então acho que me inclinaria para essa solução. Embora eu concorde com @otac0n em princípio que seria bom ter abstrações embutidas ou mesmo que as pessoas pudessem compartilhar abstrações modularmente, eu manteria simples e começaria com o recurso de abstração. Você pode resolver esses problemas adicionais no futuro. Apenas fornecer a abstração do modelo seria uma melhoria líquida na concisão e na eliminação da duplicação de código.

O Regexp::Grammars do Perl também faz isso com o operador de módulo:

# a list of one or more "item", separated by "separator", returning an array:
<rule: list>
        <[item]>+ % <separator>

É uma espécie de uso discutível do módulo. O módulo de ponto flutuante define um grupo quociente, um intervalo semi-aberto de reais (o IEEE 754 realmente flutua). É uma analogia desleixada, já que os itens retornados pelo módulo Regexp::Grammar são idênticos apenas a um padrão (e mais importante, pois as strings não são um grupo), mas é próximo o suficiente.

Em vez de torná-lo um operador interno, acabei de criar analisadores que podem ser parametrizados por analisadores.
Metagramática aqui.

@futagoza Existe um ticket para analisadores parametrizados? Acho que havia um, mas não consegui encontrá-lo.

O mais relacionado que encontrei é o nº 45, mas o OP desse problema propõe uma sintaxe diferente (semelhante ao que você implementou em sua metagramática @polkovnikov-ph), mas pretendo usar regras paramétricas (como @dmajda sugerido acima) que usam a sintaxe mais comum de < .. > (modelos, genéricos, etc).

@futagoza A sintaxe realmente não importa. O que quer que você acabe fazendo com esse problema, eu aprecio muito isso.

A escolha do símbolo importa muito, pois isso pode interferir em outros recursos de alta prioridade, como o patch de alcance do @Mingun

Não há uma alternativa particularmente sensata para intervalos, então minha inclinação seria preservar chaves angulares para aqueles

Sendo honesto, sou meio que um noob analisador. Minha solução para isso parece simples e estou um pouco preocupado que possa não funcionar em escala, mas por que não fazer algo como

fname = "bob" / "dan" / "charlie"
namelist = (WS? fname ","?)+

Se houver uma necessidade real aqui, eu apoio - as listas estão entre as coisas mais comuns que um analisador precisa fazer, e parece que provavelmente há uma necessidade real aqui, porque vários usuários fortes estão neste tópico e não disseram isso

Portanto, minha solução provavelmente não é boa o suficiente, mas gostaria de saber por que

Portanto, minha solução provavelmente não é boa o suficiente, mas gostaria de saber por que

Principalmente porque aceita algumas entradas, que na vida real devem ser inaceitáveis. Por exemplo, ele analisa bobdan .

Feira. Isso foi ingênuo da minha parte.

Isso permite bob , dan e bob,dan , mas não bobdan . O que eu perdi aqui?

Document = names
WS = [ \r\n]+

fname = "bob" / "dan" / "charlie"

nameitem = (WS? fname ",")+ 
namelastitem = (WS? fname)

namelist = nameitem? namelastitem

@polkovnikov-ph - há também o nº 36, que é semelhante ao nº 45, mas não idêntico

A explicação do autor parece sugerir que 36 está mais próximo do que você quis dizer

Agora ele analisa apenas o que é esperado, mas os resultados não são uma única matriz. Esta é a principal motivação para ter uma construção especial para analisar dados delimitados

Ok, isso está começando a parecer algo que genuinamente deveria ter um operador, como uma facilidade de uso. A maioria dos usuários não terá um especialista russo para fazer perguntas idiotas 😁

A respeito

Document = names

WS = [ \r\n]+

fname = "bob" / "dan" / "charlie"

namelist = nl:(namelast ",")+ { return nl[0][0]; }
namelast = WS? fn:fname       { return fn; }

names = nl:namelist? na:namelast { return [].concat(nl, na); } 

Ou ucraniano ou qualquer outra coisa. Peço desculpas: eu vi um nome cirílico. Eu não deveria adivinhar assim. Entender isso errado pode ser ofensivo.

Funciona, é claro, mas aqui abordamos a questão colocada na manchete -- uma _maneira mais simples_. Dados separados são usados ​​com bastante frequência para ter uma sintaxe separada para eles. Na minha prática este é o segundo lugar para o qual eu uso intervalos, o primeiro são repetições com dados variáveis ​​( peg = len:number someData|len|; na minha sintaxe de intervalo)

E sim, eu sou da Rússia. Não se preocupe, sem ofensa

Ok, isso está começando a parecer algo que genuinamente deveria ter um operador, como uma coisa fácil de usar

aqui abordamos a questão colocada na manchete -- de uma forma mais simples .

Sim, concordo agora. É só que minha primeira tentativa errada parecia muito simples, e se não estivesse errada, teria sido simples o suficiente para não incomodar

mas agora que vejo o que é necessário, concordo

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