Перейти к содержанию

UTF-8: байты, кодовые точки и диапазоны

Реализовано в 0.0.22. Библиотека utf8 читает View<u8> обычными pure-функциями, написанными на Liza. Входом может служить view массива, регионального буфера или Vec<u8>. Владелец и его заимствование сохраняются; функции не выделяют память и не копируют входные байты.

Это минимальные текстовые операции. Изменяемый String, нормализация, регистровые преобразования и графемные кластеры пока не реализованы. Существующий Text для литералов и консоли остаётся отдельным типом; неявного преобразования Text в View<u8> нет.

Три разных единицы

  • Байтовое смещение u32 адресует позицию в переданном view, начиная с нуля.
  • Кодовая точка в корректном UTF-8 — Unicode scalar value, представленное одним–четырьмя байтами. Декодер возвращает его числовое значение u32.
  • Видимый символ может состоять из нескольких кодовых точек. Например, e и следующий комбинируемый акцент — две кодовые точки.

utf8.count считает кодовые точки, не байты, не графемы и не ширину текста на экране. Пустой текст корректен; NUL, BOM и Unicode noncharacters допустимы. NUL не завершает текст, BOM не удаляется и учитывается при подсчёте.

Правила кодирования соответствуют Unicode, §3.9, таблице 3-7: декодер отвергает неверные ведущие и продолжающие байты, усечённые и избыточные кодировки, суррогаты и значения выше U+10FFFF.

API и стоимость

Namespace utf8 доступен без import, как console и file. Все аргументы вычисляются по обычным правилам вызова, слева направо.

Вызов Результат Работа с байтами
utf8.validate(data) Result<Utf8Done,Utf8Error> Проверяет весь вход до первой ошибки, O(n)
utf8.count(data) Result<u32,Utf8Error> Проверяет и считает за один проход, O(n)
utf8.next(data,index) Result<Option<Utf8Step>,Utf8Error> Декодирует одну последовательность, O(1)
utf8.is_boundary(data,index) Result<bool,Utf8Error> Проверяет весь вход, затем границу, O(n)
utf8.range(data,start,end) Result<Utf8Range,Utf8Error> Проверяет весь вход и обе границы, O(n)

Дополнительная память всех операций — O(1). n — длина входа в байтах. Результаты — небольшие независимые значения, которые могут пережить владельца байтов. Вызовы не возвращают view и не продлевают время заимствования.

struct Utf8Done {}
struct Utf8Step { value:u32; next:u32 }
struct Utf8Range { start:u32; end:u32 }
enum Utf8ErrorKind { InvalidEncoding; OutOfBounds; NotBoundary; ReversedRange }
struct Utf8Error { kind:Utf8ErrorKind; offset:u32 }

Это объявления библиотечных типов для справки; повторно объявлять их в программе не нужно. Utf8Range — пара чисел, а не сохраняемое доказательство валидности: после изменения владельца прежняя проверка ничего не гарантирует.

Декодирование и диагностика

next проверяет только последовательность, начинающуюся в index. Он не проверяет префикс или оставшийся суффикс:

  • index == data.len возвращает Ok(None), даже если предшествующие байты некорректны;
  • index > data.len возвращает OutOfBounds со смещением index;
  • начало внутри многобайтовой последовательности возвращает InvalidEncoding;
  • успех возвращает Some(Utf8Step(value,next)), где next > index.

Чтобы проверить текст целиком, используйте validate, count или проход через next, начиная с нуля и продолжая до конца.

InvalidEncoding.offset — начало первой некорректной последовательности, а не обязательно позиция неверного continuation byte. Например, для [65, 226, 40, 161] ошибка начинается на байте 1. Для усечённой последовательности также возвращается позиция её ведущего байта. Все смещения отсчитываются от начала переданного view, не исходного файла.

Некорректный UTF-8 и недопустимые диапазоны возвращают Err, а не trap. Обязательные traps при вычислении аргументов по-прежнему сохраняются.

Границы и диапазоны

На корректном тексте 0 и data.len всегда являются границами. Позиция между байтами одной кодовой точки — не граница.

is_boundary сначала проверяет index <= len, затем валидность всего входа и только после этого возвращает true или false. Даже запрос границы 0 не скрывает некорректный суффикс.

Для range порядок проверок определён:

  1. start > end → ReversedRange, смещение start.
  2. end > len → OutOfBounds, смещение end.
  3. Некорректный UTF-8 где-либо во входе → InvalidEncoding.
  4. start внутри кодировки → NotBoundary, смещение start.
  5. end внутри кодировки → NotBoundary, смещение end.
  6. Иначе Ok(Utf8Range(start,end)).

Диапазон полуоткрытый: [start,end). Пустой диапазон допустим только на границе кодовой точки. Это проверка диапазона, а не создание среза через другой view: ограничение на reborrow сохраняется.

Пример: файл → view → текст

python3 -m liza run examples/utf8_file.liza

Файл examples/data/utf8.txt содержит Лиза 😀 и перевод строки: 14 байтов, 7 кодовых точек. Программа выводит Code points: 7. Файл читается через текущий bounded API file.read, поэтому ограничен 2048 байтами. Это не потоковый декодер: разрыв кодовой точки между чанками поддерживается отдельной потоковой библиотекой 0.0.23.

examples/utf8_file.liza
// Run from the repository root. file.read accepts at most 2048 bytes.
fn main()->u32:io {
    match file.read("examples/data/utf8.txt") {
        Err(error)=>{
            match console.println("Cannot read text file") {Ok(n)=>{} Err(e)=>{return 3}}
            return 1
        }
        Ok(buffer)=>{
            bytes=buffer.data
            data=view bytes[0..buffer.len]
            match utf8.count(data) {
                Err(error)=>{
                    match console.write("Invalid UTF-8 at byte: ") {Ok(n)=>{} Err(e)=>{return 3}}
                    match console.print_u64(u64(error.offset)) {Ok(n)=>{} Err(e)=>{return 3}}
                    match console.println("") {Ok(n)=>{} Err(e)=>{return 3}}
                    return 2
                }
                Ok(count)=>{
                    match console.write("Code points: ") {Ok(n)=>{} Err(e)=>{return 3}}
                    match console.print_u64(u64(count)) {Ok(n)=>{} Err(e)=>{return 3}}
                    match console.println("") {Ok(n)=>{} Err(e)=>{return 3}}
                    return 0
                }
            }
        }
    }
}

file.read возвращает Result: ошибка чтения даёт код выхода 1. В успешной ветви bytes владеет массивом, а data ограничивает доступ реальной длиной файла, исключая заполненный нулями хвост буфера. utf8.count(data) одновременно проверяет кодирование и считает кодовые точки. Ошибка текста выводит байтовое смещение и даёт код 2; ошибка консоли — 3. Успех возвращает 0. Передача data в декодер не копирует массив.

Проверяемые свойства

На O0/O2 проверяются все одно- и двухбайтовые входы, поток всех Unicode scalar values, граничные кодировки, случайные входы, смещения и диапазоны. Результаты сверяются с независимым UTF-8 декодером Python. Отдельно проверяются views регионального Vec, области заимствования и пример чтения файла.

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