Минимальный 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. Запуск из корня проекта:
Коды завершения примера: 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-слой не нужен.
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.