Pegjs: Fournir un moyen concis d'indiquer une valeur de retour unique à partir d'une séquence

Créé le 29 janv. 2014  ·  21Commentaires  ·  Source: pegjs/pegjs

C'est assez courant pour moi. Je souhaite renvoyer uniquement la partie pertinente d'une séquence dans l'étiquette d'initialisation. Dans ce cas, juste le AssignmentExpression.

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

Je vous recommande d'ajouter @ expression qui ne renverra que l'expression suivante de la séquence. L'exemple ci-dessus ressemblerait donc à ceci :

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

Problèmes liés:

  • #427 Autoriser le retour du résultat de correspondance d'une expression spécifique dans une règle sans action
  • #545 Extensions de syntaxe simples pour raccourcir les grammaires
feature

Commentaire le plus utile

Cela a-t-il déjà été publié dans la balise dev sur npm ?

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

Tous les 21 commentaires

Je suis d'accord que c'est un problème courant. Mais je ne sais pas si cela vaut la peine d'être résolu. En d'autres termes, je ne sais pas si cela se produit assez souvent et si cela cause suffisamment de douleur pour justifier l'ajout d'une complexité à PEG.js en implémentant une solution, quelle qu'elle soit.

Je vais garder ce problème ouvert et le marquer pour examen après la version 1.0.0.

D'accord. J'aimerais vraiment voir cela en 1.0. Je ne suis pas si enthousiaste à propos de la syntaxe @ cependant. À mon humble avis, une meilleure idée serait de faire ceci: s'il n'y a qu'une seule étiquette de "niveau supérieur", renvoyez implicitement cette étiquette. Donc au lieu de :

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

Vous obtenez:

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

Et lorsque l'étiquette n'a rien de particulièrement significatif, permettez également ceci :

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

Alors sautez complètement le nom de l'étiquette.

@mulderr Je pense que l'opérateur @ est meilleur, car faire quelque chose signifie implicitement que si je lis le code de quelqu'un d'autre qui utilise cette fonctionnalité, je devrais parcourir pas mal de recherches sur Google avant de réaliser ce qu'il a fait là-bas. En revanche, l'utilisation d'un opérateur explicite me permettrait de rechercher rapidement la documentation.

+1 pour @ - Cela revient beaucoup dans mon code.

+1 pour une manière concise de le faire.

Il semble que cette douleur soit similaire à celle de la syntaxe verbeuse function en JS, que ES6 résout à l'aide des fonctions fléchées . Peut-être que quelque chose de similaire pourrait être utilisé ici ? Quelque chose comme:

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

Il me semble que cela est assez flexible, toujours explicite (à la @wildeyes 's souci), et ressemble moins à ajouter de la complexité car à la fois dans la syntaxe et l'implémentation, il s'en remet au JS sous-jacent ...

J'ai imaginé quelque chose comme :

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

Où un = (n'hésitez pas à débattre du choix du caractère) comme premier caractère non blanc d'un bloc est transformé en un return .

Cela fonctionnerait également pour les expressions complètes et devrait être possible avec une passe de transformation.

Des nouvelles? Pourquoi pas additive = left:multiplicative "+" right:additive { => left + right } ?

Cela semblerait certainement intuitif étant donné le fonctionnement actuel des fonctions fléchées ( (left, right) => left + right ).

Il existe en fait de nombreux exemples d'endroits dans votre fichier parser.pegjs qui seraient améliorés par cette fonctionnalité.

Par exemple:

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

Est fragile à cause du chiffre magique 3 dans l'appel buildList qui n'est pas intuitivement lié à la position de votre ActionExpression dans la séquence. La fonction buildList elle-même est compliquée en combinant deux opérations différentes. En utilisant l'expression @ et la syntaxe de propagation es6, cela devient plus propre :

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

Comme il ne s'agit que de sucre syntaxique, j'ai pu ajouter cette fonctionnalité à votre analyseur simplement en modifiant ActionExpression dans 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()
      }
    }

J'ai vérifié l'essentiel qui montre comment le parser.pegjs serait simplifié en utilisant la notation @.

Les fonctions extractOptional, extractList et buildList ont été complètement supprimées, car la notation @ rend triviale l'extraction des valeurs souhaitées d'une séquence.

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

@krisnye Voici à quoi cela ressemblerait s'il y avait encore plus de sucre syntaxique :

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

Je pense utiliser une combinaison de :: et # pour cette fonctionnalité de syntaxe (voir mon explication/raison dans #545 ):

  • :: l'opérateur de liaison
  • # l'opérateur d'expansion
// 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]*
  • Sur la séquence racine d'une règle, :: renverra le résultat de l'expression comme résultat des règles
  • Dans une séquence imbriquée, :: renverra le résultat de l'expression imbriquée comme le résultat de la séquence
  • Si plus d'un :: est utilisé, le résultat des expressions marquées sera renvoyé sous forme de tableau
  • Si # est utilisé sur une séquence imbriquée qui contient :: , les résultats seront poussés dans le tableau du parent
  • Si la règle a un bloc de code avec :: / # , exécutez-la d'abord, puis utilisez la méthode des résultats push

En suivant ces règles, passer 09 , 55, 7 à l'analyseur généré par l'exemple ci-dessus donnerait :

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

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

Sur la séquence racine d'une règle, :: renverra le résultat de l'expression comme résultat des règles
Dans une séquence imbriquée, :: renverra le résultat de l'expression imbriquée comme résultat de la séquence

Pourquoi sont-ils séparés ? Pourquoi ne pas simplement « :: sur une séquence fait le résultat de l'argument résultat de la séquence » ?

Si plus d'un :: est utilisé, le résultat des expressions marquées sera renvoyé sous forme de tableau.

C'est une mauvaise idée. Il préfère renflouer dans ce cas. Il n'y a aucun sens à combiner des valeurs de plusieurs types dans un tableau. (Ce serait un tuple, mais ils sont à peu près inutiles en JS.)

Si # est utilisé sur une séquence, les résultats seront Array#concat'ed dans le tableau du parent

C'est une idée horrible. C'est évidemment un tour de passe-passe pour se débarrasser des { return xs.push(x), xs } superflus. Créer un tel cas particulier n'a aucun sens, car il pourrait être résolu de manière générique avec des règles paramétrées. Il n'y a pas beaucoup de séquences à un seul caractère à utiliser pour les opérateurs, et nous ne devrions pas les gaspiller.

Si la règle a un bloc de code avec ::/#, exécutez-le d'abord, puis utilisez la méthode push des résultats

Donc, f = ::"a" { return "b" } devrait avoir comme résultat ["a", "b"] ?

passer 09 . 55 : 7 à l'analyseur généré par l'exemple ci-dessus donnerait :

Ce ne serait pas le cas, il n'y a pas de . ou : mentionné. Je ne vois pas non plus comment le comportement décrit produirait le résultat.

nombre = _ ::valeur+ _

Ce n'est pas non plus ainsi que _ doit être utilisé. Il va soit à droite soit à gauche du jeton, côté choisi une fois pour la grammaire. Si c'est à droite, la règle principale doit également commencer par _ et vice versa.

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

Un exemple de classes implémentées dans l'exemple JavaScript (pas censé être une implémentation réelle) :

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? __ "}"

Pourquoi sont-ils séparés ? Pourquoi ne pas simplement :: sur une séquence fait du résultat de l'argument le résultat de la séquence" ?

N'est-ce pas la même chose ? Je l'ai seulement rendu plus clair pour que le résultat de différents cas d'utilisation comme MethodHead , FunctionParam et FunctionBody puisse être facilement compris.

Il n'y a aucun sens à combiner des valeurs de plusieurs types dans un tableau. (Ce serait un tuple, mais ils sont à peu près inutiles en JS.)

Lors de la construction d'un AST (le résultat le plus courant d'un analyseur généré), à mon avis, cela simplifierait les cas d'utilisation comme MethodHead , au lieu d'écrire :

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

Après y avoir réfléchi davantage, bien que cela simplifie le cas d'utilisation, cela ouvre également la possibilité au développeur de faire une erreur (soit dans la façon dont il implémente sa grammaire, soit dans la façon dont les résultats sont gérés par les actions), donc je pense mettre ceci cas d'utilisation derrière une option comme multipleSingleReturns (par défaut : false ) serait la meilleure solution ici (si cette fonctionnalité est implémentée, c'est le cas).

Si # est utilisé sur une séquence, les résultats seront Array#concat'ed dans le tableau du parent

C'est une idée horrible. C'est évidemment un truc pour se débarrasser des superflus { return xs.push(x), xs }

Cela aide également dans les cas d'utilisation les plus courants comme FunctionParameters où il serait plus agréable d'écrire :

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

il pourrait être résolu de manière générique avec des règles paramétrées

Je pense que je devrais implémenter des valeurs de retour uniques avant les règles paramétrées car je ne sais toujours pas comment procéder avec ces dernières (j'aime utiliser des modèles, mais l'utilisation de rule < .., .. > = .. semble ajouter beaucoup de bruit à la grammaire PEG.js, j'essaie donc de penser à une légère altération de la syntaxe pour mieux l'intégrer), mais c'est un problème distinct.

Il n'y a pas beaucoup de séquences à un seul caractère à utiliser pour les opérateurs, et nous ne devrions pas les gaspiller.

C'est vrai, mais si nous ne les utilisons pas lorsque l'occasion se présente comme celle-ci, alors c'est tout aussi mauvais.

J'ai pensé à utiliser # pour ce cas d'utilisation après m'être souvenu qu'il est utilisé comme opérateur d'expansion dans certains langages qui implémentent des directives de prétraitement, et c'est essentiellement ce que ce cas d'utilisation couvre ici.

Donc, f = ::"a" { return "b" } devrait avoir comme résultat ["a", "b"] ?

Non, car l'analyseur s'attend à ce que le bloc de code renvoie un objet de type tableau contenant une méthode push (ce serait donc f = ::"a" { return [ "b" ] } renvoyant [ "b", "a" ] comme résultat), permettant donc de pousser non seulement dans des tableaux, mais des nœuds personnalisés qui ont la même méthode implémentée pour fonctionner de manière similaire. Étant donné que c'était votre premier raisonnement après avoir lu cela, serait-il préférable de comprendre si cela était derrière une option comme pushSingleReturns ? Si cette option est false (par défaut), avoir un bloc de code après une séquence qui contient des valeurs de retour uniques générerait une erreur.

passer 09 . 55 : 7 à l'analyseur généré par l'exemple ci-dessus donnerait :

Ce ne serait pas le cas, il n'y a pas de . ou : mentionné.

Désolé, c'était une erreur qui a été laissée lorsque j'ai réécrit l'exemple donné.

Ce n'est pas non plus ainsi que _ doit être utilisé. Il va soit à droite soit à gauche du jeton, côté choisi une fois pour la grammaire. Si c'est à droite, la règle principale doit également commencer par _ et vice versa.

Je pense que c'est juste une question de préférence :smile:, bien que je pense qu'il aurait été plus facile de comprendre cet exemple :

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

Je ne vois pas non plus comment le comportement décrit produirait le résultat.

Ce commentaire mis à jour aide-t-il à comprendre ce que j'essaie de dire, et sinon, qu'est-ce que vous ne comprenez pas.

Je suis d'accord avec @polkovnikov-ph que les changements proposés par lieux sont extrêmement peu évidents et ne seront qu'une source d'erreurs supplémentaires.

Si # est utilisé sur une séquence, les résultats seront Array#concat'ed dans le tableau du parent

Que doit-on retourner dans la grammaire start = #('a') ? Autant que je sache, il a pensé à la syntaxe du sucre pour les tableaux aplatis? Je ne pense pas que cet opérateur soit nécessaire. Pour son utilisation la plus évidente - expressions de listes de membres avec séparateurs - la syntaxe spéciale est préférable (voir #30 et mon fork, https://github.com/Mingun/pegjs/commit/db4b2b102982a53dbed1f579477c85c06f8b92e6).

Si la règle a un bloc de code avec ::/#, exécutez-le d'abord, puis utilisez la méthode push des résultats

Comportement extrêmement peu évident. Les actions dans la source de la grammaire sont situées après l'expression et sont généralement appelées après l'analyse de l'expression. Et soudain, d'une manière ou d'une autre, ils commencent à être appelés avant l'analyse. Comment les étiquettes doivent-elles se comporter ?

Les 3 points restants que vous avez réussi à décrire de sorte que leur sens clair a commencé à s'échapper. Dans quelle mesure @polkovnikov-ph a-t-il correctement noté pourquoi la description devait-elle séparer le premier et le deuxième cas ? Seules deux règles simples doivent être exécutées :

  1. :: (pour parler franchement, je n'aime pas le choix de ce caractère, trop bruyant) avant que les expressions conduisent au fait qu'à partir des éléments de nœud sequence marqués par ce caractère retournent
  2. Si dans la séquence un seul de ces éléments, son résultat est renvoyé, sinon le tableau des résultats renvoie

Exemples:

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

Le grand exemple décrit exactement ce que je m'attendrais à voir de :: et ne décrit pas de cas d'utilisation douteux ( # , plusieurs :: ).

N'est-ce pas la même chose ?

C'est ce que j'ai demandé. La description de manière générique est généralement plus utile, car elle permet au lecteur de s'assurer qu'il s'agit bien de la même chose. Merci pour les éclaircissements. :)

à mon avis, cela simplifierait les cas d'utilisation comme MethodHead

Mais pourquoi ne pas créer un objet à la place ? Il y a modifiers: et name: en notation, laissez-les tels quels dans l'objet JS résultant, et ce sera cool.

cela ouvre également la possibilité au développeur de faire une erreur (soit dans la façon dont il implémente sa grammaire, soit dans la façon dont les résultats sont traités par les actions)

Au départ, j'allais écrire à ce sujet, mais j'ai ensuite décidé que je n'avais pas assez d'arguments solides. Je préfère ne pas autoriser plusieurs :: sur le même niveau de séquence (même pas avec un indicateur).

Cela aide également dans les cas d'utilisation les plus courants comme FunctionParameters où il serait plus agréable d'écrire :

Mais c'est la même chose. La syntaxe vraiment sympa serait inter(FunctionParam, "," __) , avec

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

rule < .., .. > = .. semble ajouter beaucoup de bruit à la grammaire PEG.js

Les gens s'attendent <...> ce que inter ci-dessus). Je ne sais pas comment cela interagit dans la grammaire PEG.js avec ; omis. Il peut arriver que f a b = ... soit (partiellement) inclus dans la ligne précédente.

utilisé comme opérateur d'expansion dans certains langages qui implémentent des directives de prétraitement

Oui, mais je préfère l'utiliser intelligemment. Au lieu de l'action push proposée sur les tableaux, je l'utiliserais comme action Object.assign sur les objets, ou même une chose qui correspond à une chaîne vide ( eps ), mais renvoie son argumentation. Pour que, par exemple,

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

renverrait {type: "ident", name: "abc"} pour l'entrée "abc" .

Non, car l'analyseur s'attend à ce que le bloc de code renvoie un objet de type tableau contenant une méthode push (ce serait donc f = ::"a" { return [ "b" ] } renvoyant [ "b", " a" ] comme résultat),

O_O

Je pense que c'est juste une question de préférence vraiment

Non seulement cela, mais aussi les performances. Si chaque jeton a _ deux côtés, les séquences de caractères d'espacement ne correspondent qu'à celles de fin, tandis que _ correspond à rien. Les appels supplémentaires à parse$_ prennent un peu plus de temps. De plus, le code est plus long car il a deux fois plus de _ s.

@Mingun Je pense que @futagoza explore l'espace du design. C'est une bonne chose à faire, surtout si c'est public et que nous avons une chance de ne pas être d'accord :)

L'essentiel n'est pas de demander "pourquoi" mais de dire "non ! pas comme ça !"

image

f = type:#"ident" name:$([a-z]i [a-z0-9_]i+) renverrait {type: "ident", name: "abc"} pour l'entrée "abc" .

S'il te plaît, non, haha. Cette syntaxe est _wayyy_ trop magique.

Je pense que l'opérateur @ proposé parfait tel

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

_Such_ une douleur à taper encore et encore, surtout quand ils sont plus complexes que cela.

Cependant, avec l'opérateur @ , ce qui précède devient simplement :

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

et avec https://github.com/pegjs/pegjs/issues/235#issuecomment -66915879 ou https://github.com/pegjs/pegjs/issues/235#issuecomment -67544080 qui est encore réduit à :

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

... le premier auquel je suis très attaché.

Cela semble être un changement rétrocompatible qui serait simple à réaliser (il semble que quelqu'un l'ait déjà fait).

En fait, si je ne me trompe pas, il pourrait s'agir d'une simple bosse mineure. Peut-être quelque chose à penser pour 0.11.0 @futagoza.

Je viens d'ajouter ceci au maître. J'avais l'intention d'utiliser :: pour la cueillette multiple et @ pour la cueillette simple, mais utiliser :: avec des étiquettes avait l'air vraiment moche et déroutant, alors suspendez cette idée 🙄

J'ai commencé à implémenter cela par moi-même il y a quelque temps, mais je l'ai abandonné jusqu'à maintenant (alors que je devrais faire #579 à la place 😆) et j'ai basé le générateur de bytecode sur l'implémentation de Mingun (https://github.com/Mingun/pegjs/commit/1c1c852bae91868eaa90d9bd9f7e4f722aa6435e )

Vous pouvez l'essayer ici : https://pegjs.org/development/try (l'éditeur en ligne, mais en utilisant PEG 0.11.0-dev)

Temps d'exécution sacré, Batman. Super boulot @futagoza , cela a parfaitement fonctionné - très apprécié. Cela a-t-il déjà été publié dans le tag dev sur npm ? J'aimerais commencer à tester avec.

Pour tous ceux qui veulent l'essayer avec une grammaire de base, mettez ce chiot là-dedans et donnez-lui une entrée comme "abcd" .

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

bar
    = [abcd]*
    ;

Cela a-t-il déjà été publié dans la balise dev sur npm ?

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

Salut, c'est un autre de ces problèmes qui disparaît si nous avons es6, et nous avons besoin de ces caractères pour d'autres choses qui ont été ajoutées à es6 depuis. Ajouter des opérateurs pour des choses que vous pouvez déjà faire est très contre-productif.

Ce billet a fusionné

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

La même chose dans es6, que tout le monde comprendra de manière inhérente, et qui est gratuite lorsque les autres parties de l'analyseur sont terminées, est

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

De manière problématique, lorsqu'elle est testée, cette implémentation de plume semble être boguée, et bien sûr, elle est marquée fermée car cela est corrigé dans une branche qui ne sera jamais publiée

Veuillez rouvrir ce problème, @futagoza , jusqu'à ce que cela soit corrigé dans une version publiée

Cette page vous a été utile?
0 / 5 - 0 notes