Pegjs: Prise en charge complète d'Unicode, notamment pour les points de code en dehors du BMP

Créé le 22 oct. 2018  ·  15Commentaires  ·  Source: pegjs/pegjs

Type de probleme

  • Rapport de bogue : oui
  • Demande de fonctionnalité : un peu
  • Question : non
  • Pas de problème : non

Conditions préalables

  • Pouvez-vous reproduire le problème ? : oui
  • Avez-vous recherché les problèmes du référentiel ? : oui
  • As-tu regardé sur les forums ? : oui
  • Avez-vous effectué une recherche sur le Web (google, yahoo, etc) ? : oui

La description

JavaScript est, sans quelque passe-partout personnalisé , incapable de traiter correctement les caractères/points de code Unicode en dehors du BMP , c'est-à-dire ceux dont l'encodage nécessite plus de 16 bits.

Cette limitation semble s'appliquer à PEG.js, comme le montre l'exemple ci-dessous.

En particulier, j'aimerais pouvoir spécifier des plages telles que [\u1D400-\u1D419] (qui se transforme actuellement en [ᵀ0-ᵁ9] ) ou de manière équivalente [𝐀-𝐙] (qui renvoie une "plage de caractères invalide" Erreur). (Et l'utilisation de la nouvelle notation ES6 [\u{1D400}-\u{1D419}] entraîne l'erreur suivante : SyntaxError: Expected "!", "$", "&", "(", ".", character class, comment, end of line, identifier, literal, or whitespace but "[" found. .)

Existe-t-il un moyen de faire en sorte que cela fonctionne sans modifier PEG.js ?

Étapes pour reproduire

  1. Générez un analyseur à partir de la grammaire ci-dessous.
  2. Utilisez-le pour essayer d'analyser quelque chose d'apparemment conforme.

Exemple de code :

Cette grammaire :

//MathUpper = [𝐀-𝐙]+
MathUpperEscaped = [\u1D400-\u1D419]+

Comportement prévisible:

L'analyseur généré à partir de la grammaire donnée analyse avec succès, par exemple, "𝐀𝐁𝐂".

Comportement réel :

Une erreur d'analyse : Line 1, column 1: Expected [ᵀ0-ᵁ9] but " (Ou, lors du dé-commentaire de l'autre règle, une erreur « Plage de caractères non valide ».)

Logiciel

  • PEG.js : 0.10.0
  • Node.js : non applicable.
  • NPM ou fil : Non applicable.
  • Navigateur : Tous les navigateurs que j'ai testés.
  • Système d'exploitation : macOS Mojave.
  • Éditeur : Tous les éditeurs que j'ai testés.
feature need-help task

Tous les 15 commentaires

Pour être tout à fait honnête, à part la mise à jour du support Unicode pour l' analyseur de grammaire PEG.js et l' exemple JavaScript , j'ai peu ou pas de connaissances sur Unicode, donc je suis actuellement incapable de résoudre ce problème (c'est clairement indiqué dans les deux grammaires : _Non -Les caractères BMP sont complètement ignorés_).

Pour l'instant, tout en travaillant sur des projets personnels et professionnels plus urgents (y compris _PEG.js 0.x_), je continuerai d'attendre que quelqu'un qui comprend mieux Unicode propose des relations publiques 😆, ou éventuellement s'y adapte après _PEG. js v1_, désolé mon pote.

Pour info, les paires de substitution semblent fonctionner. La grammaire
start = result:[\uD83D\uDCA9]+ {return result.join('')}
analyse qui est u+1F4A9. Notez que result.join('') rassemble la paire de substitution, sinon vous obtenez ['\uD83D','\uDCA9'] au lieu de hankey. Les plages seraient problématiques.
En savoir plus sur les paires de substitution : https://en.wikipedia.org/wiki/UTF-16#U +010000_to_U+10FFFF

Cela ne remplace en aucun cas ce que le PO a demandé.

@drewnolan Merci pour l'avertissement

Malheureusement, cette grammaire analyse également \uD83D\uD83D .

Pour les autres qui ont rencontré ce problème : j'ai la chance de n'avoir besoin de gérer qu'un petit sous-ensemble de points de code en dehors du BMP, j'ai donc fini par les mapper dans la zone d'utilisation privée du BMP avant d'analyser et d'inverser ce mappage juste après .

Cette solution est évidemment pleine de problèmes dans le cas général, mais elle fonctionne bien dans mon domaine de problème.

@futagoza - Je ferai de mon mieux pour expliquer. Vous rencontrez ici plusieurs problèmes.

  1. Unicode a des encodages, des notations et des plages

    1. Ici, les choses qui comptent sont les "plages" - quels caractères sont pris en charge - et les "notations" - comment elles sont écrites

    2. Ceux-ci changent avec le temps. Unicode ajoute périodiquement de nouveaux caractères, comme lorsqu'ils ont ajouté des emoji ou des notes de musique

  2. Unicode a des "notations". Ce sont des choses comme utf-16 , ucs4 , et cetera. C'est ainsi que les codepoints , qui sont les données voulues, sont codées sous forme d'octets. utf-16-le par exemple vous permet de coder la plupart des lettres sous forme de paires de deux octets appelées code units , mais utilisez des groupes d'unités de code pour exprimer des caractères de grande valeur jusqu'à 0x10ffff .

    1. Compréhension clé : ce n'est pas vraiment assez élevé. Beaucoup de choses intéressantes, comme des emoji, de gros morceaux de chinois historique, et la question de base de cet article (caractères mathématiques du tableau noir) sont au-dessus de cette ligne



      1. ISO et Unicode Consortium ont été clairs : ils ne le réparent jamais . Si vous voulez des caractères plus élevés, utilisez un encodage plus grand que utf-16.



    2. Compréhension clé n ° 2: Putain de javascript est formellement utf16



      1. Cela signifie qu'il existe des caractères unicode (beaucoup d'entre eux) que le type de chaîne Javascript ne peut pas représenter


      2. OP vous demande de résoudre ce problème


      3. C'est possible mais pas facile - vous devriez implémenter l'algorithme d'analyse Unicode, qui est connu pour être un nid de rat



Je le veux aussi, mais de façon réaliste, cela n'arrivera pas

putain de merde, quelqu'un a commis un remplacement complet de l'analyseur de chaînes il y a près d'un an , et ils ont reconnu les frais généraux, alors ils nous ont généralement laissé utiliser des chaînes JS standard

POURQUOI CE N'EST PAS FUSIONNÉ

@StoneCypher J'aime le feu dans ton cœur ! Mais pourquoi donner du fil à retordre au mainteneur actuel ? Personne n'est redevable de quoi que ce soit. Pourquoi ne pas entretenir votre propre fourche ?

Il n'y a pas de mainteneur actuel. La personne qui a repris PEG n'a jamais rien lâché. Il a travaillé sur le mineur suivant pendant trois ans, puis a dit qu'il n'aimait pas à quoi il ressemblait, qu'il jette tout peg.js et qu'il recommence à partir de quelque chose qu'il a écrit à partir de zéro dans une langue différente, avec un AST.

L'outil a perdu la moitié de sa base d'utilisateurs en attendant trois ans que cet homme valide les correctifs d'une ligne que d'autres personnes ont écrits, comme la prise en charge du module es6, la prise en charge des scripts, la prise en charge des flèches, l'unicode étendu, etc.

Il y a une douzaine de personnes qui lui demandent de fusionner et il n'arrête pas de dire "non, c'est mon projet de loisir maintenant et je n'aime pas ce que c'est"

Beaucoup de gens ont des entreprises basées sur cet analyseur. Ils sont complètement foutus.

Cet homme a promis d'être le mainteneur d'un outil extrêmement important, et n'a fait aucun entretien. Il est temps de laisser quelqu'un d'autre garder cette bibliothèque en règle maintenant.

Pourquoi ne pas entretenir votre propre fourche ?

Je l'ai depuis trois ans maintenant. Mon peg a presque un tiers du suivi des problèmes corrigé.

J'ai dû le cloner, le renommer et créer un nouveau fork pour résoudre le problème de taille pour essayer de le valider, car le mien a trop dérivé

Il est temps que tout le monde reçoive ces correctifs, ainsi que ceux qui sont dans le tracker depuis 2017.

Ce type ne maintient pas la cheville ; il le laisse mourir.

Il est temps de changer.

@drewnolan - donc, je ne sais pas si cela est intéressant ou non, mais les paires de substitution ne fonctionnent pas réellement. C'est juste que, par coïncidence, ils le font habituellement.

Afin de comprendre le problème sous-jacent, vous devez penser au modèle de bits au niveau de l'encodage, et non au niveau de représentation.

C'est-à-dire que si vous avez une valeur Unicode de 240, la plupart des gens penseront "Oh, il veut dire 0b1111 0000 ." Mais en fait, ce n'est pas ainsi qu'Unicode représente 240 ; plus de 127 est représenté par deux octets, car le bit supérieur est un indicateur, pas un bit de valeur. Donc 240 en Unicode est en fait 0b0000 0001 0111 0000 en stockage (sauf en utf-7, qui est réel et pas une faute de frappe, où les choses deviennent super bizarres. Et oui, je sais que Wikipedia dit qu'il n'est pas utilisé. Wikipedia a tort . C'est ce sur quoi les SMS sont envoyés ; c'est peut-être l'encodage de caractères le plus courant par le trafic total.)

Voici donc le problème.

Si vous écrivez un octet STUV WXYZ, puis en utf16, à partir de données ucs4, si votre objet est réduit de moitié, vous pouvez très souvent simplement l'agrafer à nouveau.

Une fois sur 128 vous ne pouvez pas, pour des caractères natifs sur un encodage de plus de deux octets. (On dirait un nombre terriblement spécifique, n'est-ce pas ?)

Parce que lorsque ce bit supérieur dans la deuxième position d'octet est utilisé, le couper en deux ajoutera un zéro là où cela aurait dû être un un. Les agrafer côte à côte en tant que données binaires ne supprime pas à nouveau le concept de valeur. La valeur décodée n'est pas équivalente à la valeur encodée, et vous ajoutez des décodages, pas des encodages.

Il se trouve que la plupart des emoji sont en dehors de cette plage. Cependant, de gros morceaux de plusieurs langues ne le sont pas, y compris le chinois rare, la plupart des symboles mathématiques et musicaux.

Certes, votre approche est assez bonne pour presque tous les emoji, et pour chaque langage humain assez commun pour entrer par Unicode 6, ce qui est une énorme amélioration par rapport au statu quo

Mais ce PR devrait vraiment être fusionné, une fois qu'il est suffisamment testé à la fois pour son exactitude et contre les problèmes de performances inattendus (rappelez-vous, les problèmes de performances unicode sont la raison pour laquelle php est mort)

Il semble que l'expression . (dot character) nécessite également le mode Unicode. Comparer:

const string = '-🐎-👱-';

const symbols = (string.match(/./gu));
console.log(JSON.stringify(symbols, null, '  '));

const pegResult = require('pegjs/')
                 .generate('root = .+')
                 .parse(string);
console.log(JSON.stringify(pegResult, null, '  '));

Sortir:

[
  "-",
  "🐎",
  "-",
  "👱",
  "-"
]
[
  "-",
  "\ud83d",
  "\udc0e",
  "-",
  "\ud83d",
  "\udc71",
  "-"
]

J'ai travaillé récemment dessus, en utilisant le #616 comme base et en le modifiant pour utiliser la syntaxe ES6 \u{hhhhhhh} , je vais créer un PR dans quelques heures.

Le calcul des plages d'expressions régulières UTF-16 divisées par des substituts est un peu compliqué et j'ai utilisé https://github.com/mathiasbynens/regenerate pour cela; ce serait la première dépendance du package pegjs, j'espère que c'est possible (il existe également des polyfills pour les propriétés Unicode qui pourraient être ajoutés en tant que dépendance, voir #648). Voir Wikipedia si vous ne connaissez pas les substituts UTF-16 .

Pour rendre PEG.js compatible avec l'ensemble de l'Unicode, il existe plusieurs niveaux :

  1. Ajouter une syntaxe pour encoder les caractères Unicode au-dessus du BMP, corrigé par #616 ou ma version de la syntaxe ES6,
  2. Reconnaître des chaînes constantes, directement fournies par le point précédent,
  3. Correction du rapport SyntaxError pour afficher éventuellement 1 ou 2 unités de code pour afficher le vrai caractère Unicode,
  4. Calculez avec précision la classe regex pour les points de code BMP et/ou astral - cela seul ne fonctionne pas, voir le point suivant,
  5. Gérer l'incrément du curseur car une classe regex peut maintenant être (1), (2) ou (1 ou 2 selon l'exécution), voir les détails ci-dessous,
  6. Implémentez la règle point . pour capturer 1 ou 2 unités de code.

Pour la plupart des points, nous pouvons réussir à être rétrocompatibles et générer des analyseurs très similaires aux anciens, à l'exception du point 5 car le résultat d'une analyse peut dépendre si la règle des points capture une ou deux unités de code. Pour cela je propose d'ajouter une option runtime pour laisser le choix à l'utilisateur entre deux ou trois choix :

  1. La règle des points ne capture qu'une unité de code BMP,
  2. La règle des points capture un point de code Unicode (1 ou 2 unités de code),
  3. La règle de point capture un point de code Unicode ou un substitut isolé.

La classe Regex peut être analysée statiquement lors de la génération de l'analyseur pour vérifier si elle a une longueur fixe (en nombre d'unités de code). Il y a 3 cas : 1. seulement un BMP ou une seule unité de code, ou 2. seulement deux unités de code, ou 3. une ou deux unités de code selon le temps d'exécution. Pour l'instant, le bytecode suppose qu'une classe regex est toujours une unité de code ( voir ici ). Avec l'analyse statique, on pourrait changer ce paramètre de cette instruction bytecode à 1 ou 2 pour les deux premiers cas. Mais pour le troisième cas, je suppose qu'une nouvelle instruction bytecode doit être ajoutée pour, au moment de l'exécution, obtenir le nombre d'unités de code correspondant et augmenter le curseur en conséquence. D'autres options sans nouvelle instruction bytecode seraient : 1. de toujours calculer le nombre d'unités de code correspondant, mais cela pénalise les performances lors de l'analyse pour les analyseurs BMP uniquement, donc je n'aime pas cette option ; 2. pour calculer si l'unité de code actuelle est un substitut élevé suivi d'un substitut faible pour incrémenter de 1 ou 2, mais cela supposerait que la grammaire a toujours des substituts UTF-16 bien formés sans la possibilité d'écrire des grammaires avec des substituts seuls ( voir le point suivant) et c'est aussi une pénalité de performance pour les analyseurs BMP uniquement.

Il y a la question des mères porteuses isolées (mère porteuse élevée sans mère porteuse faible après elle, ou mère porteuse faible sans mère porteuse élevée avant elle). Mon opinion à ce sujet est qu'une classe regex devrait être exclusivement : soit avec des substituts seuls, soit avec des caractères Unicode UTF-16 bien formés (BMP ou un substitut élevé suivi d'un substitut faible), sinon il y a le danger que les auteurs de grammaire ignorent Les subtilités UTF-16 mélangent les deux et ne comprennent pas le résultat, et les auteurs de grammaire qui souhaitent gérer eux-mêmes les substituts UTF-16 peuvent le faire avec des règles PEG pour décrire les liens entre des substituts spécifiques haut et bas. Je propose d'ajouter un visiteur appliquant cette règle lors de la génération de l'analyseur.

Pour conclure, il est probablement plus facile de gérer la question des substituts isolés dans le PEG que dans les regex car l'analyseur PEG avance toujours, donc soit l'unité de code suivante est reconnue soit elle ne l'est pas, au contraire des regex où éventuellement un retour en arrière pourrait s'associer ou dissocier un substitut élevé d'un substitut faible et par conséquent modifier le nombre de caractères Unicode correspondants, etc.

La syntaxe PR pour ES6 pour le caractère Unicode astral est #651 basé sur #616 et le développement pour les classes est https://github.com/Seb35/pegjs/commit/0d33a7a4e13b0ac7c55a9cfaadc16fc0a5dd5f0c implémentant les points 2 et 3 dans mon commentaire ci-dessus, et uniquement un petit hack pour l'incrémentation du curseur (point 4) et rien pour l'instant pour le point de règle . (point 5).

Mon développement actuel sur ce problème est en grande partie terminé, le travail le plus avancé se trouve sur https://github.com/Seb35/pegjs/tree/dev-astral-classes-final. Les cinq points mentionnés ci-dessus sont traités et le comportement global essaie d'imiter les regex JS concernant les cas limites (et il y en a beaucoup).

Le comportement global est régi par l'option unicode similaire au drapeau unicode dans les regex JS : le curseur est augmenté de 1 caractère Unicode (1 ou 2 unités de code) en fonction du texte réel (ex. [^a] correspond au texte "💯" et le curseur est augmenté de 2 unités de code). Lorsque l'option unicode est fausse le curseur est toujours augmenté d'1 unité de code.

En ce qui concerne l'entrée, je ne sais pas si nous concevons PEG.js de la même manière que les regex JS : devons-nous autoriser [\u{1F4AD}-\u{1F4AF}] (équivalent à [\uD83D\uDCAD-\uD83D\uDCAF] ) dans la grammaire en mode non-Unicode ? On peut faire la différence entre « input Unicode » et « output Unicode » :

  • l'entrée Unicode consiste à autoriser tous les caractères Unicode dans les classes de caractères (qui est calculé en interne comme une unité fixe à 2 codes ou une unité fixe à 1 code)
  • sortie Unicode est l'incrément du curseur de l'analyseur résultant : 1 unité de code ou 1 caractère Unicode pour les règles « point » et « classe de caractères inversés » - les seules règles où les caractères ne sont pas explicitement répertoriés et où nous avons besoin d'une décision de la grammaire auteur

Personnellement, je suppose que je préférerais que nous autorisions l'entrée Unicode, soit de manière permanente, soit avec une option avec une valeur par défaut true car il n'y a pas de surcharge significative et cela permettrait cette possibilité pour tout le monde par défaut, mais la sortie Unicode devrait rester false car les performances des analyseurs générés sont meilleures (toujours un incrément de curseur de 1).

Concernant ce problème en général (et à propos de la sortie Unicode par défaut à false ), il faut garder à l'esprit qu'il est déjà possible d'encoder dans nos grammaires des caractères Unicode, au prix de comprendre le fonctionnement de l'UTF-16 :

// rule matching [\u{1F4AD}-\u{1F4AF}]
my_class = "\uD83D" [\uDCAD-\uDCAF]

// rule matching any Unicode character
my_strict_unicode_dot_rule = $( [\u0000-\uD7FF\uE000-\uFFFF] / [\uD800-\uDBFF] [\uDC00-\uDFFF] )

// rule matching any Unicode character or a lone surrogate
my_loose_unicode_dot_rule = $( [\uD800-\uDBFF] [\uDC00-\uDFFF]? / [\u0000-\uFFFF] )

Ainsi, un auteur de grammaire qui souhaite à la fois un analyseur rapide et être capable de reconnaître les caractères Unicode dans des parties spécifiques de sa grammaire peut utiliser une telle règle. Par conséquent, ce problème concerne simplement la simplification de la gestion Unicode sans plonger dans les éléments internes de l'UTF-16.


À propos de l'implémentation, j'ai considéré lors de ma première tentative que le texte de grammaire était encodé en caractères Unicode et que la règle « point » de l'analyseur PEG.js reconnaissait les caractères Unicode. La deuxième et dernière tentative a annulé cela (la règle de point est toujours 1 unité de code pour une analyse plus rapide) et il existe un petit algorithme dans le visiteur prepare-unicode-classes.js pour reconstruire les caractères Unicode divisés dans les classes de caractères (par exemple [\uD83D\uDCAD-\uD83D\uDCAF] est syntaxiquement reconnu comme [ "\uD83D", [ "\uDCAD", "\uD83D" ], "\uDCAF" ] et cet algorithme le transforme en [ [ "\uD83D\uDCAD", "\uD83D\uDCAF" ] ] ). J'avais prévu d'écrire ça dans la grammaire elle-même mais ça aurait été long et surtout il y a plusieurs façons d'encoder les caractères ("💯", "uD83DuDCAF", "u{1F4AF}"), donc c'est plus facile de l'écrire chez un visiteur.

J'ai ajouté deux opcodes lors de la deuxième tentative :

  • MATCH_ASTRAL similaire à MATCH_ANY mais correspondant à un caractère Unicode (input.charCodeAt(currPos) & 0xFC00) === 0xD800 && input.length > currPos + 1 && (input.charCodeAt(currPos+1) & 0xFC00) === 0xDC00
  • MATCH_CLASS2 très similaire à MATCH_CLASS mais correspondant aux deux prochaines unités de code au lieu d'un seul classes[c].test(input.substring(currPos, currPos+2)
    Ensuite, selon si nous faisons correspondre un caractère à 2 unités de code ou un caractère à 1 unité de code, le curseur est augmenté de 1 ou 2 unités de code avec l'opcode ACCEPT_N , et les classes de caractères sont divisées en deux regex de longueur fixe (1 ou 2 unités de code).

J'ai fait quelques optimisations avec la fonctionnalité "match", en éliminant lors de la génération les chemins "dead code" selon le mode (Unicode ou non) et la classe des caractères.

Notez également que les expressions régulières sont toujours positives dans cette implémentation : les expressions régulières inversées renvoient le contraire, ce qui donne le bytecode. C'était plus facile d'éviter les cas limites autour des mères porteuses.

J'ai écrit de la documentation mais j'en ajouterai probablement d'autres (peut-être un guide pour expliquer rapidement les détails des options unicode et des extraits avec la règle de point Unicode faite maison). Je vais ajouter des tests avant de le soumettre en tant que PR.

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