Topazdocs
Основные концепции

Модули и видимость

Разделяйте программы на Topaz по файлам с модулями v5.2. Импорты, экспорты, пространства имён, правила видимости, порядок инициализации и ограничения модулей, делающие сборку предсказуемой.

О версии: модули — возможность Topaz v5.2. Каждая однофайловая программа v5.1 остаётся корректной программой v5.2 с неизменным смыслом, когда служит точкой входа сборки — модульный синтаксис добавляет ноль новых зарезервированных ключевых слов. import и export — контекстные головные слова, распознаваемые только в начале списка элементов файла.

Программа на Topaz начинается с одного входного файла. Начиная с v5.2 её можно разделить на файлы: каждый файл .tpz — ровно один модуль, имя которого — путь относительно корня проекта.

Программа из двух файлов

project/
  main.tpz            точка входа
  utils/
    strings.tpz       модуль utils.strings
TOPAZ
// utils/strings.tpz
export function shout(s: string) -> string {
    return "{s}!"
}

export let greeting = "hello"
TOPAZ
// main.tpz
import utils.strings

let line = strings.shout(strings.greeting)
print(line)    // "hello!"

Сборка разрешает utils.strings в utils/strings.tpz под корнем (каталог входного файла, если корень не задан сборке явно). Имена файлов должны совпадать с путём модуля точно — разрешение чувствительно к регистру и отвергает имена файлов, совпадающие с точностью до нормализации Юникода или сведения регистра, поэтому юнит, который собирается на одной машине, собирается одинаково на любой файловой системе.

Импорты

В Topaz ровно две формы импорта.

Форма A — импорт пространства имён

TOPAZ
import utils.strings

let s = strings.shout("hi")

import utils.strings связывает одно имя: последний сегмент, strings. Точечный путь — адрес, а не выражение: utils сам по себе не связывается, и неявного родительского модуля нет.

Форма B — выборочный импорт

TOPAZ
import utils.strings { shout, greeting }

let s = shout(greeting)

Выборочный импорт связывает перечисленные экспортированные имена напрямую.

Псевдонимы через as

Обе формы принимают as, чтобы переименовать связываемое:

TOPAZ
import utils.strings as str
import net.url { encode as encodeUrl }

let s = str.shout(encodeUrl("a b"))

Псевдоним — единственное связываемое имя. Слоты не сочетаются: import m as ns { x } — не синтаксис v5.2.

Правила импорта

  • Импорты образуют пролог: каждый импорт стоит раньше всех остальных элементов верхнего уровня файла.
  • Модуль может встречаться не более чем в одном импортирующем элементе на файл — дважды формой A, дважды формой B или вперемешку: всё это ошибки дублирующего импорта.
  • Внутри одного списка импорта нельзя дважды выбрать одно исходное имя, и две позиции не могут дать одно локальное имя.
  • Импорт модуля, который ничего не экспортирует, — ошибка: импортов «только ради побочных эффектов» в Topaz нет.
  • Корни путей std и topaz зарезервированыimport std.io — статическая ошибка. Встроенная поверхность — прелюдия (поверхность v5.1 §22 без изменений), импорт ей не нужен.

Экспорты и видимость

Всё на верхнем уровне модуля приватно, пока не экспортировано. Уровней видимости ровно два, а написание экспорта одно: встроенное export перед объявлением.

TOPAZ
export function area(w: float, h: float) -> float { return w * h }

export type Size = { w: float, h: float }

export let defaultSize: Size = { w: 1.0, h: 1.0 }

export const maxSide = 4096

function clamp(v: float) -> float {    // приватный помощник
    if v > 4096.0 { return 4096.0 }
    return v
}

export — обёртка с нулевой стоимостью времени выполнения: объявление ведёт себя ровно так, как без экспорта. Правила:

  • Экспортируемый let связывает ровно один идентификатор. Экспорт с деструктуризацией, подстановочным или опровержимым паттерном — статическая ошибка.
  • export let mut — статическая ошибка. Приватный для модуля let mut законен; изменяемая ячейка привязки никогда не входит в публичную поверхность модуля (экспортированная неизменяемая привязка может при этом содержать значение, которое само изменяемо).
  • Импортированные привязки только для чтения: присваивание импортированному имени или через пространство имён (strings.greeting = "x") — статическая ошибка.
  • Тип, названный в экспортированной сигнатуре, аннотации экспортированной привязки или теле экспортированного псевдонима, должен сам быть публично разрешимым — примитив или тип прелюдии, экспортированный псевдоним того же модуля либо квалифицированный пространством имён экспортированный тип импортированного модуля. Приватный псевдоним не может протечь через публичную поверхность.

Использование пространства имён

Привязка формы A — объект разрешения времени компиляции, а не значение. Она может стоять ровно в двух позициях — головой доступа к члену в выражении или головой квалифицированного типа:

TOPAZ
// ui/theme.tpz
export type Style = { bold: bool }

export let defaultStyle: Style = { bold: false }
TOPAZ
// main.tpz
import ui.theme

let s: theme.Style = theme.defaultStyle    // ns.Type и ns.member
let b = theme.defaultStyle.bold            // дальше обычный доступ
  • Поиск в пространстве имён потребляет ровно одно имя члена; всё после него — обычный доступ к членам разрешённого значения: в theme.defaultStyle.bold пространство имён разрешает defaultStyle, а .bold читает поле записи.
  • Член должен быть экспортированным именем. Экспортированные типы используются в позиции типа, экспортированные значения и функции — в позиции выражения: let x = theme.Style — ошибка.
  • Само пространство имён нельзя передать, сохранить или вернуть: let n = theme — ошибка.
  • Локальные и вложенные области видимости затеняют имена верхнего уровня модуля по обычным правилам; разрешение имён смотрит сначала локальные области, затем верхний уровень модуля, затем прелюдию.

Инициализация

Импортированные модули инициализируются жадно, ровно один раз, до первого не-импортного элемента точки входа. Порядок детерминирован: между модулями — порядок зависимостей (сначала зависимости; ничьи — лексикографически), внутри модуля — сверху вниз. Элементы const — этап компиляции, без шага времени выполнения.

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

TOPAZ
// config.tpz — отвергается
export let cache = compute()         // ошибка: достигает `limit`,
export let limit = 100               // объявленного после `cache`

function compute() -> int { return limit * 2 }

Перестановка двух привязок устраняет ошибку. Благодаря этому правилу (плюс отказ от циклов и детерминированный порядок) ни один модуль никогда не наблюдает частично инициализированный модуль — «временной мёртвой зоны» во время выполнения нет.

Ещё два факта об импортируемых файлах:

  • На их верхнем уровне допустимы только импорты, объявления и привязки. Свободные инструкции, которые что-то делают, — инструкции-выражения, присваивания, while, defer, return, break, continue — в импортированном модуле являются статическими ошибками. Входной файл сохраняет всю свободу v5.1. Один и тот же файл может быть корректной точкой входа и некорректным импортом; диагностика показывает цепочку импортов от точки входа.
  • Фолт во время инициализации прерывает программу; перехватить его нельзя.

Циклы — ошибки

Каждый импорт создаёт ребро в графе импортов, и каждый цикл импортов — статическая ошибка, включая модуль, импортирующий сам себя, и независимо от того, «только ли типы» импортируются. Диагностика сообщает один канонический путь цикла на каждую группу взаимно импортирующих модулей, поэтому одна и та же ошибка сборки читается одинаково везде.

Чего в модулях v5.2 нет

Модульная система v5.2 намеренно мала:

  • Ни менеджера пакетов, ни манифестов — один корень, одна точка входа, пути под корнем. Файлы-манифесты не имеют языкового смысла.

  • Нет синтаксиса реэкспортаexport import, списки экспорта (export { a, b }) и подстановочные экспорты отвергаются. Узкий API можно переслать вручную обычными экспортами:

    TOPAZ
    import ui.theme
    
    export let defaultStyle = theme.defaultStyle
    export type Style = theme.Style

    Канонический стиль всё равно предпочитает импорт определяющего модуля напрямую.

  • Нет элементов useuse распознаётся и отвергается отдельной диагностикой; слово зарезервировано.

  • Нет строковых путейimport "utils/strings" отвергается; пути модулей — идентификаторы через точку.

Эти формы диагностируются как модульный синтаксис (вы получите модульную ошибку, а не сбивающую с толку ошибку базового синтаксиса), но частью языка они не являются.