Другое

Почему лексер не распознает русские токены UTF-8

Проблемы с распознаванием русских инструкций в лексере из-за неправильной обработки UTF-8 кодирования. Узнайте, как исправить многосбайтовые символы в C-коде.

Почему мой лексер выдает неизвестные токены при обработке русских инструкций?

Я пишу простой лексер для перевода русских инструкций на стандартные nasm инструкции. Мой код на C (sasm.c и lexer.c) корректно обрабатывает некоторые токены, но вывод показывает, что многие токены распознаются как неизвестные (тип 0, имя ‘Empty’).

Пример кода:

c
// sasm.c
#include <stdio.h>
#include "lexer.h"

int main() {
    char text[] = "пер ар 10";
    char *content = text;
    
    printf("%s\n\n", text);
    
    Token token;
    while ((token = lexer(&content)).type != TOKEN_EOF) {
        printf("Token: Type = %d, Name = '%s'\n", token.type, token.name[0] ? token.name : "Empty");
    }
    
    return 0;
}
c
// lexer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "lexer.h"

static int pos = 0;

static void skip_whitespace_and_comments(char **input) {
    while (isspace((*input)[pos]) || ((*input)[pos] == ';')) {
        if ((*input)[pos] == ';') {
            while ((*input)[pos] != '\n' && (*input)[pos] != '\0') {
                pos++;
            }
        } else {
            pos++;
        }
    }
}

Token lexer(char **input) {
    Token token;
    memset(&token, 0, sizeof(Token));
    
    skip_whitespace_and_comments(input); 
    
    if ((*input)[pos] == '\0') {
        token.type = TOKEN_EOF;
        return token;
    }
    
    // Обработка команд
    if (strncmp(&(*input)[pos], "пер", 3) == 0) {
        token.type = MOV;
        strncpy(token.name, "mov", sizeof(token.name));
        pos += 3;
    } 
    else if (strncmp(&(*input)[pos], "ар", 2) == 0) {
        token.type = RAX; 
        strncpy(token.name, "rax", sizeof(token.name));
        pos += 2;
    } 
    else if (isdigit((*input)[pos])) {
        token.type = TOKEN_NUMBER;
        char num[50];
        int i = 0;
        
        while (isdigit((*input)[pos])) {
            if (i < sizeof(num) - 1) {
                num[i++] = (*input)[pos];
            }
            pos++;
        }
        num[i] = '\0';
        strncpy(token.name, num, sizeof(token.name)); 
    } else {
        // Обработка неизвестного токена
        pos++; // пропуск неизвестного символа
    }
    return token;
}

Текущий вывод:

пер ар 10

Token: Type = 0, Name = 'mov'
Token: Type = 0, Name = 'Empty'
Token: Type = 0, Name = 'Empty'
Token: Type = 0, Name = 'Empty'
Token: Type = 7, Name = 'rax'
Token: Type = 0, Name = 'Empty'
Token: Type = 0, Name = 'Empty'
Token: Type = 16, Name = '10'

Я проверил кодировку UTF-8 и даже попробовал сравнивать напрямую с UTF-8 кодами (например, strncmp(&(*input)[pos], “\xD0\xBF\xD0\xB5\xD1\x80”, 6) == 0), но проблема сохраняется. Использую компилятор clang, но с gcc ситуация аналогичная. В чем может быть причина?

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

Краткий ответ:
Ваш лексер обрабатывает русские символы некорректно, потому что использует сравнения по количеству символов вместо количества байт в UTF-8 кодировке. Русские буквы “пер” занимают 6 байт, а не 3, и “ар” - 4 байта, а не 2. Это приводит к тому, что strncmp находит только часть последовательности, что вызывает проблемы с распознаванием токенов.


Содержание


Основная проблема UTF-8 кодирования

Русские символы в UTF-8 представлены 2-байтовыми последовательностями, а не однобайтовыми, как в ASCII. Например:

  • Символ “п” = 0xD0 0xBF (2 байта)
  • Символ “е” = 0xD0 0xB5 (2 байта)
  • Символ “р” = 0xD1 0x80 (2 байта)

Таким образом, строка “пер” занимает 6 байт, а не 3, как вы предполагаете в коде:

c
// НЕПРАВИЛЬНО - сравнивает только 3 байта вместо 6
if (strncmp(&(*input)[pos], "пер", 3) == 0) {
    
// ПРАВИЛЬНО - сравнивает все 6 байт UTF-8 последовательности  
if (strncmp(&(*input)[pos], "\xD0\xBF\xD0\xB5\xD1\x80", 6) == 0) {

Как указано в Stack Overflow, при работе с UTF-8 необходимо использовать правильные длины байт для сравнений, так как “strlen() и подобные функции считают байты (‘code units’), а не символы (‘code points’)”.


Технические детали работы с UTF-8

Структура UTF-8 кодирования

UTF-8 использует переменную длину кодирования:

  • ASCII символы (0-127) - 1 байт
  • Русские символы (кириллица) - 2 байта
  • Китайские символы - 3 байта
  • Редкие символы - 4 байта

Определение длины UTF-8 символа

Для правильной работы лексера нужна функция, определяющая длину UTF-8 символа:

c
// Функция для определения длины UTF-8 символа
int utf8_char_length(unsigned char c) {
    if (c < 0x80) return 1;        // ASCII
    if (c < 0xE0) return 2;        // 2-байтовые символы (русская кириллица)
    if (c < 0xF0) return 3;        // 3-байтовые символы
    return 4;                      // 4-байтовые символы
}

Проблемы с текущим подходом

Как объясняется в The Coding Forums, “разделение UTF-8 строк может быть опасным, но достаточно добавить логику, чтобы убедиться, что байт после разделения не является 10XXXXXX (следующим байтом)”.


Правильная реализация лексера

Модифицированный код лексера

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "lexer.h"

static int pos = 0;

// Функция для определения длины UTF-8 символа
int utf8_char_length(unsigned char c) {
    if (c < 0x80) return 1;        // ASCII
    if (c < 0xE0) return 2;        // 2-байтовые символы (русская кириллица)
    if (c < 0xF0) return 3;        // 3-байтовые символы
    return 4;                      // 4-байтовые символы
}

// Функция для безопасного сравнения UTF-8 строк
int utf8_strncmp(const char *s1, const char *s2, int len_bytes) {
    return strncmp(s1, s2, len_bytes);
}

static void skip_whitespace_and_comments(char **input) {
    while (isspace((*input)[pos]) || ((*input)[pos] == ';')) {
        if ((*input)[pos] == ';') {
            while ((*input)[pos] != '\n' && (*input)[pos] != '\0') {
                pos++;
            }
        } else {
            pos++;
        }
    }
}

Token lexer(char **input) {
    Token token;
    memset(&token, 0, sizeof(Token));
    
    skip_whitespace_and_comments(input); 
    
    if ((*input)[pos] == '\0') {
        token.type = TOKEN_EOF;
        return token;
    }
    
    // Определяем длину первого символа для корректного сравнения
    int first_char_len = utf8_char_length((*input)[pos]);
    
    // Обработка русских команд
    if (first_char_len == 2) {  // Русские символы
        if (utf8_strncmp(&(*input)[pos], "\xD0\xBF\xD0\xB5\xD1\x80", 6) == 0) {
            token.type = MOV;
            strncpy(token.name, "mov", sizeof(token.name));
            pos += 6;  // Инкрементируем на 6 байт, а не на 3 символа
        } 
        else if (utf8_strncmp(&(*input)[pos], "\xD0\xB0\xD1\x80", 4) == 0) {
            token.type = RAX; 
            strncpy(token.name, "rax", sizeof(token.name));
            pos += 4;  // Инкрементируем на 4 байта, а не на 2 символа
        }
    }
    // Обработка ASCII команд (если нужны)
    else if (first_char_len == 1) {
        if (strncmp(&(*input)[pos], "mov", 3) == 0) {
            token.type = MOV;
            strncpy(token.name, "mov", sizeof(token.name));
            pos += 3;
        }
        // Другие ASCII команды...
    }
    // Обработка чисел
    else if (isdigit((*input)[pos])) {
        token.type = TOKEN_NUMBER;
        char num[50];
        int i = 0;
        
        while (isdigit((*input)[pos])) {
            if (i < sizeof(num) - 1) {
                num[i++] = (*input)[pos];
            }
            pos++;
        }
        num[i] = '\0';
        strncpy(token.name, num, sizeof(token.name)); 
    } else {
        // Обработка неизвестного токена
        pos += first_char_len;  // Пропускаем один UTF-8 символ
        token.type = TOKEN_UNKNOWN;
        strncpy(token.name, "unknown", sizeof(token.name));
    }
    return token;
}

Ключевые изменения:

  1. UTF-8 длина символа: Добавлена функция utf8_char_length() для определения правильной длины символа
  2. Правильные сравнения: Используются полные UTF-8 последовательности вместо ASCII строк
  3. Корректный инкремент: pos увеличивается на количество байт, а не символов
  4. Обработка ошибок: Добавлено распознавание неизвестных токенов

Дополнительные рекомендации

1. Использование библиотек для UTF-8

Для сложных проектов рекомендуется использовать специализированные библиотеки:

  • ICU - International Components for Unicode
  • libunistring - GNU библиотека для работы с Unicode

2. Тестирование с разными кодировками

Как указано в Stack Overflow, важно убедиться, что исходные файлы сохранены в UTF-8:

bash
file sasm.c
# Должно показать: sasm.c: UTF-8 Unicode C program text

3. Настройка локали

Добавьте установку UTF-8 локали в начале программы:

c
#include <locale.h>
setlocale(LC_ALL, "en_US.UTF-8");

Отладка и тестирование

Пошаговая отладка

Для диагностики проблем добавьте вывод отладочной информации:

c
printf("Debug: pos=%d, char=0x%02X, len=%d\n", 
       pos, (unsigned char)(*input)[pos], utf8_char_length((*input)[pos]));

Тестовые случаи

Проверьте работу с разными комбинациями:

c
char test_cases[][50] = {
    "пер ар 10",      // Основной случай
    "пер 10",         // Только команда
    "ар 20",          // Только регистр
    "пер ар",         // Без числа
    "добавить 5",    // Неизвестная команда
    "пер ar 10",      // Смешанная кодировка
    "пер\tар\n10"     // С пробелами и переносами
};

Визуализация UTF-8

Для понимания структуры данных можно добавить вывод байтов:

c
void print_utf8_bytes(const char *str, int len) {
    for (int i = 0; i < len; i++) {
        printf("0x%02X ", (unsigned char)str[i]);
    }
    printf("\n");
}

Источники

  1. UTF-8 friendly parser/lexer - Handmade Network
  2. How to use UTF-8 in C code? - Stack Overflow
  3. Unicode (UTF-8) in C - Coding Forums
  4. Understanding text for C programmers (UTF-8, Unicode, ASCII) - Reddit
  5. Is it possible “force” UTF-8 in a C program? - Stack Overflow

Заключение

  • Основная проблема: Неправильная обработка UTF-8 кодирования русских символов в лексере
  • Решение: Использование правильных длин байт для сравнений и инкрементации позиции
  • Практические шаги: Добавьте функции определения длины UTF-8 символов и модифицируйте сравнения
  • Дальнейшее улучшение: Рассмотрите использование специализированных Unicode библиотек для сложных задач
  • Тестирование: Проверяйте работу с различными тестовыми случаями и кодировками

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

Авторы
Проверено модерацией
Модерация