Почему лексер не распознает русские токены UTF-8
Проблемы с распознаванием русских инструкций в лексере из-за неправильной обработки UTF-8 кодирования. Узнайте, как исправить многосбайтовые символы в C-коде.
Почему мой лексер выдает неизвестные токены при обработке русских инструкций?
Я пишу простой лексер для перевода русских инструкций на стандартные nasm инструкции. Мой код на C (sasm.c и lexer.c) корректно обрабатывает некоторые токены, но вывод показывает, что многие токены распознаются как неизвестные (тип 0, имя ‘Empty’).
Пример кода:
// 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;
}
// 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
- Правильная реализация лексера
- Дополнительные рекомендации
- Отладка и тестирование
Основная проблема UTF-8 кодирования
Русские символы в UTF-8 представлены 2-байтовыми последовательностями, а не однобайтовыми, как в ASCII. Например:
- Символ “п” = 0xD0 0xBF (2 байта)
- Символ “е” = 0xD0 0xB5 (2 байта)
- Символ “р” = 0xD1 0x80 (2 байта)
Таким образом, строка “пер” занимает 6 байт, а не 3, как вы предполагаете в коде:
// НЕПРАВИЛЬНО - сравнивает только 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 символа:
// Функция для определения длины 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 (следующим байтом)”.
Правильная реализация лексера
Модифицированный код лексера
#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;
}
Ключевые изменения:
- UTF-8 длина символа: Добавлена функция
utf8_char_length()для определения правильной длины символа - Правильные сравнения: Используются полные UTF-8 последовательности вместо ASCII строк
- Корректный инкремент:
posувеличивается на количество байт, а не символов - Обработка ошибок: Добавлено распознавание неизвестных токенов
Дополнительные рекомендации
1. Использование библиотек для UTF-8
Для сложных проектов рекомендуется использовать специализированные библиотеки:
- ICU - International Components for Unicode
- libunistring - GNU библиотека для работы с Unicode
2. Тестирование с разными кодировками
Как указано в Stack Overflow, важно убедиться, что исходные файлы сохранены в UTF-8:
file sasm.c
# Должно показать: sasm.c: UTF-8 Unicode C program text
3. Настройка локали
Добавьте установку UTF-8 локали в начале программы:
#include <locale.h>
setlocale(LC_ALL, "en_US.UTF-8");
Отладка и тестирование
Пошаговая отладка
Для диагностики проблем добавьте вывод отладочной информации:
printf("Debug: pos=%d, char=0x%02X, len=%d\n",
pos, (unsigned char)(*input)[pos], utf8_char_length((*input)[pos]));
Тестовые случаи
Проверьте работу с разными комбинациями:
char test_cases[][50] = {
"пер ар 10", // Основной случай
"пер 10", // Только команда
"ар 20", // Только регистр
"пер ар", // Без числа
"добавить 5", // Неизвестная команда
"пер ar 10", // Смешанная кодировка
"пер\tар\n10" // С пробелами и переносами
};
Визуализация UTF-8
Для понимания структуры данных можно добавить вывод байтов:
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");
}
Источники
- UTF-8 friendly parser/lexer - Handmade Network
- How to use UTF-8 in C code? - Stack Overflow
- Unicode (UTF-8) in C - Coding Forums
- Understanding text for C programmers (UTF-8, Unicode, ASCII) - Reddit
- Is it possible “force” UTF-8 in a C program? - Stack Overflow
Заключение
- Основная проблема: Неправильная обработка UTF-8 кодирования русских символов в лексере
- Решение: Использование правильных длин байт для сравнений и инкрементации позиции
- Практические шаги: Добавьте функции определения длины UTF-8 символов и модифицируйте сравнения
- Дальнейшее улучшение: Рассмотрите использование специализированных Unicode библиотек для сложных задач
- Тестирование: Проверяйте работу с различными тестовыми случаями и кодировками
Ваш лексер должен корректно обрабатывать русские инструкции после внесения этих изменений, так как теперь он будет правильно учитывать многосбайтовую природу UTF-8 кодирования.