Pegjs: Полная поддержка Unicode, а именно для кодовых точек вне BMP

Созданный на 22 окт. 2018  ·  15Комментарии  ·  Источник: pegjs/pegjs

Тип проблемы

  • Отчет об ошибке: да
  • Запрос функции: вроде
  • Вопрос: нет
  • Не проблема: нет

Предпосылки

  • Можете ли вы воспроизвести проблему ?: да
  • Вы искали проблемы в репозитории ?: да
  • Вы проверяли форумы ?: да
  • Вы выполняли поиск в Интернете (google, yahoo и т. Д.) ?: да

Описание

JavaScript без какого-либо специального шаблона не может правильно работать с символами / кодовыми точками Unicode за пределами BMP , т. Е. С теми, для кодирования которых требуется более 16 бит.

Это ограничение распространяется и на PEG.js, как показано в примере ниже.

В частности, я хотел бы иметь возможность указывать диапазоны, такие как [\u1D400-\u1D419] (который в настоящее время превращается в [ᵀ0-ᵁ9] ) или эквивалентный [𝐀-𝐙] (который выдает "Недопустимый диапазон символов" ошибка). (А использование новой нотации ES6 [\u{1D400}-\u{1D419}] приводит к следующей ошибке: SyntaxError: Expected "!", "$", "&", "(", ".", character class, comment, end of line, identifier, literal, or whitespace but "[" found. .)

Есть ли способ сделать эту работу, не требуя изменений в PEG.js?

Действия по воспроизведению

  1. Сгенерируйте синтаксический анализатор из грамматики, приведенной ниже.
  2. Используйте его, чтобы попытаться разобрать что-то якобы соответствующее.

Пример кода:

Эта грамматика:

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

Ожидаемое поведение:

Синтаксический анализатор, созданный на основе данной грамматики, успешно разбирает, например, «𝐀𝐁𝐂».

Фактическое поведение:

Ошибка синтаксического анализа: Line 1, column 1: Expected [ᵀ0-ᵁ9] but " (Или, при раскомментировании другого правила, ошибка «Недопустимый диапазон символов».)

Программное обеспечение

  • PEG.js: 0.10.0
  • Node.js: не применимо.
  • NPM или пряжа: не применимо.
  • Браузер: все браузеры, которые я тестировал.
  • ОС: macOS Mojave.
  • Редактор: Все редакторы, которые я тестировал.
feature need-help task

Все 15 Комментарий

Честно говоря, помимо обновления поддержки Unicode для парсера грамматики PEG.js и примера JavaScript , я практически ничего не знаю о Unicode, поэтому в настоящее время я не могу исправить эту проблему (это четко указано в обеих грамматических документах: _Non -BMP символы полностью игнорируются_).

На данный момент, работая над более срочными проектами, связанными с персоналом и работой (включая _PEG.js 0.x_), я буду ждать, пока кто-нибудь, кто лучше понимает Unicode, предложит PR 😆 или, в конце концов, дойдет до него после _PEG. js v1_, извини, дружище.

К вашему сведению, суррогатные пары, похоже, работают. Грамматика
start = result:[\uD83D\uDCA9]+ {return result.join('')}
разбирает 💩, который равен u + 1F4A9. Обратите внимание, что result.join ('') объединяет суррогатную пару вместе, иначе вы получите ['\uD83D','\uDCA9'] вместо hankey. Диапазоны были бы проблематичными.
Подробнее о суррогатных парах: https://en.wikipedia.org/wiki/UTF-16#U + 010000_to_U + 10FFFF

Это ни в коем случае не заменяет то, что просил OP.

@drewnolan Спасибо за внимание 👍

К сожалению, эта грамматика также анализирует \uD83D\uD83D .

Для других, кто столкнулся с этой проблемой: мне повезло в том, что мне нужно обрабатывать только небольшое подмножество кодовых точек за пределами BMP, поэтому я в конечном итоге сопоставил их с областью частного использования BMP перед синтаксическим анализом и инвертировал это сопоставление сразу после .

Это решение явно чревато проблемами в общем случае, но оно хорошо работает в моей проблемной области.

@futagoza - Я постараюсь объяснить. Здесь вы столкнулись с несколькими проблемами.

  1. Unicode имеет кодировки, обозначения и диапазоны

    1. Здесь важны «диапазоны» - какие символы поддерживаются - и «обозначения» - как они записываются.

    2. Со временем они меняются. Unicode периодически добавляет новые символы, например, когда они добавляли смайлики или музыкальные ноты.

  2. В Юникоде есть «обозначения». Это такие вещи, как utf-16 , ucs4 и т. Д. Таким образом, codepoints , которые являются предполагаемыми данными, кодируются как байты. utf-16-le качестве примера позволяет вам кодировать большинство букв в виде двухбайтовых пар, называемых code units , но использовать группы кодовых единиц для выражения символов высокого значения до 0x10ffff .

    1. Ключевое понимание: этого недостаточно. Множество интересных вещей, таких как эмодзи, большие фрагменты исторического китайского языка и основной вопрос этого поста (математические символы на доске) находятся над этой строкой.



      1. Консорциум ISO и Unicode дал понять: они никогда не исправят это . Если вам нужны более высокие символы, используйте более крупную кодировку, чем utf-16.



    2. Ключевое понимание # 2: гребаный javascript utf16



      1. Это означает, что существуют символы Юникода (их много), которые строковый тип Javascript не может представлять.


      2. OP просит вас исправить это


      3. Это возможно, но непросто - вам придется реализовать алгоритм синтаксического анализа Unicode, который, как известно, является крысиным гнездом.



Я тоже хочу этого, но реально этого не произойдет

Черт возьми, кто-то совершил полную замену парсера строк почти год назад , и они признали накладные расходы, поэтому они позволяют нам обычно использовать стандартные строки JS

ПОЧЕМУ ЭТО НЕ ОБЪЕДИНЯЕТСЯ

@StoneCypher, я люблю огонь в твоем сердце! Но зачем беспокоить текущего сопровождающего? Никто ничего не должен. Почему бы не сохранить собственную вилку?

Текущего сопровождающего нет. Человек, который захватил PEG, никогда ничего не выпускал. Он работал над следующим второстепенным три года, затем сказал, что ему не нравится, как он выглядит, выбросил все peg.js и начал заново с того, что написал с нуля на другом языке, с несовместимым AST.

Инструмент потерял половину своей пользовательской базы, ожидая трех лет от этого человека фиксации однострочных исправлений, написанных другими людьми, таких как поддержка модуля es6, поддержка машинописного текста, поддержка стрелок, расширенный юникод и т. Д.

Десяток человек просят его объединиться, и он все время говорит: «Нет, это мой хобби-проект, и мне не нравится то, что он из себя представляет».

У многих людей есть компании, основанные на этом парсере. Они полностью облажались.

Этот человек обещал быть специалистом по сопровождению чрезвычайно важного инструмента и не занимался обслуживанием. Пришло время позволить кому-то другому сохранить эту библиотеку в хорошем состоянии.

Почему бы не сохранить собственную вилку?

У меня уже три года. В моем peg исправлена ​​почти треть средств отслеживания проблем.

Мне пришлось клонировать его, переименовать и создать новую вилку, чтобы исправить проблему с размером, чтобы попытаться зафиксировать его, потому что мой слишком сильно дрейфовал.

Всем остальным пора получить эти исправления, а также те, которые сидят в трекере с 2017 года.

Этот парень не поддерживает привязку; он позволяет ему умереть.

Пришло время перемен.

@drewnolan - я не уверен, интересно это или нет, но суррогатные пары на самом деле не работают. Просто по совпадению они обычно так и поступают.

Чтобы понять основную проблему, вы должны думать о битовом шаблоне уровня кодирования, а не об уровне представления.

То есть, если у вас есть значение юникода 240, большинство людей подумают: «О, он имеет в виду 0b1111 0000 ». Но на самом деле это не то, как Unicode представляет 240; более 127 представлено двумя байтами, потому что верхний бит является флагом, а не битом значения. Итак, 240 в Unicode на самом деле 0b0000 0001 0111 0000 в хранилище (за исключением utf-7, который является реальным, а не опечаткой, где все становится очень странным. И да, я знаю, что Википедия говорит, что она не используется. Википедия неверна . Это то, через что отправляется SMS; это может быть наиболее распространенная кодировка символов в общем трафике.)

Итак, вот в чем проблема.

Если вы напишете байтовый STUV WXYZ, то в utf16 из данных ucs4, если ваша вещь будет разрезана пополам, довольно часто вы можете просто скрепить ее вместе.

Один раз из 128 вы не можете, для символов с исходной кодировкой более двух байтов. (Звучит как очень конкретное число, не так ли?)

Потому что, когда используется верхний бит во второй позиции байта, разрезание его пополам добавит ноль там, где он должен был быть единицей. Соединение их вместе бок о бок, поскольку двоичные данные снова не удаляют концепцию ценности. Декодированное значение не эквивалентно закодированному значению, и вы добавляете декодирование, а не кодирование.

Так получилось, что большинство эмодзи находятся за пределами этого диапазона. Однако большие куски нескольких языков - нет, включая редкие китайские, большинство математических и музыкальных символов.

Конечно, ваш подход достаточно хорош почти для всех эмодзи и для каждого человеческого языка, достаточно распространенного, чтобы использовать Unicode 6, что является огромным улучшением по сравнению с существующим положением вещей.

Но этот PR действительно следует объединить, как только он будет достаточно протестирован как на правильность, так и на непредвиденные проблемы с производительностью (помните, что проблемы с производительностью Unicode - это причина, по которой php умер)

Кажется, что для выражения . (dot character) также требуется режим Unicode. Сравнивать:

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

Выход:

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

Я недавно работал над этим, используя # 616 в качестве основы и модифицируя его для использования синтаксиса ES6 \u{hhhhhhh} , я создам PR через несколько часов.

Вычисление диапазонов регулярных выражений UTF-16, разделенных суррогатами, немного сложно, и для этого я использовал https://github.com/mathiasbynens/regenerate ; это будет первая зависимость пакета pegjs, я надеюсь, что это возможно (есть также полифиллы для свойств Unicode, которые можно добавить как зависимость, см. # 648). См. Википедию, если вы не знаете суррогаты UTF-16 .

Чтобы сделать PEG.js совместимым со всем Unicode, существуют различные уровни:

  1. Добавьте синтаксис для кодирования символов Unicode над BMP, исправленный # 616 или моей версией синтаксиса ES6,
  2. Распознавать постоянные строки, непосредственно предоставленные предыдущим пунктом,
  3. Исправьте сообщение SyntaxError, чтобы, возможно, отображать 1 или 2 единицы кода для отображения реального символа Unicode,
  4. Точно вычислите класс регулярного выражения для BMP и / или астральных кодовых точек - это само по себе не работает, см. Следующий пункт,
  5. Управляйте приращением курсора, потому что класс регулярного выражения теперь может быть (1), (2) или (1 или 2 в зависимости от времени выполнения), см. Подробности ниже,
  6. Реализуйте точку правила . чтобы перехватить 1 или 2 единицы кода.

Для большинства точек нам удается быть обратно совместимыми и генерировать синтаксические анализаторы, очень похожие на старые, за исключением пункта 5, потому что результат синтаксического анализа может зависеть от того, захватывает ли точечное правило одну или две единицы кода. Для этого я предлагаю добавить параметр времени выполнения, чтобы пользователь мог выбирать между двумя или тремя вариантами:

  1. Точечное правило захватывает только кодовую единицу BMP,
  2. Точечное правило фиксирует кодовую точку Unicode (1 или 2 кодовых единицы),
  3. Правило точки захватывает кодовую точку Unicode или единственный суррогат.

Класс Regex можно анализировать статически во время генерации парсера, чтобы проверить, имеют ли они фиксированную длину (в количестве единиц кода). Есть 3 случая: 1. только BMP или одна кодовая единица, или 2. только две кодовых единицы, или 3. одна или две кодовых единицы в зависимости от времени выполнения. На данный момент байт-код предполагает, что класс регулярного выражения всегда является одной единицей кода ( см. Здесь ). С помощью статического анализа мы могли бы изменить этот параметр этой инструкции байт-кода на 1 или 2 для первых двух случаев. Но для третьего случая, я полагаю, следует добавить новую инструкцию байт-кода, чтобы во время выполнения получить количество согласованных единиц кода и соответственно увеличить курсор. Другие варианты без новой инструкции байт-кода: 1. всегда вычислять количество совпадающих единиц кода, но это снижает производительность во время синтаксического анализа для синтаксических анализаторов только BMP, поэтому мне не нравится этот вариант; 2. вычислить, является ли текущая единица кода высоким суррогатом, за которым следует низкий суррогат для приращения 1 или 2, но это предполагает, что грамматика всегда имеет правильно сформированные суррогаты UTF-16 без возможности писать грамматики с одиночными суррогатами ( см. следующий пункт), и это также снижает производительность парсеров только для BMP.

Возникает вопрос об одиноких суррогатах (высокий суррогат без низкого суррогата после него или низкий суррогат без высокого суррогата перед ним). Мое мнение по этому поводу состоит в том, что класс регулярных выражений должен быть исключительно: либо с одиночными суррогатами, либо с правильно сформированными символами Unicode UTF-16 (BMP или высокий суррогат, за которым следует низкий суррогат), иначе существует опасность, что авторы грамматики не знают о Тонкости UTF-16 смешивают оба и не понимают результата, а авторы грамматики, которые хотят управлять своими суррогатами UTF-16, могут сделать это с помощью правил PEG, чтобы описать связи между конкретными высокими и низкими суррогатами. Предлагаю добавить посетителя, который будет применять это правило при генерации парсера.

В заключение, вероятно, легче решить вопрос об одиночных суррогатах в PEG, чем в регулярном выражении, потому что синтаксический анализатор PEG всегда продвигается вперед, поэтому либо следующая единица кода распознается, либо нет, в отличие от регулярных выражений, где возможно некоторое обратное отслеживание может быть связано или отсоединить высокий суррогат от низкого суррогата и, следовательно, изменить количество совпадающих символов Unicode и т. д.

PR для синтаксиса ES6 для астрального символа Unicode - # 651 на основе # 616, а разработка для классов - https://github.com/Seb35/pegjs/commit/0d33a7a4e13b0ac7c55a9cfaadc16fc0a5dd5f0c, реализующая пункты 2 и 3 в моем комментарии выше, и только быстрый способ увеличения курсора (точка 4) и пока ничего для точки правила . (точка 5).

Моя текущая разработка по этой проблеме в основном завершена, более сложные работы находятся в https://github.com/Seb35/pegjs/tree/dev-astral-classes-final. Все пять упомянутых выше пунктов обрабатываются, и глобальное поведение пытается имитировать регулярные выражения JS в отношении крайних случаев (а их много).

Глобальное поведение регулируется параметром unicode аналогичным флагу unicode в регулярных выражениях JS: курсор увеличивается на 1 символ Unicode (1 или 2 единицы кода) в зависимости от фактического текста (например, [^a] соответствует тексту «💯», и курсор увеличивается на 2 единицы кода). Когда опция unicode ложна, курсор всегда увеличивается на 1 единицу кода.

Что касается ввода, я не уверен, что мы проектируем PEG.js так же, как регулярные выражения JS: должны ли мы авторизовать [\u{1F4AD}-\u{1F4AF}] (эквивалент [\uD83D\uDCAD-\uD83D\uDCAF] ) в грамматике в режиме, отличном от

  • входной Unicode касается авторизации всех символов Unicode в классах символов (которые вычисляются внутри как фиксированные 2-кодовые единицы или фиксированные 1-кодовые единицы)
  • output Unicode - это приращение курсора результирующего синтаксического анализатора: 1 единица кода или 1 символ Unicode для правил 'точка' и 'инвертированный класс символов' - единственные правила, в которых символы не указаны явно и где нам нужно решение из грамматики автор

Лично я предполагаю, что я бы предпочел, чтобы мы авторизовали входной Unicode либо навсегда, либо с опцией по умолчанию true поскольку нет значительных накладных расходов, и это позволило бы эту возможность для всех по умолчанию, но выходной Unicode должен оставаться false потому что производительность сгенерированных синтаксических анализаторов лучше (всегда приращение курсора 1).

Что касается этой проблемы в целом (и о выходе Unicode по умолчанию на false ), мы должны иметь в виду, что уже возможно кодировать в наших грамматиках символы Unicode, ценой понимания функционирования 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] )

Таким образом, автор грамматики, которому нужен как быстрый синтаксический анализатор, так и способность распознавать символы Unicode в определенных частях своей грамматики, может использовать такое правило. Следовательно, эта проблема просто связана с упрощением управления Unicode без углубления во внутреннее устройство UTF-16.


Что касается реализации, то в своей первой попытке я считал, что текст грамматики был закодирован в символах Unicode, а правило «точки» парсера PEG.js распознавало символы Unicode. Вторая и последняя попытка вернула это (правило точки - всегда 1 единица кода для более быстрого синтаксического анализа), и в файле prepare-unicode-classes.js посетителя есть небольшой алгоритм для восстановления разделенных символов Unicode в классах символов (например, [\uD83D\uDCAD-\uD83D\uDCAF] синтаксически распознается как [ "\uD83D", [ "\uDCAD", "\uD83D" ], "\uDCAF" ] и этот алгоритм преобразует это в [ [ "\uD83D\uDCAD", "\uD83D\uDCAF" ] ] ). Я планировал написать это в самой грамматике, но это заняло бы много времени, и, что более важно, существует несколько способов кодирования символов («💯», «uD83DuDCAF», «u {1F4AF}»), так что это проще написать в посетителе.

Во второй попытке я добавил два кода операции:

  • MATCH_ASTRAL аналогично MATCH_ANY, но соответствует символу Unicode (input.charCodeAt(currPos) & 0xFC00) === 0xD800 && input.length > currPos + 1 && (input.charCodeAt(currPos+1) & 0xFC00) === 0xDC00
  • MATCH_CLASS2 очень похож на MATCH_CLASS, но соответствует двум следующим кодовым единицам вместо одного classes[c].test(input.substring(currPos, currPos+2)
    Затем, в зависимости от того, сопоставляем ли мы символ с двумя кодовыми единицами или с 1 кодовой единицей, курсор увеличивается на 1 или 2 кодовых единицы с помощью кода операции ACCEPT_N , а классы символов разделяются на две регулярные выражения фиксированной длины (1 или 2 единицы кода).

Я сделал некоторые оптимизации с функцией "соответствия", исключив во время генерации пути "мертвого кода" в зависимости от режима (Unicode или нет) и класса символов.

Также обратите внимание, что в этой реализации регулярные выражения всегда положительны: инвертированные регулярные выражения возвращают противоположное, что приводит к получению байт-кода. Так было легче избежать крайних случаев, связанных с суррогатами.

Я написал некоторую документацию, но, вероятно, добавлю еще несколько (возможно, руководство для быстрого объяснения деталей опции (ов) Unicode и фрагментов с самодельным правилом точки Unicode). Я добавлю тесты перед отправкой в ​​качестве PR.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги