Pegjs: Dukungan Unicode penuh, yaitu untuk codepoint di luar BMP

Dibuat pada 22 Okt 2018  ·  15Komentar  ·  Sumber: pegjs/pegjs

Jenis masalah

  • Laporan Bug: ya
  • Permintaan Fitur: agak
  • Pertanyaan: tidak
  • Bukan masalah: tidak

Prasyarat

  • Bisakah Anda mereproduksi masalah?: ya
  • Apakah Anda mencari masalah repositori?: ya
  • Apakah Anda memeriksa forum?: ya
  • Apakah Anda melakukan pencarian web (google, yahoo, dll)?: ya

Keterangan

JavaScript, tanpa beberapa boilerplate khusus , tidak dapat menangani karakter/titik kode Unicode dengan benar di luar BMP , yaitu, yang pengkodeannya membutuhkan lebih dari 16 bit.

Batasan ini tampaknya terbawa ke PEG.js, seperti yang ditunjukkan pada contoh di bawah ini.

Secara khusus, saya ingin dapat menentukan rentang seperti [\u1D400-\u1D419] (yang saat ini berubah menjadi [ᵀ0-ᵁ9] ) atau yang setara [𝐀-𝐙] (yang menampilkan "Rentang karakter tidak valid" kesalahan). (Dan menggunakan notasi ES6 yang baru [\u{1D400}-\u{1D419}] menghasilkan kesalahan berikut: SyntaxError: Expected "!", "$", "&", "(", ".", character class, comment, end of line, identifier, literal, or whitespace but "[" found. .)

Mungkinkah ada cara untuk membuat ini berfungsi yang tidak memerlukan perubahan pada PEG.js?

Langkah-langkah untuk Reproduksi

  1. Hasilkan parser dari tata bahasa yang diberikan di bawah ini.
  2. Gunakan itu untuk mencoba mengurai sesuatu yang seolah-olah sesuai.

Contoh kode:

tata bahasa ini:

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

Perilaku yang diharapkan:

Pengurai yang dihasilkan dari tata bahasa yang diberikan berhasil diurai, misalnya, "𝐀𝐁𝐂".

Perilaku sebenarnya:

Kesalahan penguraian: Line 1, column 1: Expected [ᵀ0-ᵁ9] but " (Atau, saat menghapus komentar pada aturan lain, kesalahan "Rentang karakter tidak valid".)

Perangkat lunak

  • PEG.js: 0.10.0
  • Node.js: Tidak berlaku.
  • NPM atau Benang: Tidak berlaku.
  • Browser: Semua browser yang saya uji.
  • OS: macOS Mojave.
  • Editor: Semua editor sudah saya uji.
feature need-help task

Semua 15 komentar

Sejujurnya, selain memperbarui dukungan Unicode untuk pengurai tata bahasa PEG.js dan contoh JavaScript , saya hanya memiliki sedikit atau tidak ada pengetahuan tentang Unicode, jadi saat ini saya tidak dapat memperbaiki masalah ini (dinyatakan dengan jelas di kedua tata bahasa: _Non -BMP karakter benar-benar diabaikan_).

Untuk saat ini, sambil mengerjakan proyek pribadi dan yang terkait dengan pekerjaan yang lebih mendesak (termasuk _PEG.js 0.x_), saya akan terus menunggu seseorang yang lebih memahami Unicode untuk menawarkan beberapa PR , atau akhirnya menyelesaikannya setelah _PEG. js v1_, maaf sobat.

FYI, pasangan pengganti tampaknya berfungsi. Tata bahasa
start = result:[\uD83D\uDCA9]+ {return result.join('')}
mem-parsing yaitu u+1F4A9. Perhatikan result.join('') menempatkan pasangan pengganti kembali bersama-sama, jika tidak, Anda mendapatkan ['\uD83D','\uDCA9'] alih-alih saputangan. Rentang akan bermasalah.
Lebih lanjut tentang pasangan pengganti: https://en.wikipedia.org/wiki/UTF-16#U +010000_to_U+10FFFF

Ini sama sekali tidak menggantikan apa yang diminta OP.

@drewnolan Terima kasih atas perhatiannya 👍

Sayangnya tata bahasa itu juga mem-parsing \uD83D\uD83D .

Untuk orang lain yang mengalami masalah ini: Saya beruntung karena saya hanya perlu menangani sebagian kecil titik kode di luar BMP, jadi saya akhirnya memetakannya ke area penggunaan pribadi BMP sebelum menguraikan dan membalikkan pemetaan ini segera setelahnya .

Solusi ini jelas penuh dengan masalah dalam kasus umum, tetapi bekerja dengan baik di domain masalah saya.

@futagoza - Saya akan melakukan yang terbaik untuk menjelaskan. Anda menghadapi beberapa masalah di sini.

  1. Unicode memiliki penyandian, notasi, dan rentang

    1. Di sini, hal yang penting adalah "rentang" - karakter mana yang didukung - dan "notasi" - cara penulisannya

    2. Ini berubah seiring waktu. Unicode secara berkala menambahkan karakter baru, seperti ketika mereka menambahkan emoji, atau not musik

  2. Unicode memiliki "notasi." Ini adalah hal-hal seperti utf-16 , ucs4 , dan lain-lain. Ini adalah cara codepoints , yang merupakan data yang dimaksud, dikodekan sebagai byte. utf-16-le dengan contoh memungkinkan Anda menyandikan sebagian besar huruf sebagai pasangan dua byte yang disebut code units , tetapi menggunakan grup unit kode untuk mengekspresikan karakter bernilai tinggi hingga 0x10ffff .

    1. Pemahaman kunci: itu tidak cukup tinggi. Banyak hal menarik, seperti emoji, potongan besar Cina bersejarah, dan pertanyaan dasar posting ini (karakter matematika papan tulis) berada di atas garis itu



      1. Konsorsium ISO dan Unicode sudah jelas: mereka tidak pernah memperbaikinya . Jika Anda ingin karakter yang lebih tinggi, gunakan penyandian yang lebih besar dari utf-16.



    2. Pemahaman kunci #2: Javascript sialan secara resmi adalah



      1. Ini berarti ada karakter unicode (banyak dari mereka) yang tidak dapat diwakili oleh tipe string Javascript


      2. OP meminta Anda untuk memperbaikinya


      3. Ini mungkin tetapi tidak mudah - Anda harus menerapkan algoritme penguraian Unicode, yang terkenal sebagai sarang tikus



Saya menginginkan ini juga, tetapi secara realistis, ini tidak akan terjadi

sial, seseorang melakukan penggantian parser string penuh hampir setahun yang lalu , dan mereka mengenali overhead, jadi mereka membiarkan kami menggunakan string JS standar biasanya

KENAPA TIDAK DIGABUNG?

@StoneCypher Saya suka api di hatimu! Tetapi mengapa menyulitkan pengelola saat ini? Tidak ada yang berutang apa pun. Mengapa tidak memelihara garpu Anda sendiri?

Tidak ada pengelola saat ini. Orang yang mengambil alih PEG tidak pernah melepaskan apapun. Dia mengerjakan minor berikutnya selama tiga tahun, lalu mengatakan dia tidak suka tampilannya, membuang semua peg.js , dan memulai dari awal dari sesuatu yang dia tulis dari awal dalam bahasa yang berbeda, dengan bahasa yang tidak sesuai. AST.

Alat ini telah kehilangan setengah basis penggunanya menunggu tiga tahun pada orang ini untuk melakukan perbaikan satu baris yang ditulis orang lain, seperti dukungan modul es6, dukungan TypeScript, dukungan panah, unicode yang diperluas, dan lain-lain.

Ada selusin orang yang memintanya untuk bergabung dan dia terus berkata "tidak, ini adalah proyek hobi saya sekarang dan saya tidak suka apa itu"

Banyak orang memiliki perusahaan berdasarkan parser ini. Mereka benar-benar kacau.

Pria ini berjanji untuk menjadi pemelihara alat yang sangat penting, dan tidak melakukan perawatan apa pun. Saatnya membiarkan orang lain menjaga perpustakaan ini dalam performa yang baik sekarang.

Mengapa tidak memelihara garpu Anda sendiri?

Saya memiliki selama tiga tahun sekarang. peg memiliki hampir sepertiga dari pelacak masalah yang diperbaiki.

Saya harus mengkloningnya, mengganti namanya, dan membuat garpu baru untuk memperbaiki masalah ukuran untuk mencoba mengkomitnya, karena milik saya terlalu banyak melayang

Sudah waktunya bagi semua orang untuk menerima perbaikan ini, serta yang telah ada di pelacak sejak 2017.

Orang ini tidak mempertahankan pasak; dia membiarkannya mati.

Saatnya untuk perubahan.

@drewnolan - jadi, saya tidak yakin apakah ini menarik atau tidak, tetapi, pasangan pengganti sebenarnya tidak berfungsi. Hanya saja, secara kebetulan, mereka biasanya melakukannya.

Untuk memahami masalah mendasar, Anda harus memikirkan pola bit level penyandian, bukan level representasi.

Artinya, jika Anda memiliki nilai unicode 240, kebanyakan orang akan berpikir "Oh, maksudnya 0b1111 0000 ." Tapi sebenarnya, Unicode tidak mewakili 240; lebih dari 127 diwakili oleh dua byte, karena bit teratas adalah flag, bukan bit nilai. Jadi 240 di Unicode sebenarnya adalah 0b0000 0001 0111 0000 dalam penyimpanan (kecuali dalam utf-7, yang nyata dan bukan salah ketik, di mana hal-hal menjadi sangat aneh. Dan ya, saya tahu Wikipedia mengatakan itu tidak digunakan. Wikipedia salah . Itu SMS yang dikirim; itu mungkin pengkodean karakter paling umum berdasarkan total lalu lintas.)

Jadi inilah masalahnya.

Jika Anda menulis satu byte STUV WXYZ, lalu di utf16, dari data ucs4, jika barang Anda terpotong menjadi dua, cukup sering Anda bisa menjepitnya kembali.

Satu kali dalam 128 Anda tidak bisa, untuk karakter yang secara asli lebih dari dua byte penyandian. (Kedengarannya seperti angka yang sangat spesifik, bukan?)

Karena ketika bit teratas di posisi byte kedua sedang digunakan, memotongnya menjadi dua akan menambahkan nol di mana yang seharusnya menjadi satu. Menjepitnya kembali berdampingan karena data biner tidak menghapus konsep nilai lagi. Nilai yang didekodekan tidak setara dengan nilai yang disandikan, dan Anda menambahkan dekode, bukan pengkodean.

Kebetulan sebagian besar emoji berada di luar kisaran ini. Namun, sebagian besar bahasa tidak termasuk, termasuk bahasa Cina yang langka, sebagian besar matematika, dan simbol musik.

Memang, pendekatan Anda cukup baik untuk hampir semua emoji, dan untuk setiap bahasa manusia yang cukup umum untuk digunakan oleh Unicode 6, yang merupakan peningkatan besar atas status quo

Tetapi PR ini benar-benar harus digabungkan, setelah cukup diuji baik untuk kebenaran dan terhadap masalah kinerja yang tidak terduga (ingat, masalah kinerja unicode adalah mengapa php mati)

Tampaknya ekspresi . (dot character) juga membutuhkan mode Unicode. Membandingkan:

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

Keluaran:

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

Saya baru-baru ini mengerjakan ini, menggunakan #616 sebagai dasar dan memodifikasinya untuk menggunakan sintaks ES6 \u{hhhhhhh} , saya akan membuat PR dalam beberapa jam.

Menghitung rentang regex UTF-16 yang dibagi oleh pengganti agak rumit dan saya menggunakan https://github.com/mathiasbynens/regenerate untuk ini; ini akan menjadi dependensi pertama dari paket pegjs, saya harap ini mungkin (ada juga polyfill untuk properti Unicode yang dapat ditambahkan sebagai dependensi, lihat #648). Lihat Wikipedia jika Anda tidak tahu pengganti UTF-16 .

Untuk membuat PEG.js kompatibel dengan seluruh Unicode, ada berbagai tingkatan:

  1. Tambahkan sintaks untuk menyandikan karakter Unicode di atas BMP, diperbaiki oleh #616 atau versi sintaks ES6 saya,
  2. Kenali string konstan, yang disediakan langsung oleh poin sebelumnya,
  3. Perbaiki pelaporan SyntaxError agar mungkin menampilkan 1 atau 2 unit kode untuk menampilkan karakter Unicode yang sebenarnya,
  4. Hitung secara akurat kelas regex untuk BMP dan/atau poin kode astral - ini saja tidak berfungsi, lihat poin berikutnya,
  5. Kelola kenaikan kursor karena kelas regex sekarang dapat berupa (1), (2), atau (1 atau 2 tergantung pada waktu proses), lihat detail di bawah,
  6. Terapkan aturan titik . untuk menangkap 1 atau 2 unit kode.

Untuk sebagian besar poin, kami dapat mengatur agar kompatibel ke belakang dan menghasilkan parser yang sangat mirip dengan yang lebih lama, kecuali untuk poin 5 karena hasil penguraian dapat bergantung jika aturan titik menangkap satu atau dua unit kode. Untuk ini saya mengusulkan untuk menambahkan opsi runtime untuk membiarkan pilihan pengguna antara dua atau tiga pilihan:

  1. Aturan titik hanya menangkap unit kode BMP,
  2. Aturan titik menangkap titik kode Unicode (1 atau 2 unit kode),
  3. Aturan titik menangkap titik kode Unicode atau pengganti tunggal.

Kelas regex dapat dianalisis secara statis selama pembuatan parser untuk memeriksa apakah mereka memiliki panjang yang tetap (dalam jumlah unit kode). Ada 3 kasus: 1. hanya BMP atau satu unit kode, atau 2. hanya dua unit kode, atau 3. satu atau dua unit kode tergantung pada runtime. Untuk saat ini bytecode mengasumsikan kelas regex selalu satu unit kode ( lihat di sini ). Dengan analisis statis, kita dapat mengubah parameter instruksi bytecode ini menjadi 1 atau 2 untuk dua kasus pertama. Tetapi untuk kasus ketiga, saya kira instruksi bytecode baru harus ditambahkan, pada saat runtime, dapatkan jumlah unit kode yang cocok dan tingkatkan kursor yang sesuai. Opsi lain tanpa instruksi bytecode baru adalah: 1. untuk selalu menghitung jumlah unit kode yang cocok tetapi ini adalah penalti kinerja selama penguraian untuk parser khusus BMP jadi saya tidak menyukai opsi ini; 2. untuk menghitung jika unit kode saat ini adalah pengganti tinggi diikuti oleh pengganti rendah untuk kenaikan 1 atau 2, tetapi ini akan mengasumsikan tata bahasa selalu pengganti UTF-16 yang terbentuk dengan baik tanpa kemungkinan untuk menulis tata bahasa dengan pengganti tunggal ( lihat poin berikutnya) dan ini juga merupakan penalti kinerja untuk parser khusus BMP.

Ada pertanyaan tentang pengganti tunggal (pengganti tinggi tanpa pengganti rendah setelahnya, atau pengganti rendah tanpa pengganti tinggi sebelumnya). Pendapat saya tentang ini adalah bahwa kelas regex harus secara eksklusif: baik dengan pengganti tunggal baik dengan karakter Unicode UTF-16 yang terbentuk dengan baik (BMP atau pengganti tinggi diikuti oleh pengganti rendah), jika tidak, ada bahaya yang tidak disadari oleh penulis tata bahasa Seluk-beluk UTF-16 mencampur keduanya dan tidak memahami hasilnya, dan penulis tata bahasa yang ingin mengelola sendiri pengganti UTF-16 dapat melakukannya dengan aturan PEG untuk menggambarkan hubungan antara pengganti tinggi dan rendah tertentu. Saya mengusulkan untuk menambahkan pengunjung yang menerapkan aturan ini selama pembuatan parser.

Untuk menyimpulkan, mungkin lebih mudah untuk mengelola pertanyaan tentang pengganti tunggal di PEG daripada di regex karena parser PEG selalu maju, jadi unit kode berikutnya dikenali atau tidak, sebaliknya dari regex di mana mungkin beberapa backtracking dapat dikaitkan atau memisahkan pengganti tinggi dengan pengganti rendah dan akibatnya mengubah jumlah karakter Unicode yang cocok, dll.

Sintaks PR untuk ES6 untuk karakter astral Unicode adalah #651 berdasarkan #616 dan pengembangan untuk kelas adalah https://github.com/Seb35/pegjs/commit/0d33a7a4e13b0ac7c55a9cfaadc16fc0a5dd5f0c mengimplementasikan poin 2 dan 3 dalam komentar saya di atas, dan hanya retasan cepat untuk kenaikan kursor (poin 4) dan tidak ada untuk saat ini untuk aturan titik . (poin 5).

Perkembangan saya saat ini tentang masalah ini sebagian besar sudah selesai, pekerjaan yang lebih maju ada di https://github.com/Seb35/pegjs/tree/dev-astral-classes-final. Kelima poin yang disebutkan di atas diperlakukan dan perilaku global mencoba meniru regex JS mengenai kasus Edge (dan ada banyak dari mereka).

Perilaku global diatur oleh opsi unicode mirip dengan flag unicode di JS regex: kursor dinaikkan 1 karakter Unicode (1 atau 2 unit kode) tergantung pada teks aktual (mis. [^a] cocok dengan teks "💯" dan kursor bertambah 2 unit kode). Ketika opsi unicode salah, kursor selalu dinaikkan 1 unit kode.

Mengenai input, saya tidak yakin apakah kita mendesain PEG.js dengan cara yang sama seperti regex JS: haruskah kita mengotorisasi [\u{1F4AD}-\u{1F4AF}] (setara dengan [\uD83D\uDCAD-\uD83D\uDCAF] ) dalam tata bahasa dalam mode non-Unicode? Kita dapat membuat perbedaan antara "input Unicode" dan "output Unicode":

  • input Unicode adalah tentang mengotorisasi semua karakter Unicode dalam kelas karakter (yang dihitung secara internal sebagai unit 2-kode tetap atau unit-kode-satu tetap)
  • output Unicode adalah kenaikan kursor dari parser yang dihasilkan: 1 unit kode atau 1 karakter Unicode untuk aturan 'titik' dan 'kelas karakter terbalik' – satu-satunya aturan di mana karakter tidak dicantumkan secara eksplisit dan di mana kita memerlukan keputusan dari tata bahasa Pengarang

Secara pribadi saya kira saya lebih suka kita mengotorisasi input Unicode, baik secara permanen atau dengan opsi dengan default true karena tidak ada overhead yang signifikan dan ini akan memungkinkan kemungkinan ini untuk semua orang secara default, tetapi output Unicode harus tetap false karena kinerja parser yang dihasilkan lebih baik (selalu kenaikan kursor 1).

Mengenai masalah ini secara umum (dan tentang output Unicode default ke false ), kita harus ingat bahwa sudah mungkin untuk mengkodekan dalam tata bahasa kita karakter Unicode, dengan harga memahami fungsi 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] )

Jadi penulis tata bahasa yang menginginkan pengurai cepat dan dapat mengenali karakter Unicode di bagian tertentu tata bahasanya dapat menggunakan aturan tersebut. Akibatnya masalah ini hanyalah tentang menyederhanakan manajemen Unicode tanpa menyelami internal UTF-16.


Tentang implementasi, saya menganggap dalam upaya pertama saya bahwa teks tata bahasa dikodekan dalam karakter Unicode dan aturan 'titik' dari parser PEG.js mengenali karakter Unicode. Upaya kedua dan terakhir mengembalikan ini (aturan titik selalu 1 unit kode untuk penguraian lebih cepat) dan ada algoritme kecil di pengunjung prepare-unicode-classes.js untuk merekonstruksi karakter Unicode yang terpisah di kelas karakter (misalnya [\uD83D\uDCAD-\uD83D\uDCAF] secara sintaks dikenali sebagai [ "\uD83D", [ "\uDCAD", "\uD83D" ], "\uDCAF" ] dan algoritme ini mengubahnya menjadi [ [ "\uD83D\uDCAD", "\uD83D\uDCAF" ] ] ). Saya membayangkan untuk menulis ini dalam tata bahasa itu sendiri tetapi itu akan menjadi panjang dan yang lebih penting ada banyak cara untuk menyandikan karakter ("💯", "uD83DuDCAF", "u{1F4AF}"), jadi lebih mudah untuk menulisnya dalam seorang pengunjung.

Saya menambahkan dua opcode dalam upaya kedua:

  • MATCH_ASTRAL mirip dengan MATCH_ANY tetapi cocok dengan karakter Unicode (input.charCodeAt(currPos) & 0xFC00) === 0xD800 && input.length > currPos + 1 && (input.charCodeAt(currPos+1) & 0xFC00) === 0xDC00
  • MATCH_CLASS2 sangat mirip dengan MATCH_CLASS tetapi cocok dengan dua unit kode berikutnya, bukan hanya satu classes[c].test(input.substring(currPos, currPos+2)
    Kemudian, tergantung apakah kita mencocokkan karakter 2-kode-unit atau karakter 1-kode-unit, kursor dinaikkan dari 1 atau 2 unit kode dengan opcode ACCEPT_N , dan kelas karakter dibagi menjadi dua regex dengan panjang tetap (1 atau 2 unit kode).

Saya melakukan beberapa optimisasi dengan fitur "cocok", menghilangkan selama pembuatan jalur "kode mati" tergantung pada mode (Unicode atau tidak) dan kelas karakter.

Perhatikan juga bahwa regex selalu positif dalam implementasi ini: regex terbalik mengembalikan kebalikan yang menghasilkan bytecode. Ini lebih mudah untuk menghindari kasus tepi di sekitar pengganti.

Saya menulis beberapa dokumentasi tetapi saya mungkin akan menambahkan lebih banyak lagi (mungkin panduan untuk dengan cepat menjelaskan detail opsi unicode dan cuplikan dengan aturan titik Unicode buatan sendiri). Saya akan menambahkan tes sebelum mengirimkannya sebagai PR.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat