Pegjs: Implémenter des règles paramétrables

Créé le 25 août 2011  ·  29Commentaires  ·  Source: pegjs/pegjs

Ce serait bien de pouvoir paramétrer des règles avec des variables ;

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

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

Commentaire le plus utile

Merci pour votre temps d'explication

J'aurai probablement une autre question à vous poser dans dix ans. Bonne année 2020

Tous les 29 commentaires

Avez-vous un cas d'utilisation spécifique où cela vous ferait économiser beaucoup de travail ou rendrait possible quelque chose actuellement impossible ?

Cela rend l'analyse des niveaux d'indentation beaucoup plus facile en appelant des règles qui ont le niveau passé en argument.

De plus, dans une pure logique DRY, lorsque vous faites des choses comme "des choses délimitées par ce caractère avec une séquence d'échappement telle", il est plus agréable d'appeler quelque chose comme delimited('\'', '\\') que de simplement faire la règle (et ses actions !) trois fois .

J'aurais dû être plus clair. Par "spécifique", je cherchais quelque chose comme "Je travaillais sur une grammaire de la langue X et il y a 5 règles là-dedans qui auraient pu être combinées en une seule, les voici :" C'est-à-dire que je voulais voir le monde réel cas d'utilisation et code du monde réel. À partir de là, je peux mieux juger dans quels cas cette fonctionnalité serait utile et pour combien de personnes.

S'il vous plaît ne prenez pas cela car je suis opposé à cette fonctionnalité en soi. Je ne veux généralement pas implémenter des fonctionnalités utiles uniquement pour une infime fraction de langages ou de développeurs en raison de la complexité et du coût de mise en œuvre. Et dans ce cas, le coût est relativement élevé.

En écrivant simplement un analyseur pour javascript, je pourrais avoir string = delimited_by('\'') / delimited_by('\"') puis plus tard regexp = delimited_by('/') .

Dernièrement, j'ai écrit un analyseur pour mon propre langage. J'ai quelque chose comme ça dans un framework PEG que j'ai écrit pour python :

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

Et je peux alors écrire :

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

Comme j'ai à peu près autant de niveaux de priorité qu'en C++ (tous ses opérateurs plus quelques autres), je vous laisse imaginer à quel point in peut être utile. Je n'ai pas encore fini d'analyser les expressions, pourtant je l'utilise déjà 12 fois.

Ce serait formidable s'il était combiné avec une fonction "d'importation"

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

(C'est un peu plus compliqué que la demande d'OP, mais cela semblait trop proche pour justifier son propre fil.)

Je construis un analyseur R5RS Scheme avec l'aide de PEG.js. Tout est rose sauf pour les quasiquotes, qui nécessitent une analyse contextuelle. Il serait utile de pouvoir paramétrer des règles pour générer des règles à la volée à partir de modèles, en évitant une grande quantité de post-traitement gênant. Par exemple, une grammaire de quasiquotation simplifiée pourrait ressembler à :

    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]

Je suis intéressé à contribuer au développement de cette fonctionnalité s'il y a un intérêt à l'ajouter à l'outil.

La principale raison de le faire serait de prendre en charge les grammaires sensibles au contexte, ce qui, si je ne me trompe pas, est la plupart des langages populaires (je sais avec certitude que C et python ont des éléments spécifiques au contexte). Selon Trevor Jim, Haskell n'est pas non plus sans contexte et affirme que la plupart des langages ne le sont pas :

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

L'utilisation d'un état externe dans un analyseur qui peut revenir en arrière (comme le peut PEG) est dangereuse et peut produire des problèmes tels que ceux que l'on peut voir dans cet analyseur :

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

Ce qui précède renvoie 2 au lieu de la bonne réponse de 1. Des problèmes comme celui-ci peuvent être difficiles à résoudre, peuvent créer des bogues insidieux difficiles à trouver, et lorsqu'ils sont trouvés, il peut être très difficile de les contourner, encore moins de le faire avec élégance. . Je ne sais pas comment faire cela sans faire de post-traitement des données renvoyées par PEG. Si d'une manière ou d'une autre, votre analyseur lui-même a besoin du décompte, ce n'est tout simplement pas de chance.

Actuellement, l'utilisation (dangereuse) de l'état externe est le seul moyen d'analyser la grammaire sensible au contexte. Avec des règles paramétrées, un analyseur pourrait analyser ceci sans risquer un état invalide :

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, vous avez demandé des situations réelles, et la syntaxe d'indentation des espaces blancs de python est clairement un exemple ici. Je veux faire une syntaxe d'indentation d'espace blanc similaire à Lima (le langage de programmation que je crée avec PEG). Mais je ne voudrais pas implémenter quelque chose comme ça quand je pourrais créer par inadvertance un état invalide qui ferait tout exploser. Je pourrais nommer n'importe quelle construction d'analyse qui nécessite un contexte, comme x* y de C (est-ce que x fois y ou y est défini comme un pointeur vers une valeur de type x ?).

Notez que pour que les grammaires sensibles au contexte soient analysables, il faudrait nécessairement transmettre les informations renvoyées par les sous-expressions déjà appariées dans une règle paramétrée - sinon l'analyseur ne peut réellement utiliser aucun des contextes. Voici un exemple réel d'un type de chaîne que j'envisage pour Lima qui ne fonctionne que si l'analyse paramétrée est disponible et peut accéder (en tant que variables) aux étiquettes d'expressions précédemment appariées :

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
}

Cela serait capable d'analyser une chaîne comme string[10:abcdefghij] . Vous ne pouvez pas faire cela avec un joli PEG.js pur tel quel. Vous avez fait quelque chose d'horrible comme :

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

De nombreux protocoles ont ce type de besoin d'analyse - par exemple, les paquets IPv4 ont un champ décrivant leur longueur totale. Vous avez besoin de ce contexte pour analyser correctement le reste du paquet. Il en va de même pour IPv6, UDP et probablement tout autre protocole basé sur les paquets. La plupart des protocoles utilisant TCP vont également avoir besoin de quelque chose comme ça, car il faut être capable de transmettre plusieurs objets complètement séparés en utilisant le même flux de caractères conceptuels.

Quoi qu'il en soit, j'espère avoir donné de bons exemples et des raisons pour lesquelles je pense que ce n'est pas seulement une fonctionnalité intéressante, non seulement une fonctionnalité puissante, mais vraiment une fonctionnalité essentielle qui manque à de nombreux analyseurs (y compris, pour le moment, PEG.js ).

Pegasus (un projet qui partage la majeure partie de sa syntaxe avec peg.js) résout ce problème en ayant une expression #STATE{} qui a la possibilité de muter un dictionnaire d'état. Ce dictionnaire d'état est inversé lorsque les règles sont inversées. Cela lui permet de prendre en charge l'analyse des espaces blancs significatifs (voir l'entrée wiki sur les espaces blancs significatifs pour les détails).

De plus, en revenant sur l'état avec le curseur d'analyse, la mémorisation peut également être effectuée pour les règles avec état.

Peg.js pourrait facilement faire la même chose, je pense.

Comment Pegasus gère-t-il l'état de retour en arrière lorsque les règles reviennent en arrière ? Je peux imaginer que vous pourriez garder un instantané de tout l'état du programme qui a changé et le rétablir, mais cela coûterait cher. Je pourrais imaginer garder un instantané des seules variables qui ont changé, mais cela obligerait soit l'utilisateur à le spécifier, ce qui ajouterait de la complexité à la création d'analyseurs, soit obligerait l'analyseur à comprendre d'une manière ou d'une autre tout l'état changé dans un peu de code. Aucun de ces éléments ne semble idéal, alors comment Pegasus le fait-il ?

Théoriquement, l'analyseur pourrait éviter les actions exécutées de manière non valide si A. les actions sont mises en file d'attente dans les fermetures et exécutées uniquement une fois que l'analyseur est complètement terminé, et B. parce qu'elles s'exécutent après la fin de l'analyseur, elles ne peuvent pas annuler une correspondance de règle. Peut-être que ce schéma serait plus optimal que le retour en arrière de l'état effectué dans pegasus ?

De plus, résoudre le problème de l'état invalide est vraiment très agréable, mais cela ne résout pas le problème d'expressibilité que j'ai soulevé lié à une chaîne littérale comme string[10:abcdefghij], mais je suis vraiment intéressé par la façon dont cela fonctionne

Il ne revient pas sur l'état de l'ensemble du programme. Il maintient un dictionnaire d'état immuable. Il enregistre le dictionnaire d'état actuel avec le curseur et chaque fois que le curseur revient en arrière, le dictionnaire d'état revient en arrière avec lui. Le dictionnaire est immuable partout en dehors des actions #STATE{} et est COPIÉ juste avant chaque changement d'état.

Il y a une petite pénalité de performance pour définir une variable supplémentaire chaque fois que vous avancez le curseur, mais cela est largement compensé par la possibilité de mémoriser des règles avec état. De plus, cela ne conduit pas à des tonnes d'allocation de mémoire, car la nature immuable du dictionnaire d'état lui permet d'être partagé jusqu'à ce qu'il soit muté. Par exemple, si vous n'aviez pas d'état dans votre analyseur, il n'y aurait qu'une seule allocation : un seul dictionnaire d'état (vide).

JavaScript n'a pas (à ma connaissance) la capacité de rendre un objet immuable, mais c'était surtout une fonctionnalité de sécurité. Peg.js aurait juste besoin de copier un dictionnaire d'état avant de traiter chaque bloc de code #STATE{} et de revenir en arrière chaque fois que le curseur est en arrière.

Oh ok, donc l'utilisateur doit essentiellement spécifier l'état qu'il change. C'est plutôt cool. Mais je ne pense toujours pas que cela couvre vraiment les mêmes avantages que le paramétrage. Cela semble être probablement utile en soi pour d'autres choses.

Je viens d'écrire un fork qui fournit un environnement, accessible via la variable env : https://github.com/tebbi/pegjs
C'est la même chose que l'objet #STATE{} suggéré ci-dessus.
C'est un hack rapide, utilisant une variable globale (package-), qui est restaurée chaque fois qu'une fonction d'analyse est laissée. La copie de env est accomplie avec Object.create().

Voici un exemple de grammaire qui l'utilise pour analyser des blocs définis par des espaces à 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]

Voici un exemple d'entrée pour l'analyseur résultant :

b:
   c
   d:
       e
   f
g

J'ai l'impression que PEG.js ne prend en charge aucun paramètre sur les règles - ce qui est surprenant. Cette fonctionnalité est très importante pour moi.

Ce dont j'ai besoin est plus simple que la demande de l'OP - l'OP veut modifier la grammaire elle-même en fonction du paramètre, mais au minimum, j'ai juste besoin de passer un entier dans une règle. Fondamentalement, je veux traduire une règle LLLPG qui ressemble à ceci (où PrefixExpr est une expression de haute priorité telle qu'une expression de préfixe comme -x , ou un identifiant...) :

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

Mon langage a 25 niveaux de priorité, et avec ces règles, je les ai presque tous réduits pour être traités par une seule règle (vous pouvez penser à Precedence comme un wrapper autour de quelques entiers qui décrivent la priorité d'un opérateur). De plus, mon langage a un nombre infini d'opérateurs (essentiellement n'importe quelle séquence de ponctuation) et la priorité d'un opérateur est décidée après son analyse. Bien qu'il soit _techniquement_ possible d'analyser le langage de la manière habituelle, avec une règle distincte pour chacun des 25 types d'opérateurs, ce serait une manière horrible de le faire.

Vous pouvez également voir ici que la règle interne FinishPrimaryExpr construit un arbre de syntaxe qui incorpore un paramètre passé à partir de la règle englobante.

Alors ... existe-t-il un moyen de passer des paramètres à une règle PEG.js ?

+1 ! Dans mon cas, je veux simplement générer un parseur pour une syntaxe, où certains délimiteurs sont globalement configurables. Dans ce cas, je peux y parvenir en remplaçant les littéraux délimiteurs par des expressions match any combinées avec un prédicat, mais ce serait beaucoup plus élégant (et aussi plus efficace) si le match-everything pouvait simplement être remplacé par une variable.

+1, avez-vous une chance de voir cela mis en œuvre dans un avenir prévisible ?

Un autre cas d'utilisation. Ceci provient de votre exemple 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
  (...)

(...)

Toutes ces règles ...NoIn (et il y en a BEAUCOUP ) sont requises simplement à cause de l'instruction for in . Une bien meilleure approche ne serait-elle pas quelque chose comme:

(...)

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

(...)

Cela ressemble beaucoup à la façon dont, par exemple, la grammaire JavaScript est spécifiée : https://tc39.github.io/ecma262/#prod -IterationStatement (notez le ~In )

Un langage que je développe actuellement a ce problème précis : j'aimerais désactiver/activer certaines règles à certains points uniquement. J'aimerais beaucoup m'abstenir de dupliquer toutes les règles affectées comme vous l'avez fait pour la grammaire JavaScript.

Existe-t-il un autre moyen d'y parvenir sans dupliquer les règles ?

+1, avez-vous une chance de voir cela mis en œuvre dans un avenir prévisible ?

Ce problème a un jalon assigné (post-1.0.0). La version actuelle de PEG.js est 0.10.0. Évidemment, les problèmes post-1.0.0 seront traités après la sortie de la 1.0.0, ce qui arrivera à un moment donné après la sortie de la 0.11.0 selon la feuille de route.

Ceci devrait répondre à votre question. La meilleure façon d'accélérer l'ensemble du processus est d'aider à résoudre les problèmes ciblés pour 0.11.0 et 1.0.0 .

Existe-t-il un autre moyen d'y parvenir sans dupliquer les règles ?

Un moyen possible consiste à suivre l'état manuellement, puis à utiliser des prédicats sémantiques. Mais cette approche a des problèmes de retour en arrière et je ne la recommanderais pas (en d'autres termes, lorsqu'on me donne le choix entre la duplication de règles et le suivi manuel de l'état, je choisirais la duplication).

Il existe deux types d'arguments qui peuvent être passés aux parseurs :

  1. Valeurs. Les grammaires pour des langages comme Python, Nim et Haskell (et aussi Scheme d'une manière différente) dépendent de la "profondeur" de l'expression à l'intérieur de l'arbre. Écrire une grammaire correcte nécessite de passer en quelque sorte ce contexte.
  2. Analyseurs. leftAssociative(element, separator) , escapedString(quote) et withPosition(parser) en sont de bons exemples.

Il devrait y avoir un moyen de marquer d'une manière ou d'une autre si l'argument est un analyseur ou une valeur. Lorsque j'ai essayé de trouver la bonne approche, j'ai fini par utiliser des variables globales pour le contexte, et c'est évidemment une impasse. Est-ce que quelqu'un a des idées là-dessus?

Et les macros ?

Donné:

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

Lorsque:

  = Add <MyExpression, MyAdd>

MyExpression
  = [0-9]+

Puis:

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

MyExpression
  = [0-9]+

Cela nous permet de construire des règles de bas en haut :smirk:

Je suis d'accord, je recommande aux développeurs d'ajouter cette fonctionnalité :)

J'ai vraiment besoin de cette fonctionnalité pour une grammaire JavaScript mise à jour que j'écris, donc c'est en haut de ma liste de souhaits. Je vais essayer et voir comment ça marche.

@samvv Je suis tombé sur cela d'un itinéraire très différent et je n'ai pas encore lu tout le fil.
Cependant, dans #572, dont je me suis référé ici, je montre une technique avec laquelle vous pouvez simuler des règles paramétrées.

C'est-à-dire, essentiellement : les fonctions de retour comme résultats d'analyse intermédiaires.

Ce "truc" n'est en aucun cas mon invention, et probablement plutôt maladroit pour votre objectif, je suppose. Mais cela pourrait être une solution de contournement pour vous. Je veux dire jusqu'à "post v1.0"... :)

@meisl Cool, merci pour le conseil ! Je vais l'essayer quand je trouverai un peu de temps.

@samvv Ooh, ah... J'ai bien peur d'avoir oublié quelque chose d'assez important :

Cela fait une grande différence si vous voulez que la règle paramétrée

  • seulement être en mesure de produire des valeurs , qui dépendent du paramètre
  • ou (aussi) de faire dépendre ses décisions d'analyse du paramètre

Ce que je proposais n'aide qu'avec le premier - alors que le second est le problème réel de l'OP...
Désolé.

Cependant, il existe une solution de contournement même pour ce dernier, quoique encore PLUS maladroit.
Et, la partie "décisions dépendantes" n'a rien à voir avec les fonctions de retour...

J'ajoute un exemple à essayer dans https://pegjs.org/online

L'idée de base est la suivante : utilisez l'état global pour mémoriser le "terminateur" actuel. C'est tout à fait un hack, certes, et répétitif.
Mais : ajouter encore un autre délimiteur, disons | signifierait simplement ajouter une autre alternative à str :

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

qui ne diffère des autres que par ce seul caractère |


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

... sur les entrées

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

ps : ce n'est vraiment PAS quelque chose que l'on voudrait écrire à la main, je sais. Mais bon, imaginez qu'il soit généré pour vous... Je pense qu'en principe c'est comme ça .

@ceymard - Je me rends compte que c'est dix ans plus tard, mais je suis curieux de savoir en quoi c'est différent de #36

Wow, j'ai mis du temps à m'en souvenir. 10 années !

Dans ce PR, les règles prennent des arguments et peuvent être paramétrées. Ceci est destiné à être utilisé par la grammaire elle-même pour éviter de répéter des règles similaires mais différentes.

Dans #36, les règles sont spécifiées en dehors de la grammaire elle-même. La grammaire est donc elle-même paramétrée.

Je pense que la portée est différente, bien que l'on puisse soutenir qu'une grammaire est elle-même une règle et qu'il s'agit donc du même problème. Je pense que ce n'est pas le cas, car # 36 signifierait probablement de légers changements d'API, alors que ce PR ne le ferait pas.

Donc, pour abuser de la terminologie C++ d'une manière profondément incorrecte, les premiers sont des modèles statiques, tandis que les seconds sont des appels de constructeur ?

Je suppose que cette analogie fonctionne un peu, oui.

Merci pour votre temps d'explication

J'aurai probablement une autre question à vous poser dans dix ans. Bonne année 2020

Ce serait vraiment utile pour supprimer la redondance de ma définition d'analyseur. J'ai une grammaire personnalisée qui est volontairement très détendue, et certaines règles doivent être appliquées dans des contextes légèrement différents.

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