Основы Liza: как читать и писать программу
Этот документ знакомит с работающим синтаксисом Liza 0.0.20.
Сначала — имена, значения и функции, затем — условия, циклы и подробный
разбор match. В конце соберём понятия, нужные для чтения примера с Vec.
Каждый блок Liza ниже — самостоятельная программа с main.
Сохраните нужный блок в hello.liza и выполните из каталога репозитория:
На Windows используйте python вместо python3.
Подготовка инструментов описана в быстром старте.
1. Из чего состоит программа
fnобъявляет функцию.main— имя точки входа программы.()— список параметров; здесь он пуст.-> u32— тип результата функции: беззнаковое 32-битное целое.{ ... }— блок инструкций.answer: u32 = 42связывает имяanswerсо значением42типаu32.==сравнивает значения, а=связывает имя или выполняет присваивание.returnнемедленно завершает функцию с указанным результатом.
В main результат становится кодом завершения процесса. Обычно 0 обозначает
успех. return 42 не печатает число 42 в консоль.
Инструкции можно разделять переносами строк или ;. Комментарий начинается
с // и продолжается до конца строки. Пока заголовок функции или выражение
нужно писать на одной строке; открывающая { следует на той же строке.
2. Имена, типы и var
fn main() -> u32 {
count = 10
small: u8 = 42
offset: i32 = -3
enabled: bool = true
var current: u32 = count
snapshot = current
current = current + 1
if enabled && current == 11 && snapshot == 10 && small == 42 && offset < 0 {
return 0
}
return 1
}
Без var привязка неизменяема: повторно присвоить count новое значение нельзя.
var current разрешает последующие присваивания, но тип остаётся прежним.
Писать var повторно при присваивании не нужно.
snapshot = current сохраняет отдельное числовое значение. Изменение current
не меняет snapshot.
| Тип | Значения |
|---|---|
u8, u16, u32, u64 |
Целые без знака, ширина указана в битах |
i8, i16, i32, i64 |
Целые со знаком |
bool |
Только true или false |
Text |
Текстовые литералы UTF-8; это ещё не динамическая строка |
Аннотацию можно опустить, когда тип выводится из контекста.
Для целого литерала без контекста используется u32: поэтому count = 10
даёт u32. Для отрицательного числа указывайте знаковый контекст, как у offset.
Между уже типизированными целыми нет неявных преобразований. Например,
расширение small до u32 записывается как u32(small).
Литерал 256 в привязке u8 — ошибка компиляции.
Обычные +, -, * проверяют переполнение: нарушение диапазона вызывает
trap, то есть прекращение нормального выполнения. Это не переход к Err.
Режимы wrapping, насыщения и преобразований описаны в
числовой модели.
3. Функции и if
fn is_adult(age: u32) -> bool {
return age >= 18
}
fn main() -> u32 {
age: u32 = 20
if is_adult(age) {
return 0
} else {
return 1
}
}
age: u32 в объявлении функции — параметр. Вызов is_adult(age) передаёт
значение и получает результат bool. Область имени параметра — тело функции.
if выполняет блок, когда условие равно true. else выполняется при false;
его можно опустить. Поддерживается и else if.
Условие обязательно имеет тип bool: запись if age { ... } недопустима.
Неисполняемая ветка не вычисляется.
Основные операции над условиями:
| Запись | Значение |
|---|---|
a == b, a != b |
Равно, не равно |
a < b, a <= b, a > b, a >= b |
Сравнения |
!condition |
Логическое отрицание |
left && right |
Оба условия истинны; правое вычисляется только при истинном левом |
left || right |
Хотя бы одно истинно; правое вычисляется только при ложном левом |
Имена, объявленные внутри блока, недоступны после его завершения.
Переобъявлять уже видимое имя нельзя; для изменения существующей привязки
используется присваивание к var.
4. Цикл while, continue и break
fn main() -> u32 {
var i: u32 = 0
var sum: u32 = 0
while i < 10 {
i = i + 1
if i == 2 {
continue
}
if i == 5 {
break
}
sum = sum + i
}
if sum == 8 {
return 0
}
return 1
}
while проверяет условие перед каждой итерацией. Если оно сразу ложно,
тело не выполнится ни разу.
Здесь суммируются 1 + 3 + 4:
continueприi == 2пропускает остаток тела и возвращает к проверкеi < 10.breakприi == 5выходит из ближайшего цикла.returnзавершил бы всю функцию, а не только цикл.
Вложенные циклы допустимы. for пока не входит в реализованный синтаксис.
5. Option<T>: значение может отсутствовать
Option<T> означает «либо значение типа T, либо отсутствие значения».
У него два варианта:
| Вариант | Что содержит |
|---|---|
Some(value) |
Одно значение типа T |
None |
Ничего |
fn first(data: View<u32>) -> Option<u32> {
if data.len == 0 {
return None
}
return Some(data[0])
}
fn main() -> u32 {
numbers: [u32; 2] = [42, 7]
data = view numbers[0..2]
item = first(data)
match item {
Some(value) => {
if value == 42 {
return 0
}
return 1
}
None => {
return 2
}
}
}
Здесь массив содержит два числа, а data даёт readonly-доступ к ним.
first сначала проверяет длину. При пустом входе она возвращает None,
при непустом — Some с первым элементом. Таким образом, отсутствие элемента
обрабатывается без выхода за границы и без trap.
T в Option<T> — место для конкретного типа. В данном примере используется
Option<u32>, поэтому внутри Some находится число u32.
6. Как читать match
Разберём match item из предыдущей программы:
- Выражение после
matchвычисляется один раз. - По варианту полученного значения выбирается одна ветка.
- Если вариант содержит данные, шаблон связывает их с новым именем.
- Выполняется только выбранный блок.
Some(value) => { ... } читается так: «если получен Some, назвать его
содержимое value и выполнить этот блок».
Здесь Some(value) — шаблон разбора, а не вызов функции.
Имя value выбирает программист; оно доступно только в этой ветке.
None => { ... } ничего не извлекает, потому что у None нет содержимого.
Стрелка => отделяет шаблон от блока.
Компилятор требует обработать все варианты. Для Option нужны и Some,
и None; забытая или повторная ветка — ошибка компиляции.
Проваливания в следующую ветку нет, break в конце ветки не нужен.
Если блок ветки закончился без return, выполнение продолжается после всего
match. Если в нём выполнен return, завершается функция.
В текущей Liza match — инструкция над вариантами типов. Он не возвращает
значение как выражение: запись x = match ... не поддерживается.
Числовые шаблоны вроде 42 => ..., guards и универсальная ветка _ => ...
тоже не входят в текущую версию. Для проверки числового условия используйте if.
7. Result<T, E>: успех или ожидаемая ошибка
Result<T, E> похож по форме на Option, но вместо простого отсутствия
содержит причину ошибки:
| Вариант | Что содержит |
|---|---|
Ok(value) |
Успешный результат типа T |
Err(error) |
Ошибку типа E |
enum InputError {
TooLarge
}
fn checked_byte(value: u32) -> Result<u8, InputError> {
if value > 255 {
return Err(InputError.TooLarge)
}
return Ok(u8(value))
}
fn main() -> u32 {
result = checked_byte(300)
match result {
Ok(value) => {
return 1
}
Err(error) => {
match error {
InputError.TooLarge => { return 0 }
}
}
}
}
InputError — пользовательский тип с одним вариантом TooLarge.
checked_byte явно проверяет диапазон до преобразования к u8.
Для входа 300 она возвращает Err(InputError.TooLarge).
Пример намеренно проверяет ошибочный вход, поэтому ожидаемая ветка ошибки
возвращает код завершения 0.
Err — обычный вариант значения. Он не бросает исключение, не вызывает trap
и сам не завершает функцию. Решение принимает код, который разбирает результат.
Option удобно использовать, когда достаточно ответа «нет элемента».
Result нужен, когда вызывающей стороне важна причина отказа.
8. Свои варианты: enum
enum Event {
Connected(u32)
Disconnected
}
fn handle(event: Event) -> u32 {
match event {
Event.Connected(user_id) => {
if user_id == 42 {
return 0
}
return 1
}
Event.Disconnected => {
return 2
}
}
}
fn main() -> u32 {
return handle(Event.Connected(42))
}
В каждый момент Event содержит ровно один вариант. Connected несёт число,
а Disconnected не содержит данных. match проверяет вариант и предоставляет
доступ именно к его содержимому.
Это тот же принцип, на котором устроены Some/None и Ok/Err.
Имена собственных вариантов записываются с именем типа: Event.Connected.
9. Структуры, массивы и readonly view
struct Point {
x: i32
y: i32
}
fn main() -> u32 {
var point = Point(10, 20)
original = copy point
point.x = 30
numbers: [u32; 3] = [10, 20, 30]
part = view numbers[1..3]
if point.x == 30 && original.x == 10 && part.len == 2 && part[0] == 20 {
return 0
}
return 1
}
Структура хранит все объявленные поля одновременно. Point(10, 20) задаёт
поля в порядке объявления; point.x читает поле. Обновление поля разрешено
только у доступной var-привязки без действующего readonly-заимствования.
copy point создаёт независимое логическое значение: изменение point.x
не меняет original.x.
[u32; 3] — массив из трёх элементов; длина входит в его тип.
Индексация начинается с нуля. Индексы и границы диапазона имеют тип u32.
Неверный индекс вызывает trap.
view numbers[1..3] предоставляет доступ без копирования содержимого.
Начальная граница включается, конечная — нет: в part видны 20 и 30.
Индексы представления отсчитываются заново, поэтому part[0] — это 20.
View не владеет исходным массивом. Пока действует заимствование, владелец защищён от изменения и перемещения. В первой модели это ограничение действует до конца лексической области заимствования, даже после последнего чтения view.
10. copy и move: кто может использовать значение
fn identity<T>(value: T) -> T {
return move value
}
fn main() -> u32 {
var current: u32 = 10
snapshot = copy current
next = identity(move current)
current = 20
if snapshot == 10 && next == 10 && current == 20 {
return 0
}
return 1
}
copy current сохраняет исходную привязку доступной.
move current передаёт значение и прекращает возможность читать через прежнее
имя. Присваивание current = 20 снова инициализирует изменяемую привязку.
Это правило действует даже для числа: смысл move — передача права
использования, а не обещание конкретной машинной инструкции.
Для копируемых чисел обычное snapshot = current тоже разрешено.
identity<T> — generic-функция. Компилятор выводит здесь T = u32 из аргумента.
Для ресурсов, например File или vec.Vec<T>, копирование запрещено,
а передача требует move. На нормальном выходе ресурс нужно завершить
или передать дальше.
Поэтому перед match иногда встречается move:
если результат содержит ресурс, его разбирают с передачей владения —
match move result. В предыдущих примерах Option<u32> и
Result<u8, InputError> копируемы, поэтому move им не требовался.
11. Консоль и эффекты
fn main() -> u32 : io {
match console.println("Hello, Liza!") {
Ok(written) => {
return 0
}
Err(error) => {
return 1
}
}
}
Теперь программа действительно печатает текст. console.println возвращает
Result: вывод может завершиться ошибкой, которую нужно учитывать.
Имена written и error связывают данные соответствующих вариантов.
Здесь достаточно выбрать код завершения; выводить саму ошибку необязательно.
Обозначение после типа результата задаёт разрешённые эффекты функции:
| Запись | Что разрешает |
|---|---|
Без аннотации или : pure |
Вычисления без io и alloc |
: io |
Внешние операции, например консоль и файлы |
: alloc |
Операции выделения и управления региональным хранилищем |
: io,alloc |
Оба вида эффектов |
Эффект проверяется и через вызовы: pure-функция не может скрыто вызвать
io-функцию. При этом pure не означает «невозможен trap» или «функция всегда
завершится».
Шпаргалка перед примером Vec
| Запись | Как читать |
|---|---|
name: Type = value |
Привязать имя к значению указанного типа |
var name = value |
Разрешить последующее присваивание этому имени |
fn f(x: T) -> U |
Функция принимает T и возвращает U |
fn f<T>(x: T) -> T |
Generic-функция с параметром типа T |
match value |
Выбрать блок по варианту значения |
Ok(x) => { ... } |
В успешной ветке связать содержимое с именем x |
return x |
Завершить всю функцию |
move x |
Передать значение, прекратив доступ через прежнюю привязку |
(next, item) = expression |
Целиком распаковать линейный результат в две новые привязки |
Последняя конструкция понадобится для Vec. Его операции возвращают владельца вместе со статусом или элементом; ресурсная обязанность не должна потеряться при распаковке. Это уже следующий шаг: построчный разбор программы с Vec.
Для точных правил и ограничений используйте спецификацию ядра. Импорт, публичность и приватность разобраны в документе о модулях.