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

Минимальный I/O

Реализовано в 0.0.12: pure/io, консольный вывод, UTF-8 литералы Text, ошибки вывода через Result. Runtime поддерживает Linux, macOS и Windows. Реализовано в 0.0.13: ограниченное чтение файлов. Реализовано в 0.0.14: move-only File, открытие, чтение чанками и явное закрытие. Полноценные строки и динамические коллекции следуют позже.

Три уровня

Уровень Ответственность
Язык Контракты эффектов, владение, порядок вычисления и вызовов
Библиотека Функции консоли, типы ошибок, правила частичного успеха
Платформа Запись в стандартный вывод и преобразование ошибок ОС

Консольные функции определены в библиотеке Liza и вызываются обычными операциями call в Semantic IR. Платформенная граница имеет доверенные сигнатуры и эффект io. Оператора печати, пользовательских extern и общего FFI пока нет. console — фиксированное пространство библиотечных имён; полной системы модулей пока нет.

Переносимость означает общий контракт и модель ошибок. Она не обещает одинакового окружения или результата взаимодействия с ОС.

Проверяемые pure и io

Функция без аннотации имеет контракт pure. Она может вызывать только чистые функции. : io разрешает вызов как чистых, так и io-функций. Разрешение определяется объявленным контрактом вызываемой функции, даже если её текущее тело не выполняет внешних операций. Все тела проверяются, поэтому запрет действует и через промежуточные функции, и в рекурсивных группах.

Проверяются обе ветви, включая статически неисполняемые. Оптимизация не позволяет обойти контракт. Локальный var, trap и возможное незавершение не являются I/O. io может блокировать; отдельного эффекта block ещё нет.

Вызовы наблюдаемы, даже если результат не используется. Вывод перед trap нельзя удалить или перенести за него. Неисполняемая ветвь ничего не выводит. Аргументы вычисляются слева направо; вызов выполняется после них. Контракты сохраняются в AST, сигнатурах функций и операциях вызова Semantic IR версии 14.

Text

Text — неизменяемая UTF-8 последовательность из литерала со временем жизни всей программы. Это не динамический String. Значение можно передавать, возвращать, хранить в структуре, копировать и перемещать. move запрещает дальнейший доступ через исходную привязку по обычным правилам Liza. Копирование Text не копирует содержимое литерала и не выделяет heap.

Литерал заключён в двойные кавычки и занимает одну строку исходника. Поддерживаются JSON escapes: \", \\, \/, \b, \f, \n, \r, \t, \uXXXX. Корректная surrogate-пара задаёт один Unicode scalar value; одиночные суррогаты, неизвестные escapes и неэкранированные управляющие символы отклоняются. Unicode-нормализация не выполняется. "\u0000" допустим и содержит один нулевой байт, а не конец строки.

text.len: u32 — число UTF-8 байтов, не число символов. Литерал длиннее u32::MAX байтов отклоняется. Индексация, конкатенация, сравнение, декодирование массивов байтов в Text и интерполяция пока не реализованы. Физический адрес литерала программе недоступен. Статическое время жизни Text не ослабляет ограничения заимствований обычных View<T>.

Консольный API 0.0.12

Все функции имеют эффект io и возвращают Result<u64,IoFailure>:

Функция Аргумент Действие
console.write Text Все UTF-8 байты без добавления перевода строки
console.println Text Текст, затем один байт LF
console.write_bytes View<u8> Байты без проверки UTF-8 и без поиска NUL
console.print_u64 u64 Десятичное беззнаковое число
console.print_i64 i64 Десятичное знаковое число

Неявных расширений нет: для u32 используйте console.print_u64(u64(value)). Числа выводятся без разделителей, пробелов и перевода строки, независимо от locale. Знак - есть только у отрицательных чисел. Ноль выводится как 0.

fn main()->u32:io {
    match console.println("Hello, Liza!") {
        Ok(written)=>{return 0}
        Err(error)=>{return 1}
    }
}

examples/hello.liza — исполняемый пример. Самостоятельных инструкций вида console.println(...) пока нет: результат используется в match, возвращается или связывается с именем. Неиспользуемая привязка результата разрешена, как и для других Result; автоматической обработки ошибки при этом нет.

Ok(n) означает, что все запрошенные байты записаны, и содержит их число, включая LF у println. Пустой write возвращает Ok(0) без обращения к ОС; пустой println всё равно записывает LF. Успех не обещает отображения на терминале, доставки другому процессу или сохранности данных на носителе.

enum IoError { Closed; PermissionDenied; BrokenPipe; WouldBlock; NoSpace; Other }
struct IoFailure { kind:IoError; written:u64 }

Это библиотечные определения; повторно объявлять их не нужно. Err(IoFailure) содержит категорию и число байтов, записанных этим вызовом до ошибки. Вывод не откатывается. При ошибке записи LF println включает ранее записанный текст в written. Текст и LF не являются атомарной записью; вывод других процессов может вклиниться между ними.

Категория Контракт
Closed Стандартный выход недоступен как открытый дескриптор
PermissionDenied Операция запрещена окружением
BrokenPipe Принимающая сторона pipe закрыта
WouldBlock Неблокирующая запись сейчас не может продолжаться
NoSpace Окружение сообщает отсутствие места
Other Прочая ошибка, в том числе невозможность обеспечить протокол записи

Короткая успешная запись продолжается; прерывание до записи байтов повторяется. WouldBlock возвращается вызывающему, а не превращается в цикл ожидания. Неизвестные ошибки ОС отображаются в Other; errno не входит в API Liza. Разорванный pipe возвращает BrokenPipe, не завершает программу через SIGPIPE. Платформенная реализация временно блокирует сигнал в вызывающем потоке, восстанавливает маску и не заменяет обработчики сигналов процесса.

Минимальная runtime предназначена для самостоятельных host-программ Liza. Произвольное встраивание в приложение с конкурентным управлением сигналами, signal-handler I/O и отменой потоков пока не поддерживается. Реализация опирается на POSIX pthread_sigmask.

Стоимость и границы реализации

Текст хранится статически; запись через view не материализует копию буфера. Форматирование целого использует фиксированный локальный буфер до 21 байта. Windows-консоль дополнительно использует ограниченные локальные буферы преобразования UTF-8 в UTF-16; в файловом/pipe тракте преобразования нет. Нет вызовов heap-аллокации, locale-форматирования и пользовательской stdio буферизации в runtime Liza. Это не обещание отсутствия выделений внутри ОС или системных библиотек. I/O может блокировать и не подходит под будущий контракт realtime без дополнительных ограничений.

build/run подключают runtime автоматически при использовании консольной библиотеки. Чистая программа без неё не требует этой runtime. emit-llvm выдаёт модуль с внешними символами: для ручной сборки консольной программы нужно также связать liza/runtime/console_posix.c с -pthread на Linux/macOS либо liza/runtime/console_windows.c на Windows. Нативное представление агрегатов не является публичным C ABI.

Ограниченное чтение файла 0.0.13

Реализован библиотечный вызов с эффектом io:

fn file.read(path:Text)->Result<FileBuffer,FileFailure>:io
struct FileBuffer { data:[u8;2048]; len:u32 }
struct FileFailure { kind:FileError; read:u32 }
enum FileError {
    NotFound; PermissionDenied; TooLarge; InvalidPath;
    PathTooLong; WouldBlock; Other
}

Вместимость первого API — 2048 байтов. Библиотека создаёт собственное хранилище фиксированного размера, заполняет его и передаёт независимое значение вызывающей функции. У программы нет доступа к буферу во время заполнения. Изменяемые views, heap и автоматический рост не требуются.

fn main()->u32:io {
    match file.read("examples/data/rtp.bin") {
        Ok(buffer)=>{
            bytes=buffer.data
            data=view bytes[0..buffer.len]
            return data.len
        }
        Err(error)=>{return 1}
    }
}

Успех означает достижение EOF и успешное закрытие файла. len — число байтов файла; пустой файл даёт Ok с нулевой длиной. Весь массив инициализирован: хвост после len заполнен нулями, но содержимым файла считается только [0..len]. Используйте этот диапазон для парсера.

Короткое чтение продолжается до EOF или ошибки; прерывание чтения сигналом повторяется. При заполнении всех 2048 байтов выполняется пробное чтение ещё одного байта. EOF означает успех, дополнительный байт — TooLarge. Он не входит в FileFailure.read: счётчик показывает число байтов, поместившихся в буфер, и никогда не превышает вместимость. Усечённый файл не выдаётся за успешный результат. Предварительная проверка размера не заменяет чтение; атомарный снимок изменяемого другим процессом файла не обещается.

При любой ошибке данные не возвращаются. FileFailure.read сохраняет число прочитанных в буфер байтов; при ошибке открытия это ноль. После открытия закрытие вызывается на каждом пути. Ошибка чтения или TooLarge имеет приоритет перед ошибкой закрытия; если чтение успешно, ошибка закрытия превращает результат в Err. POSIX close не повторяется: после ошибки дескриптор уже может быть освобождён. Гарантировать результат неуспешного системного закрытия библиотека не может.

Путь — Text, относительный путь отсчитывается от рабочей директории процесса. Пустой путь и встроенный NUL дают InvalidPath; максимум — 4095 байтов UTF-8, более длинный путь даёт PathTooLong. ОС может устанавливать меньший предел. На Linux/macOS передаются байты UTF-8, на Windows используется преобразование в UTF-16 и CreateFileW. Имена POSIX с невалидным UTF-8 пока не представимы. Типичные ошибки отсутствия файла и доступа имеют отдельные варианты; остальные ошибки ОС могут попадать в Other. Вызов может блокироваться, в частности при работе со специальными файлами; гарантии неблокирующего I/O нет.

Вместимость 2 КиБ — ограничение экспериментального API. Она подходит для демонстрационного RTP-пакета, но не для любого допустимого RTP-пакета. Большие буферы и выбор вместимости следуют после улучшения обработки агрегатов. Логическая передача значения не обещает O(1): внутри файлового вызова пока сохраняются физические копии. Отдельно проверяется отсутствие копирования всего массива при передаче успешного результата через view RTP-парсеру. Нулевое заполнение буфера также имеет стоимость.

Полный пример: examples/rtp_file.liza. Запуск из корня проекта:

python3 -m liza run examples/rtp_file.liza -O 2
# RTP sequence: 4660

Коды завершения примера: 0 — успех, 1 — ошибка чтения, 2 — некорректный RTP, 3 — ошибка консольного вывода. Путь пока задан литералом; аргументы программы не входят в этот API.

Платформенные основания: POSIX read, close и Windows ReadFile.

Ресурсы 0.0.14

Реализован непрозрачный тип File. Его нельзя создать из числа, сравнить, скопировать или преобразовать в адрес. Владение передаётся через move; обычная передача ресурса в пользовательскую функцию тоже требует move.

fn file.open(path:Text)->Result<File,FileError>:io
fn file.read_chunk(handle:File)->Result<FileChunk,FileFailure>:io
fn file.close(handle:File)->Result<u32,FileError>:io
struct FileChunk { data:[u8;2048]; len:u32; eof:bool }

file.open открывает существующий файл для чтения с теми же правилами пути, что и file.read. Успешный результат передаёт один ресурс владельцу.

fn main()->u32:io {
    match file.open("examples/data/rtp.bin") {
        Err(error)=>{return 1}
        Ok(input)=>{
            result=file.read_chunk(input)
            closed=file.close(move input)
            match result {
                Err(error)=>{return 2}
                Ok(chunk)=>{
                    match closed { Err(error)=>{return 3} Ok(n)=>{} }
                    return chunk.len
                }
            }
        }
    }
}

Временное использование и чтение чанков

file.read_chunk(input) — специальный библиотечный контракт временного использования: передайте доступную привязку без move. Владение остаётся у вызывающей функции, а временный доступ заканчивается с вызовом. Это I/O с изменением позиции файла, не обещание readonly-состояния ресурса. Пользовательские заимствованные параметры пока не поддерживаются.

Один вызов читает до 2048 байтов и повторяет прерванное чтение. Короткий положительный результат возвращается сразу, без попытки заполнить буфер. eof=true означает нулевое чтение; len=0. Положительная длина означает eof=false, даже для последнего или ровно заполнившего буфер чанка: EOF подтверждается следующим чтением. При изменении файла другим процессом это наблюдение текущего вызова, не неизменяемое свойство файла.

В отличие от file.read, чтение чанками не ограничивает общий размер файла двумя КиБ и не выполняет дополнительное пробное чтение. Каждый чанк — самостоятельное значение с нулевым хвостом после len. Оно остаётся доступным после следующего чтения и после закрытия файла. View заимствует массив чанка, а не файловый ресурс.

При ошибке возвращается FileFailure без данных. read — число байтов, полученных в буфер данным неуспешным вызовом, не суммарный счётчик файла. Позиция после ошибки определяется ОС; автоматического отката нет. Владение файлом сохраняется: его нужно закрыть или передать дальше.

Закрытие и выходы из областей

file.close(move input) потребляет ресурс и возвращает Ok(0) либо ошибку. В обоих случаях прежняя привязка недоступна. Повторный вызов запрещён: системное закрытие при ошибке уже могло освободить дескриптор. Ошибка закрытия не создаёт новый File и не разрешает повторить попытку.

На каждом нормальном выходе из области все её ресурсы должны быть закрыты или переданы дальше. Проверяются return, завершение блока, break, continue и обратные рёбра циклов. Нельзя перезаписать живой var-ресурс; после передачи можно инициализировать его заново.

Анализ консервативный: продолжающие пути ветвления должны согласовать состояние владения, обратные рёбра цикла — восстановить состояние его входа. Например, закрыть файл только в одной ветке и продолжить после if нельзя. Можно закрыть его в обеих ветках либо закрыть и сразу вернуть результат из одной, сохранив владение на другой. Неисполняемая константная ветка тоже проверяется.

Option<File>, Result<File,E> и пользовательские варианты с ресурсом также move-only. Для именованного варианта нужен match move result; ресурсный payload нельзя отбросить через _. Успешный payload становится новым владельцем. Структуры и массивы с ресурсными полями, частичный move и views ресурсов пока запрещены — для них нужен отдельный контракт разбора составного значения.

Автоматических деструкторов и defer нет. При trap не выполняется раскрутка стека или пользовательское закрытие: программа завершается, дальнейшее освобождение системных handles выполняет ОС. Бесконечное вычисление может удерживать ресурс; гарантия закрытия относится к нормальным выходам, не к завершению любой программы. Ошибка системного закрытия также не означает доказанного освобождения ресурса ОС.

Пример examples/file_stream.liza передаёт бинарный файл в stdout чанками. Он всегда пытается закрыть файл после ожидаемой ошибки чтения или вывода; ранняя ошибка имеет приоритет перед ошибкой закрытия. Коды: 1 — открытие, 2 — чтение, 3 — вывод, 4 — закрытие, 0 — успех. Путь задан литералом.

Стоимость: хранение файлового токена не требует heap или таблицы handles в runtime Liza. Буфер каждого чанка фиксирован, но физические копии значений и нулевое заполнение пока остаются. Это не обещание zero-copy всего I/O.

Правила платформенного закрытия: POSIX close, Windows CloseHandle, закрытие файлов при завершении процесса.

Эталонные программы

Для 0.0.13: бинарный файл → ограниченный буфер → View<u8> → чистый parse_rtp → консольный результат. Ошибка файлового ввода, ParseError и ошибка вывода остаются разными результатами. Пустой файл успешно читается, после чего RTP-парсер возвращает TooShort. Ожидаемые ошибки не вызывают trap; закрытие вызывается по определённому контракту. Передача данных парсеру не копирует весь пакет; это не обещание zero-copy внутри ОС.

Windows

Поддерживается x86-64 Windows с Clang, MSVC Build Tools и Windows SDK. Используйте x64 Native Tools Command Prompt (либо предварительно выполните vcvars64.bat установленной Visual Studio). Python запускается командой python; отдельный Unix-слой не нужен.

python -m liza run examples/hello.liza
python -m liza build examples/hello.liza
build\hello.exe

build добавляет .exe, если суффикс не указан; run сохраняет все 32 бита кода завершения. Windows exception status при trap отличается от POSIX signal status, но trap не превращается в успешный возврат.

При перенаправлении stdout в файл или pipe записываются точные байты, без BOM и без замены LF на CRLF. Для текстового вывода в настоящую консоль runtime использует WriteConsoleW с ограниченным преобразованием UTF-8 в UTF-16. Кодовая страница консоли не меняется. write_bytes остаётся байтовой записью: отображение произвольных байтов терминалом не является декодированием Text.

Счётчик текстового вывода в Windows-консоль выражен в исходных UTF-8 байтах полностью переданных Unicode scalar values. Если консоль откажет посреди UTF-16 surrogate-пары, неполный символ может уже присутствовать в выводе, но в счётчик не включается; атомарность отображения символа не гарантируется. Для файлов и pipe счётчик всегда отражает фактически записанные байты. Закрытый handle возвращает Closed, закрытый pipe — BrokenPipe; нулевая запись в заполненный неблокирующий pipe — WouldBlock.

Разделение консоли и перенаправления следует контрактам WriteConsole и WriteFile.