Pegjs: Volle Unicode-Unterstützung, nämlich für Codepunkte außerhalb des BMP

Erstellt am 22. Okt. 2018  ·  15Kommentare  ·  Quelle: pegjs/pegjs

Problemtyp

  • Fehlerbericht: ja
  • Funktionsanfrage: irgendwie
  • Frage: nein
  • Kein Problem: nein

Voraussetzungen

  • Können Sie das Problem reproduzieren?: ja
  • Haben Sie die Repository-Probleme durchsucht?: ja
  • Hast du in den Foren nachgesehen?: ja
  • Haben Sie eine Websuche durchgeführt (Google, Yahoo usw.)?: Ja

Beschreibung

JavaScript ist ohne benutzerdefinierten Boilerplate nicht in der Lage, mit Unicode-Zeichen/Codepunkten außerhalb des BMP richtig umzugehen, dh solchen, deren Codierung mehr als 16 Bit erfordert.

Diese Einschränkung scheint sich auf PEG.js zu übertragen, wie im folgenden Beispiel gezeigt.

Insbesondere möchte ich in der Lage sein, Bereiche wie [\u1D400-\u1D419] (das sich derzeit in [ᵀ0-ᵁ9] verwandelt) oder entsprechend [𝐀-𝐙] (was einen "Ungültigen Zeichenbereich" auslöst) anzugeben. Error). (Und die Verwendung der neueren ES6-Notation [\u{1D400}-\u{1D419}] führt zu folgendem Fehler: SyntaxError: Expected "!", "$", "&", "(", ".", character class, comment, end of line, identifier, literal, or whitespace but "[" found. .)

Gibt es eine Möglichkeit, dies zum Laufen zu bringen, ohne dass Änderungen an PEG.js erforderlich sind?

Schritte zum Reproduzieren

  1. Generieren Sie einen Parser aus der unten angegebenen Grammatik.
  2. Verwenden Sie es, um zu versuchen, etwas vorgeblich konformes zu analysieren.

Beispielcode:

Diese Grammatik:

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

Erwartetes Verhalten:

Der aus der gegebenen Grammatik generierte Parser analysiert erfolgreich, zum Beispiel "𝐀𝐁𝐂".

Tatsächliches Verhalten:

Ein Analysefehler: Line 1, column 1: Expected [ᵀ0-ᵁ9] but " (Oder, wenn die andere Regel auskommentiert, ein Fehler "Ungültiger Zeichenbereich".)

Software

  • PEG.js: 0.10.0
  • Node.js: Nicht zutreffend.
  • NPM oder Garn: Nicht zutreffend.
  • Browser: Alle von mir getesteten Browser.
  • Betriebssystem: macOS Mojave.
  • Editor: Alle Editoren, die ich getestet habe.
feature need-help task

Alle 15 Kommentare

Um ganz ehrlich zu sein, abgesehen von der Aktualisierung der Unicode-Unterstützung für den PEG.js-Grammatik-Parser und das JavaScript-Beispiel habe ich wenig bis gar keine Kenntnisse über Unicode, daher kann ich dieses Problem derzeit nicht beheben (es ist in beiden Grammatiken klar angegeben: _Non -BMP-Zeichen werden komplett ignoriert_).

Während ich an dringenderen personellen und arbeitsbezogenen Projekten arbeite (einschließlich _PEG.js 0.x_), warte ich weiterhin darauf, dass jemand, der Unicode besser versteht, einige PRs anbietet 😆 oder irgendwann nach _PEG dazukommt. js v1_, sorry Kumpel.

Zu Ihrer Information, Ersatzpaare scheinen zu funktionieren. Die Grammatik
start = result:[\uD83D\uDCA9]+ {return result.join('')}
analysiert 💩, was u+1F4A9 ist. Beachten Sie, dass result.join('') das Ersatzpaar wieder zusammensetzt, andernfalls erhalten Sie ['\uD83D','\uDCA9'] anstelle von Hankey. Reichweiten wären problematisch.
Mehr zu Ersatzpaaren: https://en.wikipedia.org/wiki/UTF-16#U +010000_to_U+10FFFF

Dies ersetzt keinesfalls das, was das OP verlangt hat.

@drewnolan Danke für den Hinweis 👍

Leider analysiert diese Grammatik auch \uD83D\uD83D .

Für andere, die auf dieses Problem gestoßen sind: Ich habe das Glück, dass ich nur eine kleine Teilmenge von Codepunkten außerhalb des BMP verarbeiten muss, also habe ich sie vor dem Parsing in den privaten Nutzungsbereich des BMP gemappt und direkt danach invertiert .

Diese Lösung ist im allgemeinen Fall offensichtlich mit Problemen behaftet, funktioniert aber in meinem Problembereich gut.

@futagoza - Ich werde mein Bestes tun, um es zu erklären. Sie stehen hier vor mehreren Problemen.

  1. Unicode hat Kodierungen, Notationen und Bereiche

    1. Wichtig sind hier "Bereiche" - welche Zeichen unterstützt werden - und "Notationen" - wie sie geschrieben werden

    2. Diese ändern sich im Laufe der Zeit. Unicode fügt regelmäßig neue Zeichen hinzu, z. B. beim Hinzufügen von Emojis oder Musiknoten

  2. Unicode hat "Notationen". Das sind Dinge wie utf-16 , ucs4 usw. Auf diese Weise werden die codepoints , die die beabsichtigten Daten sind, als Bytes codiert. utf-16-le können Sie beispielsweise die meisten Buchstaben als Zwei-Byte-Paare namens code units codieren, aber Gruppen von Codeeinheiten verwenden, um hochwertige Zeichen bis zu 0x10ffff auszudrücken.

    1. Schlüsselverständnis: Das ist nicht wirklich hoch genug. Viele interessante Dinge, wie Emojis, große Teile von historischem Chinesisch und die grundlegende Frage dieses Beitrags (Mathe-Zeichen auf der Tafel) befinden sich über dieser Linie



      1. ISO und Unicode Consortium haben klargemacht: Sie werden es nie reparieren . Wenn Sie höhere Zeichen wünschen, verwenden Sie eine größere Kodierung als utf-16.



    2. Schlüsselverständnis #2: Verdammtes Javascript ist



      1. Dies bedeutet, dass es Unicode-Zeichen (viele davon) gibt, die der Javascript-String-Typ nicht darstellen kann


      2. OP bittet Sie, dies zu beheben


      3. Dies ist möglich, aber nicht einfach - Sie müssten den Unicode-Parsing-Algorithmus implementieren, der bekanntlich ein Rattennest ist



Ich will das auch, aber realistischerweise wird das nicht passieren

Heilige Scheiße, jemand hat vor fast einem Jahr einen vollständigen String-Parser ersetzt , und sie haben den Overhead erkannt, also ließen sie uns normalerweise Standard-JS-Strings verwenden

WARUM WIRD DAS NICHT ZUSAMMENGEFÜHRT

@StoneCypher Ich liebe das Feuer in deinem Herzen! Aber warum es dem aktuellen Betreuer so schwer machen? Niemand ist etwas geschuldet. Warum pflegen Sie nicht Ihre eigene Gabel?

Es gibt keinen aktuellen Betreuer. Die Person, die PEG übernommen hat, hat nie etwas freigegeben. Er arbeitete drei Jahre lang an der nächsten Minor, sagte dann, dass ihm nicht gefiel, wie es aussah, wirf alle peg.js weg und begann mit etwas, das er von Grund auf in einer anderen Sprache geschrieben hatte, mit einem inkompatiblen AST.

Das Tool hat die Hälfte seiner Benutzerbasis verloren, als es drei Jahre darauf wartete, dass dieser Mann einzeilige Fixes festlegt, die andere Leute geschrieben haben, wie es6-Modul-Unterstützung, Typoskript-Unterstützung, Pfeil-Unterstützung, erweiterter Unicode usw.

Es gibt ein Dutzend Leute, die ihn bitten, sich zusammenzuschließen, und er sagt immer wieder: "Nein, das ist jetzt mein Hobbyprojekt und ich mag es nicht."

Viele Leute haben Unternehmen, die auf diesem Parser basieren. Sie sind komplett verschraubt.

Dieser Mann hat versprochen, Betreuer für ein äußerst wichtiges Werkzeug zu sein, und hat keine Wartung durchgeführt. Es ist an der Zeit, diese Bibliothek jetzt von jemand anderem in gutem Zustand halten zu lassen.

Warum pflegen Sie nicht Ihre eigene Gabel?

Ich habe jetzt seit drei Jahren. Mein peg hat fast ein Drittel der Problemverfolgung behoben.

Ich musste es klonen, umbenennen und eine neue Abzweigung erstellen, um das Größenproblem zu beheben, um zu versuchen, es zu begehen, weil meins zu stark gedriftet ist

Es ist Zeit für alle anderen, diese Fixes zu erhalten, sowie diejenigen, die seit 2017 im Tracker sitzen.

Dieser Typ hält keinen Peg aufrecht; er lässt es sterben.

Es ist Zeit für Veränderung.

@drewnolan - ich bin mir nicht sicher, ob das interessant ist oder nicht, aber

Um das zugrunde liegende Problem zu verstehen, müssen Sie an das Bitmuster der Codierungsebene denken, nicht an die Darstellungsebene.

Das heißt, wenn Sie einen Unicode-Wert von 240 haben, denken die meisten Leute "Oh, er meint 0b1111 0000 ." Aber eigentlich stellt Unicode 240 nicht so dar; über 127 wird durch zwei Bytes dargestellt, da das oberste Bit ein Flag und kein Wertbit ist. 240 in Unicode ist also tatsächlich 0b0000 0001 0111 0000 im Speicher (außer in utf-7, was echt und kein Tippfehler ist, wo die Dinge super seltsam werden. Und ja, ich weiß, Wikipedia sagt, dass es nicht verwendet wird. Wikipedia liegt falsch . Es ist das, worüber SMS gesendet werden; es könnte die am häufigsten verwendete Zeichencodierung nach dem gesamten Datenverkehr sein.)

Hier ist also das Problem.

Wenn Sie ein Byte STUV WXYZ schreiben, dann können Sie es in utf16 aus ucs4-Daten, wenn Ihr Ding halbiert wird, ziemlich oft einfach wieder zusammenheften.

Einmal in 128 ist dies nicht möglich, für Zeichen, die nativ über zwei Bytes kodiert sind. (Klingt nach einer ganz bestimmten Zahl, nicht wahr?)

Denn wenn das oberste Bit an der zweiten Byte-Position verwendet wird, wird durch das Halbieren eine Null hinzugefügt, wo dies eine Eins hätte sein sollen. Sie wieder nebeneinander als Binärdaten zusammenzuheften entfernt das Wertkonzept nicht wieder. Der decodierte Wert entspricht nicht dem codierten Wert, und Sie fügen Decodierungen, keine Codierungen an.

Zufällig liegen die meisten Emojis außerhalb dieses Bereichs. Große Teile mehrerer Sprachen sind es jedoch nicht, darunter seltenes Chinesisch, die meisten mathematischen und musikalischen Symbole.

Zugegeben, Ihr Ansatz ist gut genug für fast alle Emojis und für jede menschliche Sprache, die für Unicode 6 gängig genug ist, was eine enorme Verbesserung gegenüber dem Status quo darstellt

Aber dieser PR sollte wirklich zusammengeführt werden, sobald er ausreichend sowohl auf Korrektheit als auch auf unerwartete Leistungsprobleme getestet wurde (denken Sie daran, Unicode-Leistungsprobleme sind der Grund, warum PHP gestorben ist).

Es scheint, dass der Ausdruck . (dot character) auch den Unicode-Modus benötigt. Vergleichen:

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, '  '));

Ausgabe:

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

Ich habe vor kurzem daran gearbeitet, indem ich #616 als Basis verwendet und es modifiziert habe, um die ES6-Syntax \u{hhhhhhh} . Ich werde in einigen Stunden eine PR erstellen.

Die Berechnung der nach Surrogaten aufgeteilten UTF-16-Regex-Bereiche ist etwas kompliziert und ich habe dafür https://github.com/mathiasbynens/regenerate verwendet; dies wäre die erste Abhängigkeit des Pakets pegjs, ich hoffe, es ist möglich (es gibt auch Polyfills für Unicode-Eigenschaften, die als Abhängigkeit hinzugefügt werden könnten, siehe #648). Siehe Wikipedia, wenn Sie UTF-16-Surrogate nicht kennen .

Um PEG.js mit dem gesamten Unicode kompatibel zu machen, gibt es verschiedene Ebenen:

  1. Fügen Sie eine Syntax hinzu, um Unicode-Zeichen über dem BMP zu codieren, behoben durch #616 oder meine ES6-Syntax-Version.
  2. Konstante Zeichenfolgen erkennen, die direkt vom vorherigen Punkt bereitgestellt werden,
  3. Korrigieren Sie die SyntaxError-Berichterstellung, um möglicherweise 1 oder 2 Codeeinheiten anzuzeigen, um das echte Unicode-Zeichen anzuzeigen.
  4. Berechne die Regex-Klasse für BMP- und/oder Astral-Codepunkte genau - das allein funktioniert nicht, siehe nächster Punkt,
  5. Verwalten Sie das Cursorinkrement, da eine Regex-Klasse jetzt (1), (2) oder (1 oder 2 je nach Laufzeit) sein kann, siehe Details unten.
  6. Implementieren Sie die Regel dot . , um 1 oder 2 Codeeinheiten zu erfassen.

Für die meisten Punkte können wir abwärtskompatibel sein und Parser generieren, die den älteren sehr ähnlich sind, mit Ausnahme von Punkt 5, da das Ergebnis eines Parsens davon abhängen kann, ob die Punktregel eine oder zwei Codeeinheiten erfasst. Dazu schlage ich vor, eine Laufzeitoption hinzuzufügen, um dem Benutzer die Wahl zwischen zwei oder drei Auswahlmöglichkeiten zu lassen:

  1. Die Punktregel erfasst nur eine BMP-Codeeinheit,
  2. Die Punktregel erfasst einen Unicode-Codepunkt (1 oder 2 Codeeinheiten),
  3. Die Punktregel erfasst einen Unicode-Codepunkt oder ein einsames Surrogat.

Regex-Klassen können während der Parser-Generierung statisch analysiert werden, um zu überprüfen, ob sie eine feste Länge (in Anzahl von Codeeinheiten) haben. Es gibt 3 Fälle: 1. nur ein BMP oder eine einzelne Codeeinheit oder 2. nur zwei Codeeinheiten oder 3. ein oder zwei Codeeinheiten je nach Laufzeit. Im Moment geht der Bytecode davon aus, dass eine Regex-Klasse immer eine Codeeinheit ist ( siehe hier ). Mit der statischen Analyse könnten wir diesen Parameter dieser Bytecode-Anweisung für die beiden ersten Fälle auf 1 oder 2 ändern. Aber für den dritten Fall sollte meiner Meinung nach ein neuer Bytecode-Befehl hinzugefügt werden, um zur Laufzeit die Anzahl der übereinstimmenden Codeeinheiten zu erhalten und den Cursor entsprechend zu erhöhen. Andere Optionen ohne einen neuen Bytecode-Befehl wären: 1. immer die Anzahl der übereinstimmenden Codeeinheiten zu berechnen, aber dies ist eine Leistungseinbuße beim Parsen für reine BMP-Parser, daher mag ich diese Option nicht; 2. zu berechnen, ob die aktuelle Codeeinheit ein hohes Surrogat gefolgt von einem niedrigen Surrogat zum Inkrement von 1 oder 2 ist, aber dies würde annehmen, dass die Grammatik immer wohlgeformte UTF-16-Surrogate hat, ohne die Möglichkeit, Grammatiken mit einsamen Surrogaten zu schreiben ( siehe nächsten Punkt) und dies ist auch eine Leistungseinbuße für reine BMP-Parser.

Da stellt sich die Frage nach einsamen Leihmüttern (hohe Leihmutter ohne niedrige Leihmutter danach oder niedrige Leihmutter ohne hohe Leihmutter davor). Meine Meinung dazu ist, dass eine Regex-Klasse ausschließlich sein sollte: entweder mit einsamen Ersatzzeichen entweder mit wohlgeformten UTF-16-Unicode-Zeichen (BMP oder einem hohen Ersatzzeichen gefolgt von einem niedrigen Ersatzzeichen), sonst besteht die Gefahr, dass Grammatikautoren nichts davon wissen UTF-16-Feinheiten vermischen beides und verstehen das Ergebnis nicht, und Grammatikautoren, die UTF-16-Surrogate selbst verwalten möchten, können es mit PEG-Regeln tun, um Verbindungen zwischen bestimmten hohen und niedrigen Surrogate zu beschreiben. Ich schlage vor, einen Besucher hinzuzufügen, der diese Regel während der Parser-Generierung durchsetzt.

Zusammenfassend lässt sich sagen, dass es wahrscheinlich einfacher ist, die Frage nach einsamen Surrogaten in PEG als in Regex zu handhaben, da der PEG-Parser immer voranschreitet, sodass entweder die nächste Codeeinheit erkannt wird, dies nicht der Fall ist, im Gegensatz zu Regexes, bei denen möglicherweise ein Zurückverfolgen verbunden sein könnte oder Trennen Sie ein hohes Ersatzzeichen von einem niedrigen Ersatzzeichen und ändern Sie folglich die Anzahl der übereinstimmenden Unicode-Zeichen usw.

Die PR für ES6-Syntax für astrale Unicode-Zeichen ist #651 basierend auf #616 und die Entwicklung für Klassen ist https://github.com/Seb35/pegjs/commit/0d33a7a4e13b0ac7c55a9cfaadc16fc0a5dd5f0c implementiert die Punkte 2 und 3 in meinem obigen Kommentar und nur ein schneller Hack für Cursor-Inkrement (Punkt 4) und vorerst nichts für den Regelpunkt . (Punkt 5).

Meine aktuelle Entwicklung zu diesem Thema ist größtenteils abgeschlossen, die fortgeschrittenere Arbeit befindet sich in https://github.com/Seb35/pegjs/tree/dev-astral-classes-final. Alle fünf oben genannten Punkte werden behandelt und das globale Verhalten versucht, JS-Regexes in Bezug auf die Randfälle (und davon gibt es viele) nachzuahmen.

Das globale Verhalten wird durch die Option unicode gesteuert, ähnlich dem Flag unicode in JS-Regexes: Der Cursor wird je nach aktuellem Text um 1 Unicode-Zeichen (1 oder 2 Codeeinheiten) erhöht (zB [^a] stimmt mit dem Text "💯" überein und der Cursor wird um 2 Codeeinheiten erhöht). Wenn die Option unicode falsch ist, wird der Cursor immer um 1 Codeeinheit erhöht.

Was die Eingabe angeht, bin ich mir nicht sicher, ob wir PEG.js genauso gestalten wie JS-Regexe: Sollen wir [\u{1F4AD}-\u{1F4AF}] (entspricht [\uD83D\uDCAD-\uD83D\uDCAF] ) in der Grammatik im Nicht-Unicode-Modus autorisieren ? Wir können zwischen "Eingabe Unicode" und "Ausgabe Unicode" unterscheiden:

  • Bei input Unicode geht es darum, alle Unicode-Zeichen in Zeichenklassen zu autorisieren (die intern entweder als feste 2-Code-Einheiten oder als feste 1-Code-Einheit berechnet werden)
  • Ausgabe Unicode ist das Cursorinkrement des resultierenden Parsers: 1 Codeeinheit oder 1 Unicode-Zeichen für die Regeln 'Punkt' und 'Invertierte Zeichenklasse' – die einzigen Regeln, bei denen die Zeichen nicht explizit aufgelistet sind und wir eine Entscheidung aus der Grammatik benötigen Autor

Persönlich würde ich es vorziehen, Unicode-Eingabe zu autorisieren, entweder dauerhaft oder mit einer Option mit einem Standardwert von true da kein erheblicher Overhead entsteht und dies diese Möglichkeit standardmäßig für alle ermöglichen würde, aber der Ausgabe-Unicode sollte false bleiben

In Bezug auf dieses Problem im Allgemeinen (und über die Ausgabe von Unicode, die standardmäßig auf false ), sollten wir bedenken, dass es bereits möglich ist, in unseren Grammatiken Unicode-Zeichen zu kodieren, um den Preis des Verständnisses der Funktionsweise von 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] )

Ein Grammatikautor, der sowohl einen schnellen Parser als auch Unicode-Zeichen in bestimmten Teilen seiner Grammatik erkennen möchte, kann eine solche Regel verwenden. Folglich geht es bei diesem Thema lediglich darum, die Unicode-Verwaltung zu vereinfachen, ohne in UTF-16-Interna einzutauchen.


Bei der Implementierung habe ich bei meinem ersten Versuch überlegt, dass der Grammatiktext in Unicode-Zeichen kodiert ist und die 'Punkt'-Regel des PEG.js-Parsers Unicode-Zeichen erkennt. Der zweite und letzte Versuch hat dies rückgängig gemacht (die Punktregel ist immer 1 Codeeinheit für schnelleres Parsen) und es gibt einen kleinen Algorithmus in der Besucher-Prepare-unicode-classes.js, um geteilte Unicode-Zeichen in Zeichenklassen zu rekonstruieren (zB [\uD83D\uDCAD-\uD83D\uDCAF] wird syntaktisch als [ "\uD83D", [ "\uDCAD", "\uD83D" ], "\uDCAF" ] erkannt und dieser Algorithmus wandelt dies in [ [ "\uD83D\uDCAD", "\uD83D\uDCAF" ] ] . Ich hatte vor, dies in der Grammatik selbst zu schreiben, aber es wäre lang gewesen und vor allem gibt es mehrere Möglichkeiten, die Zeichen zu codieren ("💯", "uD83DuDCAF", "u{1F4AF}"), so dass es einfacher ist, es zu schreiben bei einem Besucher.

Ich habe im zweiten Versuch zwei Opcodes hinzugefügt:

  • MATCH_ASTRAL ähnlich wie MATCH_ANY, aber passend zu einem Unicode-Zeichen (input.charCodeAt(currPos) & 0xFC00) === 0xD800 && input.length > currPos + 1 && (input.charCodeAt(currPos+1) & 0xFC00) === 0xDC00
  • MATCH_CLASS2 ist MATCH_CLASS sehr ähnlich, entspricht aber den nächsten beiden Codeeinheiten statt nur einer classes[c].test(input.substring(currPos, currPos+2)
    Dann, je nachdem, ob wir ein 2-Code-Unit-Zeichen oder ein 1-Code-Unit-Zeichen finden, wird der Cursor mit dem Opcode ACCEPT_N um 1 oder 2 Code-Einheiten erhöht, und die Zeichenklassen werden in zwei geteilt Regexes fester Länge (1 oder 2 Codeeinheiten).

Ich habe einige Optimierungen mit der "Match"-Funktion vorgenommen, die während der Generierung die "toten Code"-Pfade je nach Modus (Unicode oder nicht) und Zeichenklasse eliminiert.

Beachten Sie auch, dass Regexes in dieser Implementierung immer positiv sind: Invertierte Regexes geben das Gegenteil zurück, was zum Bytecode führt. Dies war einfacher, Randfälle um Leihmütter zu vermeiden.

Ich habe einige Dokumentationen geschrieben, aber ich werde wahrscheinlich noch mehr hinzufügen (vielleicht eine Anleitung, um schnell die Details der Option(en) Unicode und der Schnipsel mit der hausgemachten Unicode-Punktregel zu erklären). Ich werde Tests hinzufügen, bevor ich es als PR einreiche.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen