Pegjs: Fornece uma maneira concisa de indicar um único valor de retorno de uma sequência

Criado em 29 jan. 2014  ·  21Comentários  ·  Fonte: pegjs/pegjs

Isso é bastante comum para mim. Quero retornar apenas a parte relevante de uma sequência no rótulo de inicialização. Nesse caso, apenas AssignmentExpression.

pattern:Pattern init:(_ "=" _ a:AssignmentExpression {return a})?

Eu recomendo que você adicione @ expression, que retornará apenas a seguinte expressão da sequência. Portanto, o exemplo acima seria assim:

pattern:Pattern init:(_ "=" _ @AssignmentExpression)?

Assuntos relacionados:

  • # 427 Permitir o retorno do resultado da correspondência de uma expressão específica em uma regra sem uma ação
  • # 545 Extensões de sintaxe simples para encurtar gramáticas
feature

Comentários muito úteis

Isso já foi lançado para a tag dev no npm?

https://www.npmjs.com/package/pegjs/v/0.11.0-dev.325

Todos 21 comentários

Eu concordo que este é um problema comum. Mas não tenho certeza se vale a pena resolver. Em outras palavras, não tenho certeza se isso ocorre com frequência suficiente e se causa dor suficiente para garantir a adição de uma complexidade ao PEG.js com a implementação de uma solução, seja ela qual for.

Vou manter este problema aberto e marcá-lo para consideração após 1.0.0.

Concordou. Eu realmente gostaria de ver isso em 1.0. No entanto, não estou tão entusiasmado com a sintaxe @. IMHO, uma ideia melhor seria fazer isso: se houver apenas um único rótulo de "nível superior", retorne implicitamente esse rótulo. Então, em vez de:

rule = space* a:(text space+ otherText)+ newLine* { return a; }

Você obtém:

rule = space* a:(text space+ otherText)+ newLine*

E quando o rótulo não for nada particularmente significativo, também permita isso:

rule = space* :(text space+ otherText)+ newLine*

Portanto, ignore completamente o nome do rótulo.

@mulderr Acho que o operador @ é melhor, já que fazer algo implicitamente significa que, se eu ler o código de outra pessoa que usa esse recurso, terei que pesquisar bastante no Google antes de perceber o que ele fez ali. Por outro lado, usar um operador explícito me permitiria pesquisar a documentação rapidamente.

+1 para @ - Isso se repete muito em todo o meu código.

1 para alguma forma concisa de fazer isso.

Parece que essa dor é semelhante à sintaxe detalhada function em JS, que o ES6 aborda usando as funções de seta . Talvez algo semelhante possa ser usado aqui? Algo como:

rule = (space* a:(text space+ otherText) newLine*) => a

Parece-me que isso é bastante flexível, ainda explícito (preocupação de @wildeyes ) e parece menos com adicionar complexidade, uma vez que tanto na sintaxe quanto na implementação, ele difere para o JS subjacente ...

Tenho imaginado algo um pouco como:

additive = left:multiplicative "+" right:additive {= left + right; }

Onde um = (sinta-se à vontade para debater a escolha do caractere) como o primeiro caractere sem espaço em branco de um bloco é transformado em return .

Isso também funcionaria para expressões completas e deve ser possível com uma passagem de transformação.

Qualquer notícia? Por que não additive = left:multiplicative "+" right:additive { => left + right } ?

Certamente pareceria intuitivo, dado como as funções de seta agora funcionam ( (left, right) => left + right ).

Na verdade, existem alguns exemplos de lugares em seu arquivo parser.pegjs que seriam melhorados por este recurso.

Por exemplo:

  = head:ActionExpression tail:(__ "/" __ ActionExpression)* {
      return tail.length > 0
        ? {
            type: "choice",
            alternatives: buildList(head, tail, 3),
            location: location()
          }
        : head;
    }

É frágil por causa do número mágico 3 na chamada buildList, que não está intuitivamente vinculado à posição de sua ActionExpression na sequência. A própria função buildList é complicada pela combinação de duas operações diferentes. Usando a expressão @ e a sintaxe de propagação es6, isso se torna mais claro:

  = head:ActionExpression tail:(__ "/" __ @ActionExpression)* {
      return tail.length > 0
        ? {
            type: "choice",
            alternatives: [head, ...tail],
            location: location()
          }
        : head;
    }

Uma vez que isso é basicamente um açúcar sintático, fui capaz de adicionar esse recurso ao seu analisador apenas modificando a ActionExpression em parser.pegjs

  = ExtractSequenceExpression
  / expression:SequenceExpression code:(__ CodeBlock)? {
      return code !== null
        ? {
            type: "action",
            expression: expression,
            code: code[1],
            location: location()
          }
        : expression;
    }

ExtractExpression
  = "@" __ expression:PrefixedExpression {
      return {
        type: "labeled",
        label: "value",
        expression: expression,
        location: location()
      };
    }

ExtractSequenceExpression
  = head:(__ PrefixedExpression)* _ extract:ExtractExpression tail:(__ PrefixedExpression)* {
      return {
        type: "action",
        expression: {
          type: "sequence",
          elements: extractList(head, 1).concat(extract, extractList(tail, 1)),
          location: location()
        },
        code: "return value;",
        location: location()
      }
    }

Eu verifiquei uma essência que mostra como o parser.pegjs seria simplificado usando a notação @.

As funções extractOptional, extractList e buildList foram completamente removidas, uma vez que a notação @ torna trivial extrair os valores desejados de uma sequência.

https://gist.github.com/krisnye/a6c2aac94ffc0e222754c52d69e44b83

@krisnye Veja como ficaria se houvesse ainda mais açúcar de sintaxe:

https://github.com/polkovnikov-ph/newpeg/blob/master/parse.np

Estou pensando em usar uma combinação de :: e # para este recurso de sintaxe (veja minha explicação / razão em # 545 ):

  • :: o operador de ligação
  • # o operador de expansão
// this is imported into grammar
class List extends Array {
  constructor() { this.isList = true; }
}

// grammar
number = _ ::value+ _
value = ::int #(_ "," _ ::int)* { return new List(); }
int = $[0-9]+
_ = [ \t]*
  • Na sequência raiz de uma regra, :: retornará o resultado da expressão como o resultado da regra
  • Em uma sequência aninhada, :: retornará o resultado da expressão aninhada como o resultado da sequência
  • Se mais de um :: for usado, o resultado das expressões marcadas será retornado como um array
  • Se # for usado em uma sequência aninhada que contém :: , os resultados serão colocados na matriz do pai
  • Se a regra tiver um bloco de código junto com :: / # , execute-o primeiro e, em seguida, use o método push

Seguindo essas regras, passar 09 , 55, 7 para o analisador gerado do exemplo acima resultaria em:

result = [
    isList: true
    0: "09"
    1: "55"
    2: "7"
]

result instanceof Array # true in ES2015+ enviroments
result instanceof List # true

Na sequência raiz de uma regra, :: retornará o resultado da expressão como o resultado da regra
Em uma sequência aninhada, :: retornará o resultado da expressão aninhada como o resultado da sequência

Por que eles estão separados? Por que não apenas " :: em uma sequência torna o resultado do argumento resultado da sequência"?

Se mais de um :: for usado, o resultado das expressões marcadas será retornado como um array.

É uma má ideia. Prefere resgatar neste caso. Não faz sentido combinar valores de vários tipos em uma matriz. (Isso seria uma tupla, mas eles são praticamente inúteis em JS.)

Se # for usado em uma sequência, os resultados serão Array # concatados na matriz do pai

Essa é uma ideia horrível. Obviamente, isso é um truque para se livrar de { return xs.push(x), xs } estranhos. Criar tal caso especial não faz sentido, pois poderia ser resolvido de forma genérica com regras parametrizadas. Não há muitas sequências de um único caractere para usar pelos operadores e não devemos desperdiçá-las.

Se a regra tiver um bloco de código junto com :: / #, execute-o primeiro e use o método push de resultados

Então f = ::"a" { return "b" } deveria ter ["a", "b"] como resultado?

passando 09. 55: 7 para o analisador gerado do exemplo acima resultaria em:

Não seria, não há . ou : mencionados. Também não vejo como o comportamento descrito produziria o resultado.

número = _ :: valor + _

Além disso, não é assim que _ deve ser usado. Ele vai para a direita ou para a esquerda do token, lado escolhido uma vez para a gramática. Caso esteja à direita, a regra principal também deve começar com _ e vice-versa.

start = _ value
value = ::int #("," _ ::int)* { return new List(); }
number = ::$[0-9]+ _
_ = [ \t]*

Um exemplo de classes implementadas no exemplo de JavaScript (não pretende ser uma implementação real):

ClassMethod
  = head:FunctionHead __ params:FunctionParameters __ body:FunctionBody {
      return {
        type: "method",
        name: head[ 1 ],
        modifiers: head[ 0 ],
        params: params,
        body: body
      };
    }

// `::` inside the zero_or_more "( ... )*" builds an array as we want,
// so this rule returns `[FunctionModifier[], Identifier]` as expected
MethodHead = (::MethodModifier __)* ("function" __)? ::Identifier

// https://github.com/tc39/proposal-class-fields#private-fields
MethodModifier
  = "#"
  / "static"
  / "async"

FunctionParameters
  = "(" __ head:FunctionParam tail:(__ "," __ ::FunctionParam)* __ ")" {
      // due to `::`, tail is `FunctionParam[]` instead of `[__, "", __, FunctionParam][]`
      return [ head ].concat( tail );
    }
    / "(" __ ")" { return []; }

FunctionParam
  = name:Identifier value:(__ "=" __ ::Expression)? {
      return { name, value };
    }

FunctionBody = "{" __ ::SourceElements? __ "}"

Por que eles estão separados? Por que não apenas :: em uma sequência torna o resultado do argumento resultado da sequência "?

Não é a mesma coisa? Eu apenas deixei mais claro para que o resultado de diferentes casos de uso como MethodHead , FunctionParam e FunctionBody possam ser facilmente compreendidos.

Não faz sentido combinar valores de vários tipos em uma matriz. (Isso seria uma tupla, mas eles são praticamente inúteis em JS.)

Ao construir um AST (o resultado mais comum de um analisador gerado), na minha opinião, isso simplificaria casos de uso como MethodHead , em vez de escrever:

MethodHead
  = modifiers:(::MethodModifier __)* ("function" __)? name:Identifier {
      return [ modifiers, name ];
    }

Depois de pensar mais, embora simplifique o caso de uso, também abre a possibilidade de o desenvolvedor cometer um erro (seja na maneira como eles implementam sua gramática, ou como os resultados são tratados por ações), portanto, acho que colocar isso caso de uso por trás de uma opção como multipleSingleReturns (padrão: false ) seria o melhor curso de ação aqui (se este recurso for implementado).

Se # for usado em uma sequência, os resultados serão Array # concatados na matriz do pai

Essa é uma ideia horrível. Isso é obviamente um erro para se livrar do estranho {return xs.push (x), xs}

Também ajuda nos casos de uso mais comuns, como FunctionParameters onde seria melhor escrever:

// should always return `FunctionParam[]`
FunctionParameters
  = "(" __ ::FunctionParam #(__ "," __ ::FunctionParam)* __ ")"
  / "(" __ ")" { return []; }

poderia ser resolvido de forma genérica com regras parametrizadas

Estou pensando que devo implementar valores de retorno único antes das regras parametrizadas, pois ainda não tenho certeza de como proceder com as últimas (eu gosto de usar modelos, mas usar rule < .., .. > = .. parece adicionar muito ruído para a gramática PEG.js, então estou tentando pensar em uma ligeira alteração de sintaxe para que se encaixe melhor), mas isso é um problema separado.

Não há muitas sequências de um único caractere para usar pelos operadores e não devemos desperdiçá-las.

Isso é verdade, mas se não os usarmos quando a ocasião se apresentar assim, será igualmente ruim.

Pensei em usar # para este caso de uso depois de lembrar que ele é usado como um operador de expansão em algumas linguagens que implementam diretivas de pré-processamento, e isso é essencialmente o que este caso de uso está cobrindo aqui.

Então f = ::"a" { return "b" } deve ter ["a", "b"] como resultado?

Não, como o bloco de código é esperado pelo analisador para retornar um objeto semelhante a um array que contém um método push (então f = ::"a" { return [ "b" ] } retornaria [ "b", "a" ] como resultado), portanto, permitindo o envio não apenas para matrizes, mas também para nós customizados que possuem o mesmo método implementado para funcionar de maneira semelhante. Vendo como essa foi sua primeira linha de pensamento depois de ler isso, seria melhor entender se isso estava por trás de uma opção como pushSingleReturns ? Se esta opção for false (padrão), ter um bloco de código após uma sequência que contém valores de retorno simples geraria um erro.

passando 09. 55: 7 para o analisador gerado do exemplo acima resultaria em:

Não seria, não há . ou : mencionados.

Desculpe, esse foi um erro que perdi quando reescrevi o exemplo dado.

Além disso, não é assim que _ deve ser usado. Ele vai para a direita ou para a esquerda do token, lado escolhido uma vez para a gramática. Caso esteja à direita, a regra principal também deve começar com _ e vice-versa.

Eu acho que isso é apenas uma questão de preferência: sorria :, embora eu ache que teria sido mais fácil entender este exemplo:

number = _ ::value+ EOS
...
EOS = !.

Também não vejo como o comportamento descrito produziria o resultado.

Este comentário atualizado ajuda a entender o que estou tentando dizer, e se não, o que você não entende.

Eu concordo com @ polkovnikov-ph que as mudanças oferecidas por lugares são extremamente não óbvias e serão apenas uma fonte de erros adicionais.

Se # for usado em uma sequência, os resultados serão Array # concatados na matriz do pai

O que deve ser retornado na gramática start = #('a') ? Tanto quanto eu entendo, ele pensou como a sintaxe de açúcar para matrizes planas? Não acho que essa operadora seja necessária. Para seu uso mais óbvio - expressões de listas de membros com separadores - a sintaxe especial é melhor fazer (consulte # 30 e minha bifurcação, https://github.com/Mingun/pegjs/commit/db4b2b102982a53dbed1f579477c85c06f8b92e6).

Se a regra tiver um bloco de código junto com :: / #, execute-o primeiro e use o método push de resultados

Comportamento extremamente desagradável. As ações na origem da gramática estão localizadas após a expressão e geralmente são chamadas após a análise da expressão. E de repente, de alguma forma, eles começam a ser chamados antes de serem analisados. Como os rótulos devem se comportar?

Os 3 pontos restantes você conseguiu descrever para que seu sentido claro começasse a escapar. Como anotou @ polkovnikov-ph corretamente, por que estava na descrição para separar o primeiro e o segundo caso? Apenas duas regras simples devem ser executadas:

  1. :: (falando francamente, não gosto da escolha deste caractere, muito barulhento) antes que as expressões levem ao fato de que do sequence nodo elementos marcados com este caractere retornam
  2. Se na sequência apenas um desses elementos, seu resultado é retornado, caso contrário, a matriz de resultados retorna

Exemplos:

start =   'a' 'b'   'c'; // => ['a', 'b', 'c']
start = ::'a' 'b'   'c'; // => 'a'
start = ::'a' 'b' ::'c'; // => ['a', 'c']

O grande exemplo descreve exatamente o que eu esperaria ver de :: e não descreve casos de uso duvidosos ( # , vários :: ).

Não é a mesma coisa?

É isso que venho perguntando. A descrição de forma genérica costuma ser mais útil, pois faz com que o leitor tenha certeza de que é realmente a mesma coisa. Obrigado pelo esclarecimento. :)

na minha opinião, isso simplificaria casos de uso como MethodHead

Mas por que não criar um objeto em vez disso? Existem modifiers: e name: na notação, deixe-os como estão no objeto JS resultante, e isso será legal.

também abre a possibilidade de o desenvolvedor cometer um erro (seja na maneira como eles implementam sua gramática, ou como os resultados são tratados por ações)

Inicialmente, eu ia escrever sobre isso, mas depois decidi que não tenho argumentos difíceis o suficiente. Prefiro não permitir vários :: no mesmo nível de sequência (nem mesmo com um sinalizador).

Também ajuda nos casos de uso mais comuns, como FunctionParameters, onde seria melhor escrever:

Mas é a mesma coisa. Sintaxe realmente boa seria inter(FunctionParam, "," __) , com

inter a b = x:a xs:(b ::a)* { return xs.unshift(x), xs; }

regra <.., ..> = .. parece adicionar muito ruído à gramática PEG.js.

As pessoas esperam que <...> seja usado para tipos, enquanto neste caso os argumentos não são tipos. A melhor maneira é a maneira Haskell sem nenhum caractere extra (veja inter acima). Não tenho certeza de como isso interage na gramática PEG.js com ; omitidos. Pode haver um caso em que f a b = ... seja (parcialmente) incluído na linha anterior.

usado como um operador de expansão em algumas linguagens que implementam diretivas de pré-processamento

Sim, mas prefiro usar de forma inteligente. Em vez da ação push proposta em matrizes, eu a usaria como ação Object.assign em objetos, ou mesmo algo que corresponda a uma string vazia ( eps ), mas retorna seu argumento. Então, por exemplo,

f = type:#"ident" name:$([a-z]i [a-z0-9_]i+)

retornaria {type: "ident", name: "abc"} para a entrada "abc" .

Não, como o bloco de código é esperado pelo analisador para retornar um objeto tipo array que contém um método push (então seria f = :: "a" {return ["b"]} retornando ["b", " a "] como resultado),

O_O

Eu acho que isso é apenas uma questão de preferência

Não só isso, mas também desempenho. Se cada token tiver _ em ambos os lados, as sequências de caracteres de espaço em branco corresponderão apenas aos finais, enquanto _ precedentes não corresponderão a nada. Chamadas extras para parse$_ demoram um pouco mais. Além disso, o código é mais longo porque tem o dobro de _ s.

@Mingun Acho que @futagoza explora o espaço do design. Isso é uma boa coisa a fazer, especialmente se for público e tivermos a chance de discordar :)

A principal coisa a fazer não é perguntar "por que", mas dizer "não! Desse jeito!"

image

f = type:#"ident" name:$([a-z]i [a-z0-9_]i+) retornaria {type: "ident", name: "abc"} para a entrada "abc" .

Por favor, não, haha. Essa sintaxe é _wayyy_ muito mágica.

Acho que o operador @ originalmente proposto é perfeito como está. Muitas e muitas vezes me deparo com o problema de sequência:

sequence
    = first:element rest:(whitespace next:element {return next;})*
    {
        return [first].concat(rest);
    }
    ;

_Tão_ uma dor de digitar repetidamente, especialmente quando eles são mais complexos do que isso.

No entanto, com o operador @ , o acima se torna simplesmente:

sequence = first:element rest:(whitespace @element)* { return [first].concat(rest); };

e com https://github.com/pegjs/pegjs/issues/235#issuecomment -66915879 ou https://github.com/pegjs/pegjs/issues/235#issuecomment -67544080 que fica ainda mais reduzido a:

sequence = first:element rest:(whitespace @element)* => [first].concat(rest);
/* or */
sequence = first:element rest:(whitespace @element)* {=[first].concat(rest)};

... o primeiro do qual eu sou muito parcial.

Isso parece ser uma alteração compatível com versões anteriores que seria simples de conseguir (parece que alguém já fez isso).

Na verdade, se não estou enganado, pode ser um mero solavanco. Pode ser algo para se pensar sobre 0.11.0 @futagoza.

Acabei de adicionar isso ao mestre. Estava planejando usar :: para depenagem múltipla e @ para depenagem única, mas usar :: com rótulos parecia muito feio e confuso, então essa ideia foi suspensa 🙄

Comecei a implementar isso sozinho um tempo atrás, mas desisti até agora (quando deveria estar fazendo # 579 em vez disso 😆) e baseei o gerador de bytecode na implementação de Mingun (https://github.com/Mingun/pegjs/commit/1c1c852bae91868eaa90d9bd9f7e4f722aa6435e )

Você pode tentar aqui: https://pegjs.org/development/try (o editor online, mas usando PEG 0.11.0-dev)

Santo tempo de resposta, batman. Trabalho incrível dev no npm? Adoraria começar a testar com ele.

Para qualquer um que queira experimentar com uma gramática básica, coloque este cachorro lá e dê a ele uma entrada como "abcd" .

foo
    = '"' @$bar '"'
    ;

bar
    = [abcd]*
    ;

Isso já foi lançado para a tag dev no npm?

https://www.npmjs.com/package/pegjs/v/0.11.0-dev.325

Olá, este é outro daqueles problemas que simplesmente desaparecem se tivermos es6, e precisamos desses caracteres para outras coisas que foram adicionadas a es6 desde então. Adicionar operadores para coisas que você já pode fazer é muito contraproducente.

Este tíquete foi mesclado

pattern:Pattern init:(_ "=" _ <strong i="7">@a</strong>:AssignmentExpression)?

A mesma coisa em es6, que todos entenderão inerentemente, e que vem de graça quando as outras partes do analisador forem concluídas, é

pattern:Pattern init:(_ "=" _ a:AssignmentExpression)? => a

De forma problemática, quando testada, esta implementação de arrancar parece estar cheia de bugs e, claro, está marcada como fechada porque foi corrigida em um branch que nunca será lançado

Reabra este problema,

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