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

Основы Liza: как читать и писать программу

Этот документ знакомит с работающим синтаксисом Liza 0.0.20. Сначала — имена, значения и функции, затем — условия, циклы и подробный разбор match. В конце соберём понятия, нужные для чтения примера с Vec.

Каждый блок Liza ниже — самостоятельная программа с main. Сохраните нужный блок в hello.liza и выполните из каталога репозитория:

python3 -m liza run hello.liza

На Windows используйте python вместо python3. Подготовка инструментов описана в быстром старте.

1. Из чего состоит программа

fn main() -> u32 {
    answer: u32 = 42
    if answer == 42 {
        return 0
    }
    return 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 из предыдущей программы:

  1. Выражение после match вычисляется один раз.
  2. По варианту полученного значения выбирается одна ветка.
  3. Если вариант содержит данные, шаблон связывает их с новым именем.
  4. Выполняется только выбранный блок.

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.

Для точных правил и ограничений используйте спецификацию ядра. Импорт, публичность и приватность разобраны в документе о модулях.