Pegjs: Suporte total a Unicode, ou seja, para pontos de código fora do BMP

Criado em 22 out. 2018  ·  15Comentários  ·  Fonte: pegjs/pegjs

Tipo de problema

  • Relatório de Bug: sim
  • Solicitação de recurso: meio
  • Questão: não
  • Não é um problema: não

Pré-requisitos

  • Você pode reproduzir o problema ?: sim
  • Você pesquisou os problemas do repositório ?: sim
  • Você verificou os fóruns ?: sim
  • Você fez uma pesquisa na web (google, yahoo, etc) ?: sim

Descrição

JavaScript é, sem algum padrão personalizado , incapaz de lidar adequadamente com caracteres / pontos de código Unicode fora do BMP , ou seja, aqueles cuja codificação requer mais de 16 bits.

Essa limitação parece ser transportada para o PEG.js, conforme mostrado no exemplo abaixo.

Em particular, gostaria de poder especificar intervalos como [\u1D400-\u1D419] (que atualmente se transforma em [ᵀ0-ᵁ9] ) ou equivalentemente [𝐀-𝐙] (que gera um "intervalo de caracteres inválido" erro). (E usando a notação ES6 mais recente [\u{1D400}-\u{1D419}] resulta no seguinte erro: SyntaxError: Expected "!", "$", "&", "(", ".", character class, comment, end of line, identifier, literal, or whitespace but "[" found. .)

Pode haver uma maneira de fazer esse trabalho que não exija alterações no PEG.js?

Passos para reproduzir

  1. Gere um analisador a partir da gramática fornecida a seguir.
  2. Use-o para tentar analisar algo aparentemente em conformidade.

Código de exemplo:

Esta gramática:

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

Comportamento esperado:

O analisador gerado a partir da gramática fornecida analisa com sucesso, por exemplo, "𝐀𝐁𝐂".

Comportamento real:

Um erro de análise: Line 1, column 1: Expected [ᵀ0-ᵁ9] but " (Ou, ao descomentar a outra regra, um erro "Intervalo de caracteres inválido".)

Programas

  • PEG.js: 0.10.0
  • Node.js: Não aplicável.
  • NPM ou Fios: Não aplicável.
  • Navegador: todos os navegadores que testei.
  • SO: macOS Mojave.
  • Editor: Todos os editores que testei.
feature need-help task

Todos 15 comentários

Para ser totalmente honesto, além de atualizar o suporte a Unicode para o analisador gramatical PEG.js e o exemplo de JavaScript , tenho pouco ou nenhum conhecimento sobre Unicode, portanto, no momento, não consigo corrigir esse problema (está claramente declarado em ambas as gramáticas: _Non -Os caracteres BMP são completamente ignorados_).

Por enquanto, enquanto trabalho em projetos de pessoal e relacionados ao trabalho mais urgentes (incluindo _PEG.js 0.x_), vou continuar esperando por alguém que entenda melhor de Unicode para oferecer alguns PR's 😆, ou eventualmente dar a volta por cima depois de _PEG. js v1_, desculpe amigo.

Para sua informação, pares substitutos parecem funcionar. A gramática
start = result:[\uD83D\uDCA9]+ {return result.join('')}
analisa 💩 que é u + 1F4A9. Observe que result.join ('') coloca o par substituto novamente, caso contrário, você obterá ['\uD83D','\uDCA9'] vez de hankey. Faixas seriam problemáticas.
Mais sobre pares substitutos: https://en.wikipedia.org/wiki/UTF-16#U + 010000_to_U + 10FFFF

Isso não substitui de forma alguma o que o OP pediu.

@drewnolan Obrigado pelo aviso 👍

Infelizmente, essa gramática também analisa \uD83D\uD83D .

Para outras pessoas que se depararam com esse problema: tenho sorte de precisar lidar apenas com um pequeno subconjunto de pontos de código fora do BMP, então acabei mapeando-os na área de uso privado do BMP antes de analisar e inverter esse mapeamento logo em seguida .

Esta solução é obviamente repleta de problemas no caso geral, mas funciona bem no meu domínio de problemas.

@futagoza - Vou dar o meu melhor para explicar. Você está enfrentando vários problemas aqui.

  1. Unicode tem codificações, notações e intervalos

    1. Aqui, o que importa são "intervalos" - quais caracteres são suportados - e "notações" - como são escritos

    2. Isso muda com o tempo. O Unicode adiciona novos personagens periodicamente, como quando eles adicionam emoji ou notas musicais

  2. Unicode tem "notações". São coisas como utf-16 , ucs4 , etc. É assim que codepoints , que são os dados pretendidos, são codificados como bytes. utf-16-le por exemplo permite codificar a maioria das letras como pares de dois bytes chamados code units , mas usar grupos de unidades de código para expressar caracteres de alto valor até 0x10ffff .

    1. Entendimento principal: isso não é realmente alto o suficiente. Muitas coisas interessantes, como emoji, grandes pedaços de chinês histórico e a pergunta básica deste post (caracteres matemáticos do quadro-negro) estão acima dessa linha



      1. ISO e Unicode Consortium foram claros: eles nunca estão consertando isso . Se você quiser caracteres maiores, use uma codificação maior do que utf-16.



    2. Entendimento principal nº 2: javascript é formalmente utf16



      1. Isso significa que existem caracteres Unicode (muitos deles) que o tipo de string Javascript não pode representar


      2. OP está pedindo que você conserte isso


      3. Isso é possível, mas não é fácil - você teria que implementar o algoritmo de análise Unicode, que é famoso por ser um ninho de rato



Eu também quero isso, mas, realisticamente, isso não vai acontecer

puta merda, alguém cometeu uma substituição completa do analisador de string quase um ano atrás , e eles reconheceram a sobrecarga, então eles nos deixaram usar strings JS padrão geralmente

POR QUE ISSO NÃO ESTÁ FUNDIDO

@StoneCypher Eu amo o fogo em seu coração! Mas por que incomodar o mantenedor atual? Ninguém deve nada. Por que não manter seu próprio garfo?

Não há mantenedor atual. A pessoa que assumiu o PEG nunca lançou nada. Ele trabalhou no próximo menor durante três anos, depois disse que não gostou da aparência, está jogando peg.js fora e recomeçando a partir de algo que escreveu do zero em um idioma diferente, com um código incompatível AST.

A ferramenta perdeu metade de sua base de usuários esperando três anos para que esse homem fizesse correções de uma linha que outras pessoas escreveram, como suporte a módulo es6, suporte a texto digitado, suporte a setas, unicode estendido etc.

Há uma dúzia de pessoas pedindo a ele para se fundir e ele continua dizendo "não, este é meu projeto de hobby agora e eu não gosto do que é"

Muitas pessoas têm empresas baseadas neste analisador. Eles estão completamente ferrados.

Este homem prometeu ser o mantenedor de uma ferramenta extremamente importante, e não fez nenhuma manutenção. É hora de deixar outra pessoa manter esta biblioteca em boas condições agora.

Por que não manter seu próprio garfo?

Eu tenho há três anos. Meu peg tem quase um terço do rastreador de problemas corrigido.

Tive que cloná-lo, renomeá-lo e fazer um novo fork para corrigir o problema de tamanho para tentar confirmá-lo, porque o meu mudou muito

É hora de todos receberem essas correções, bem como aquelas que estão no rastreador desde 2017.

Esse cara não está mantendo a fixação; ele está deixando morrer.

É hora de mudar.

@drewnolan - então, não tenho certeza se isso é interessante ou não, mas pares substitutos não funcionam. É só que, por coincidência, eles costumam fazer.

Para entender o problema subjacente, você deve pensar sobre o padrão de bits do nível de codificação, não o nível de representação um.

Ou seja, se você tiver um valor Unicode de 240, a maioria das pessoas pensará "Oh, ele significa 0b1111 0000 ." Mas, na verdade, não é assim que Unicode representa 240; mais de 127 é representado por dois bytes, porque o bit superior é um sinalizador, não um bit de valor. Portanto, 240 em Unicode é, na verdade, 0b0000 0001 0111 0000 em armazenamento (exceto em utf-7, que é real e não um erro de digitação, onde as coisas ficam muito estranhas. E sim, eu sei que a Wikipedia diz que não está em uso. A Wikipedia está errada . É para onde o SMS é enviado; pode ser a codificação de caracteres mais comum pelo tráfego total.)

Então aqui está o problema.

Se você escrever um byte STUV WXYZ, então em utf16, a partir de dados ucs4, se sua coisa for cortada pela metade, quase sempre você pode grampear novamente.

Uma vez em 128 você não pode, para caracteres nativamente com codificação acima de dois bytes. (Parece um número muito específico, não é?)

Porque quando o bit superior na posição do segundo byte estiver em uso, cortá-lo pela metade adicionará um zero onde deveria ser um. Agrafá-los lado a lado como dados binários não remove o conceito de valor novamente. O valor decodificado não é equivalente ao valor codificado, e você está acrescentando decodificações, não codificações.

Acontece que a maioria dos emojis está fora dessa faixa. No entanto, grandes pedaços de vários idiomas não são, incluindo o chinês raro, a maioria dos símbolos matemáticos e musicais.

Com certeza, sua abordagem é boa o suficiente para quase todos os emojis e para todas as linguagens humanas comuns o suficiente para entrar no Unicode 6, o que é uma grande melhoria em relação ao status quo

Mas este PR realmente deve ser mesclado, uma vez que é suficientemente testado para correção e contra problemas de desempenho inesperados (lembre-se, problemas de desempenho de Unicode são o motivo pelo qual o php morreu)

Parece que a expressão . (dot character) também precisa do modo Unicode. Comparar:

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

Saída:

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

Trabalhei recentemente nisso, usando # 616 como base e modificando-o para usar a sintaxe ES6 \u{hhhhhhh} , vou criar um PR em algumas horas.

Calcular os intervalos de regex UTF-16 divididos por substitutos é um pouco complicado e usei https://github.com/mathiasbynens/regenerate para isso; esta seria a primeira dependência do pacote pegjs, espero que seja possível (também existem polyfills para propriedades Unicode que podem ser adicionadas como dependência, consulte # 648). Consulte a Wikipedia se você não conhece os substitutos UTF-16 .

Para tornar o PEG.js compatível com todo o Unicode, existem vários níveis:

  1. Adicione uma sintaxe para codificar caracteres Unicode acima do BMP, corrigido por # 616 ou minha versão de sintaxe ES6,
  2. Reconhecer strings constantes, fornecidas diretamente pelo ponto anterior,
  3. Corrija o relatório SyntaxError para possivelmente exibir 1 ou 2 unidades de código para exibir o caractere Unicode real,
  4. Calcule com precisão a classe regex para BMP e / ou pontos de código astral - isso por si só não está funcionando, consulte o próximo ponto,
  5. Gerencie o incremento do cursor porque uma classe regex agora pode ser (1), (2) ou (1 ou 2 dependendo do tempo de execução), consulte os detalhes abaixo,
  6. Implemente o ponto de regra . para capturar 1 ou 2 unidades de código.

Para a maioria dos pontos, podemos conseguir ser compatíveis com as versões anteriores e gerar analisadores muito semelhantes aos mais antigos, exceto para o ponto 5 porque o resultado de uma análise pode depender se a regra de ponto captura uma ou duas unidades de código. Para isso, proponho adicionar uma opção de tempo de execução para permitir que o usuário escolha entre duas ou três opções:

  1. A regra de pontos captura apenas uma unidade de código BMP,
  2. A regra de pontos captura um ponto de código Unicode (1 ou 2 unidades de código),
  3. A regra de ponto captura um ponto de código Unicode ou um substituto solitário.

A classe Regex pode ser analisada estaticamente durante a geração do analisador para verificar se eles têm um comprimento fixo (em número de unidades de código). Existem 3 casos: 1. apenas um BMP ou uma única unidade de código, ou 2. apenas duas unidades de código, ou 3. uma ou duas unidades de código, dependendo do tempo de execução. Por enquanto, o bytecode assume que uma classe regex é sempre uma unidade de código ( veja aqui ). Com a análise estática, poderíamos alterar este parâmetro desta instrução bytecode para 1 ou 2 para os dois primeiros casos. Mas, para o terceiro caso, acho que uma nova instrução de bytecode deve ser adicionada para, em tempo de execução, obter o número de unidades de código correspondidas e aumentar o cursor de acordo. Outras opções sem uma nova instrução de bytecode seriam: 1. sempre calcular o número de unidades de código correspondidas, mas isso é uma penalidade de desempenho durante a análise para analisadores somente BMP, portanto, não gosto dessa opção; 2. calcular se a unidade de código atual é um substituto alto seguido por um substituto baixo para incrementar de 1 ou 2, mas isso pressupõe que a gramática sempre tem substitutos UTF-16 bem formados sem a possibilidade de escrever gramáticas com substitutos solitários ( veja o próximo ponto) e isso também é uma penalidade de desempenho para analisadores somente BMP.

Há a questão dos substitutos solitários (substituto alto sem substituto baixo depois dele, ou substituto baixo sem substituto alto antes dele). Minha opinião sobre isso é que uma classe regex deve ser exclusivamente: ou com substitutos solitários ou com caracteres Unicode UTF-16 bem formados (BMP ou um substituto alto seguido por um substituto baixo), caso contrário, há o perigo de que os autores de gramática desconhecem As sutilezas do UTF-16 misturam as duas coisas e não entendem o resultado, e os autores de gramática que desejam administrar a si próprios substitutos UTF-16 podem fazê-lo com regras PEG para descrever ligações entre substitutos altos e baixos específicos. Proponho adicionar um visitante que imponha esta regra durante a geração do analisador.

Para concluir, é provavelmente mais fácil gerenciar a questão de substitutos solitários em PEG do que em regex porque o analisador de PEG sempre avança, então a próxima unidade de código é reconhecida ou não, ao contrário de regexes onde possivelmente algum retrocesso poderia se associar ou desassociar um substituto alto de um substituto baixo e, consequentemente, alterar o número de caracteres Unicode correspondentes, etc.

A sintaxe do PR para ES6 para o caractere astral Unicode é # 651 com base no # 616 e o ​​desenvolvimento para classes é https://github.com/Seb35/pegjs/commit/0d33a7a4e13b0ac7c55a9cfaadc16fc0a5dd5f0c implementando os pontos 2 e 3 em meu comentário acima, e apenas um rápido hack para incremento do cursor (ponto 4) e nada por agora para o ponto de regra . (ponto 5).

Meu desenvolvimento atual sobre este problema está quase concluído, o trabalho mais avançado está em https://github.com/Seb35/pegjs/tree/dev-astral-classes-final. Todos os cinco pontos mencionados acima são tratados e o comportamento global tenta imitar regexes JS em relação aos casos extremos (e há muitos deles).

O comportamento global é governado pela opção unicode semelhante ao sinalizador unicode em regexes JS: o cursor é aumentado em 1 caractere Unicode (1 ou 2 unidades de código) dependendo do texto real (por exemplo [^a] corresponde ao texto "💯" e o cursor é aumentado em 2 unidades de código). Quando a opção unicode é falsa, o cursor é sempre acrescido de 1 unidade de código.

Em relação à entrada, não tenho certeza se projetamos PEG.js da mesma forma que regexes JS: devemos autorizar [\u{1F4AD}-\u{1F4AF}] (equivalente a [\uD83D\uDCAD-\uD83D\uDCAF] ) na gramática em modo não Unicode? Podemos fazer a diferença entre "Unicode de entrada" e "Unicode de saída":

  • entrada Unicode é sobre a autorização de todos os caracteres Unicode em classes de caracteres (que é calculado internamente como 2 unidades de código fixas ou 1 unidade de código fixa)
  • saída Unicode é o incremento do cursor do analisador resultante: 1 unidade de código ou 1 caractere Unicode para as regras 'ponto' e 'classe de caracteres invertidos' - as únicas regras onde os caracteres não são listados explicitamente e onde precisamos de uma decisão da gramática autor

Pessoalmente, acho que preferiria que autorizássemos o Unicode de entrada, permanentemente ou com uma opção com um true padrão, pois não há sobrecarga significativa e isso permitiria essa possibilidade para todos por padrão, mas o Unicode de saída deve permanecer false porque o desempenho dos analisadores gerados é melhor (sempre um incremento de cursor de 1).

Em relação a este problema em geral (e sobre a saída Unicode padronizada para false ), devemos ter em mente que já é possível codificar em nossas gramáticas caracteres Unicode, ao preço de compreender o funcionamento do 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] )

Portanto, um autor de gramática que deseja um analisador rápido e é capaz de reconhecer caracteres Unicode em partes específicas de sua gramática pode usar essa regra. Conseqüentemente, este problema é apenas sobre a simplificação do gerenciamento Unicode sem mergulhar nos componentes internos do UTF-16.


Sobre a implementação, considerei em minha primeira tentativa que o texto da gramática estava codificado em caracteres Unicode e a regra do 'ponto' do analisador PEG.js reconhecia caracteres Unicode. A segunda e última tentativa reverteu isso (a regra do ponto é sempre 1 unidade de código para análise mais rápida) e há um pequeno algoritmo no visitante prepare-unicode-classes.js para reconstruir caracteres Unicode divididos em classes de caracteres (por exemplo, [\uD83D\uDCAD-\uD83D\uDCAF] é sintaticamente reconhecido como [ "\uD83D", [ "\uDCAD", "\uD83D" ], "\uDCAF" ] e este algoritmo transforma isso em [ [ "\uD83D\uDCAD", "\uD83D\uDCAF" ] ] ). Eu planejava escrever isso na própria gramática, mas teria sido longo e, mais importante, existem várias maneiras de codificar os caracteres ("💯", "uD83DuDCAF", "u {1F4AF}"), por isso é mais fácil de escrever em um visitante.

Eu adicionei dois opcodes na segunda tentativa:

  • MATCH_ASTRAL semelhante a MATCH_ANY, mas correspondendo a um caractere Unicode (input.charCodeAt(currPos) & 0xFC00) === 0xD800 && input.length > currPos + 1 && (input.charCodeAt(currPos+1) & 0xFC00) === 0xDC00
  • MATCH_CLASS2 muito semelhante a MATCH_CLASS, mas correspondendo às próximas duas unidades de código em vez de apenas uma classes[c].test(input.substring(currPos, currPos+2)
    Então, dependendo se correspondermos a um caractere de unidade de 2 códigos ou a um caractere de unidade de 1 código, o cursor é aumentado em 1 ou 2 unidades de código com o opcode ACCEPT_N , e as classes de caracteres são divididas em duas regexes de comprimento fixo (1 ou 2 unidades de código).

Fiz algumas otimizações com o recurso "match", eliminando durante a geração os caminhos de "dead code" dependendo do modo (Unicode ou não) e da classe de caracteres.

Observe também que as regexes são sempre positivas nesta implementação: as regexes invertidas retornam o oposto, resultando no bytecode. Era mais fácil evitar casos extremos em torno de substitutos.

Eu escrevi alguma documentação, mas provavelmente adicionarei mais (talvez um guia para explicar rapidamente os detalhes da (s) opção (ões) unicode e os snippets com a regra de ponto Unicode feita em casa). Vou adicionar testes antes de enviá-lo como um PR.

Esta página foi útil?
0 / 5 - 0 avaliações