flang язык, в котором спецификация исполняется

Раскладка проекта на flang

Читайте с поправкой на 16 августа 2026. Документ писался, когда в репозитории жили два языка: FTS для предметной области и flang для всего остального. Старый проект вынесен (тег fts-pered-udaleniem, дом — github.com/digitable-lol/fts), обе модели примера переписаны на самом языке (lib/fine.flang, lib/loan.flang), и слоя domain/ больше нет. Правила раскладки от этого не изменились — изменилось, чем написан верхний слой: везде, где ниже сказано «FTS» и .fts, сегодня стоит .flang. Что при этом потеряно, названо в шапке lib/loan.flang: право выдать книгу принималось доказательством с сертификатом, а стало обычной функцией.

Этот документ не список пожеланий. Каждое правило здесь выведено из работающего примера — [examples/library-api](../../examples/library-api/README.md), REST-сервиса библиотеки, — и на этот пример ссылается. Ни одного правила, которому не следует сам пример, тут нет.

Пример устроен так:

examples/library-api/
  lib/       flang чистые функции над данными проекта, включая предметные правила
  stdlib/    flang библиотека проекта: не знает про предметную область
  host/      Node  HTTP, хранилище, всё, чего в языке нет
  test/            прогон целиком

1. Три слоя и граница между ними

Правило. Логика уезжает в модель FTS или в модуль flang, если у неё может быть пример. На хозяине остаётся то, у чего примера быть не может.

Почему. Пример — не документация, а исполняемая проверка: fts test и flang test находят их сами и падают, если поведение разошлось. Та же арифметика, написанная в обработчике HTTP, проверяется только тем тестом, который кто-то не поленится написать отдельно.

Где смотреть. В [host/server.mjs](../../examples/library-api/host/server.mjs) нет ни одного числа из тарифа штрафов, нет проверки контрольной цифры ISBN, нет разбора строки запроса и нет условия «кому можно выдавать книги». Всё это лежит в файлах, проверяемых командой.

Что остаётся хозяину и почему именно ему. Функции в flang не являются значениями первого класса — это записано прямым текстом и объяснено как осознанное ограничение ([flang/SPEC.md](../../flang/SPEC.md), раздел 3). Ввода-вывода в языке нет вовсе: среди встроенных форм (разделы 4 и 5) нет ни одной, которая читала бы файл, ходила в сеть или спрашивала время, а встраиваемый режим факт-чекинга запрещает это отдельно (раздел 8). Значит, HTTP, файловая система, время, случайность и хранилище пишутся не на flang — и это устройство, а не недоделка.

Проверить, что граница проведена верно, можно печатью. flang emit печатает библиотеку проекта целиком во все восемь целевых языков — c, go, rust, python, java, csharp, elixir, js, — а host/ не печатается никуда, потому что печатать нечего. Если после переноса очередного куска в lib/ печать сломалась, кусок взят не тот. Тест проекта эту печать сторожит.

Исключение, которое тоже правило. Не переписывай на flang то, что платформа уже делает правильно. Percent-декодирование в примере делает decodeURIComponent хозяина, а не таблица кодовых точек на flang: второй источник истины ради принципа хуже, чем один чужой (см. комментарий у маршрута GET /books).


2. Что кладут в FTS, а что в flang

Правило. В .fts идёт то, что читает и правит не только программист: формы данных, тарифы, правила, свойства, разрешения. В .flang — разбор и сборка данных, строки, коллекции, чистые вычисления.

Почему. Утилита FTS нарочно узкая: у неё есть поля объекта, сравнения, добавить/результат равен, проценты — и всё. Этого хватает, чтобы правило прочла заведующая библиотекой, и не хватает, чтобы посчитать контрольную цифру ISBN. Расширять FTS до этого значило бы потерять то, ради чего он существует.

Где смотреть. [domain/late-fee.fts](../../examples/library-api/domain/late-fee.fts) — четыре правила и предел штрафа, читается без программиста. [lib/isbn.flang](../../examples/library-api/lib/isbn.flang) — разложение строки на символы, свёртка с записью-аккумулятором, остаток от деления: в FTS этого нет и не будет.

**Модель FTS обязана проходить check без предупреждений.** Не «валидна», а именно без предупреждений: FTS_COVERAGE_HOLE означает область входа, где не срабатывает ни одно правило, FTS_PROPERTY_UNATTAINABLE — свойство, до предела которого правила не дотягиваются и которое поэтому не проверяет ничего. В примере обе беды закрыты нарочно: правило «Возврат в срок» закрывает область «просрочки нет», а предел свойства равен 500 — ровно сумме всех надбавок. Причины записаны в шапке модели, а тест [test/library-api.test.mjs](../../examples/library-api/test/library-api.test.mjs) это требование сторожит. Сообщения уровня info (пересечение складывающихся правил) остаются: они верны и ни на что не указывают.

Селектор теоремы выбирает роль записи, а не идентификатор. Соседние примеры репозитория пишут в данных заказы найти где номер равен «ЗК-7781» — для разового доказательства это верно, но сервис такой теоремой пользоваться не может: у каждой заявки свой номер. В [domain/loan-permission.fts](../../examples/library-api/domain/loan-permission.fts) селектор находит запись по этапу — «та заявка, о которой сейчас спрашивают», — и теорема работает для любой заявки, не меняя ни буквы. Ядро требует, чтобы под селектор подошла ровно одна запись, поэтому этап проставляет сервер, а не клиент.

И сам селектор хозяин не повторяет, а читает. Скомпилированный документ несёт готовый путь к свидетельству — ["выдачи", {"этап": "рассмотрение"}, "читатель допущен"], — и из него читаются и раздел снимка, и селектор ([host/fts.mjs](../../examples/library-api/host/fts.mjs), путьТеоремы). Напиши хозяин { выдачи: [{ …, этап: "рассмотрение" }] } руками — модель и сервис хранили бы одну строку в двух местах, а такие пары расходятся всегда.


3. Имена файлов

Правило. Имя файла — латиницей, строчными, слова через дефис. Расширение говорит, чем файл проверяется: .fts, .flang, .mjs.

Почему латиницей. Путь к файлу едет в использует «Модуль» из "путь", в командные строки README, в образцы CI (examples/**/*.fts в [.github/workflows/fts-example.yml](../../.github/workflows/fts-example.yml)), в --out кодогенерации и в URL репозитория. Часть этих мест ломается на пробелах и кириллице молча — а молча ломающийся путь дороже красивого имени. Так уже устроен весь репозиторий: order-shipment.fts, credit-limit.fts, lists.flang.

Почему расширение важнее договорённости. Загрузчик flang выбирает разбор по суффиксу: .fts идёт через мост совместимости, .flang — через парсер языка, .json — как готовый AST (loadProgramFromSource в [flang/bin/flang.mjs](../../flang/bin/flang.mjs)). Файл с неверным расширением разберётся не тем разбором и даст непонятную ошибку.

Имя — про содержание, а не про слой. isbn.flang, а не isbn-module.flang: слой уже назван каталогом.

Спутники модели — тем же именем плюс роль. loan-permission.fts, loan-permission.context.json, loan-permission.blocked.context.json. Снимок без своей модели бесполезен, и по имени сразу видно, к какой он.

Когда иначе. Суффикс, который ищет инструмент, важнее правила: тест называется *.test.mjs, потому что его по этому образцу находит прогонщик. Документ проекта — README.md даже если написан по-русски: так велит правило именования документации в README.ru.mdREADME.md и SPEC.md рядом с кодом остаются под этими именами на том языке, на котором написаны, потому что GitHub показывает их титульной страницей каталога. Суффикс X.ru.md — для документов в docs/, где он отличает язык.


4. Имена внутри файлов

Правило. Имена функций, типов, полей и правил — по-русски. Имя из одного слова можно писать без кавычек, из нескольких — обязательно в ёлочках: «Рассчитать штраф», «дней просрочки».

Почему не латиницей. Поверхность языка русская целиком, и правило читает тот же человек, который читает его название. Смешение алфавитов в одной строке — лишняя работа глазу без единой выгоды. Английская поверхность у FTS есть, но она отдельная и целиком (examples/utilities/discount.en.fts), а не вперемешку.

Почему ёлочки. Имя из нескольких слов иначе не отличить от продолжения конструкции; кавычки — не украшение, а граница токена. Обычные кавычки равноправны, ёлочки выбраны потому, что внутри строковых литералов стоят именно " — и глазу проще, когда имя и строка выглядят по-разному.

Форма имени постоянна. Ядро намеренно не угадывает падежи ([docs/language.ru.md](language.ru.md)), поэтому имя в вызове пишется так же, как в объявлении. Послабление есть только для локальных имён внутри функции flang и только по неизменяемой основе; на имена функций, типов и полей оно не распространяется.

Как называть.

ЧтоФормаПример из проекта
функция-действиеглагольная фраза«Рассчитать штраф», «Разобрать запрос», «Отобрать»
функция-предикатутверждение«Код верен», «Заявка принята»
тип и записьсуществительное«Книга», «Параметр», «Сводка»
поле записистрочными, как в данных«на полке», «дней просрочки»

Имён вроде «Помощник», «Утилиты», «Общее» в проекте нет и быть не может: связывание сливает объявления в одну программу, и **совпадение имён — ошибка FLANG_DUPLICATE_NAME, а не перекрытие** (flang/src/link.mjs). Имя обязано быть узнаваемым во всей собранной программе, а не только в своём файле.

Когда иначе. Наружу, в HTTP, уходят латинские имена: пути /books, /loans, /returns и параметры author, shelf. Их читает клиент, который про модель ничего не знает, и они — часть протокола, а не предметной области. Ключи JSON-тела при этом остаются русскими («код», «на полке»), потому что это поля модели: второй словарь имён пришлось бы держать в согласии вручную, а рассогласование двух словарей не ловит ни один тип ([host/storage.mjs](../../examples/library-api/host/storage.mjs)).


5. Каталоги

Правило. Каталог — это слой, а не сущность. Не книги/ и выдачи/, а domain/, stdlib/, lib/, host/, test/.

Почему. Слой отвечает на два вопроса сразу: кто это правит и чем это проверяется. domain/ правит предметник, проверяется fts check|test; lib/ и stdlib/ правит программист, проверяются flang check|test; host/ — обычный код, у которого этих проверок нет вовсе. Разложи то же самое по сущностям — и в одной папке окажутся три разных инструмента проверки.

Проверка правила. Посмотри на файл и скажи, какой командой он проверяется. Не можешь ответить сразу — файл лежит не там.


6. Модули: где проходит граница

Правило. Один модуль — один файл. Первая строка — модуль «Имя», и это имя обязано совпасть с тем, как модуль импортируют.

Почему. Расхождение — ошибка FLANG_IMPORT_NAME (flang/src/link.mjs): читатель видит одно имя, а получает объявления из другого файла.

Граница модуля — по вопросу «что меняется вместе». Правила отбора книг меняются вместе; формула контрольной суммы ISBN с ними не меняется никогда — поэтому [lib/catalog.flang](../../examples/library-api/lib/catalog.flang) и [lib/isbn.flang](../../examples/library-api/lib/isbn.flang) разные файлы.

Модуль не знает про своих потребителей. [lib/query.flang](../../examples/library-api/lib/query.flang) не знает ни про книги, ни про ISBN: любой проект со строкой запроса взял бы его как есть. Знание про книги живёт этажом выше.

Граф импортов — дерево, слои сверху вниз. Цикл — ошибка FLANG_IMPORT_CYCLE, и это не ограничение, а подсказка: цикл означает, что граница проведена не там. В примере:

api.flang ──> catalog.flang ──> isbn.flang
          │                 └─> flang/stdlib/lists.flang  (только «Сумма», «Максимум», «Длина»)
          └─> query.flang ────> stdlib/text.flang         (только «Первая часть», «Хвост через», «Непустые»)

У библиотеки проекта один входной модуль. [lib/api.flang](../../examples/library-api/lib/api.flang) — единственный файл, который загружает хозяин; всё остальное подтягивает связывание. Причина в устройстве языка: импорт — это слияние объявлений, а не пространство имён, и загрузить два модуля по отдельности значит держать на хозяине две программы и помнить, какая функция в какой.

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


7. Импорт: полный и выборочный

Правило. использует «Модуль» из "путь" вносит все имена модуля. только «А», «Б» вносит перечисленные.

**Главное про только: он перечисляет ИМЕНА, а не замыкание зависимостей.** Взяв так функцию, которая внутри зовёт соседку по модулю, получишь FLANG_UNKNOWN_NAME на первом же обращении — и, если функция объявлена тотальной, ещё и FLANG_NOT_TOTAL вдогонку. Поэтому в примере «ISBN» берётся целиком («Код верен» зовёт «Цифры» и «Контрольную сумму»), а «Списки» и «Текст проекта» — выборочно: там все нужные функции самодостаточны. Обе причины записаны прямо в шапках файлов.

Зачем вообще сужать. Не ради чистоты: конфликт имён при связывании — ошибка, а не молчаливое перекрытие. Модуль на два десятка имён, внесённый целиком, — это два десятка будущих FLANG_DUPLICATE_NAME, и первое же совпадение остановит сборку.

Пути относительные, считаются от каталога файла. "isbn.flang", "../stdlib/text.flang", "../../../flang/stdlib/lists.flang". Абсолютный путь делает проект непереносимым, и в примере его нет ни разу.


8. Библиотека проекта (stdlib/)

Правило входа. Сюда попадает функция, которая не знает про предметную область, но нужна проекту.

Правило выхода наверх. Если функция пригодилась бы всякому, кто пишет на flang, её место не здесь, а в flang/stdlib — с шапкой, примерами и разбором в общем наборе. Библиотека проекта — не свалка «пока не дошли руки».

Почему она вообще нужна. Типы в языке не параметрические, а flang/stdlib/lists.flang объявлен для список числа: «Длина», «Элемент», «Без значения» работают с числами и только с ними. Списку строк — а из него состоит всё, что получается делением строки, — те же функции нужны заново. Это плата за отсутствие параметрических типов, и платить её лучше один раз в одном месте.

Где смотреть. [stdlib/text.flang](../../examples/library-api/stdlib/text.flang) — четыре функции над списком строк, ни одна не знает слова «книга».


9. Примеры

Правило. Пример — часть объявления, а не отдельный файл. пример пишется внутри функции flang и внутри утилиты FTS.

Почему. flang test и fts test находят такие примеры сами и падают на расхождении. Пример, вынесенный в отдельный файл, никем не запускается, пока кто-нибудь не вспомнит.

Что закрывать примерами обязательно. Границу: пустой список, пустая строка, ноль, «не нашли». Такой пример есть у каждой функции проекта, у которой граница вообще бывает: «Пустой каталог», «Пустая строка», «Параметра нет», «Хвоста нет», «Вернули вовремя».

Пример на каждую ловушку, а не на каждую строку. «Разделитель внутри хвоста» в stdlib/text.flang существует потому, что склейка хвоста через = — ровно то место, где ошибаются: разрезав "поиск=a=b", вторым куском получаешь "a", а значением обязано быть "a=b".

Отдельный файл заводят только для того, что примером не выражается. Снимок данных для теоремы — *.context.json, и рядом с ним встречный снимок *.blocked.context.json, на котором вывод обязан не строиться. Доказательство, у которого нет отрицательного случая, ничего не доказывает.

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


10. Прогон и CI

Команды, которыми проверяется проект:

node flang/bin/flang.mjs check examples/library-api/lib/api.flang
node flang/bin/flang.mjs test  examples/library-api/lib/api.flang
node --test examples/library-api/test/library-api.test.mjs

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

Правило. Тест проекта лежит в проекте, а в общий набор подключается переходником.

Почему. Переехав, проект унесёт свои проверки с собой. Но набор в package.json собирается образцом flang/test/*.test.mjs, и заводить ради одного примера второй образец значит править файл, который правят все. Один импорт дешевле: [flang/test/example-library-api.test.mjs](../../flang/test/example-library-api.test.mjs) — десять строк, из них девять комментарий.

Что достаётся бесплатно. Модель .fts, положенная под examples/, автоматически попадает в дифференциальные сверки репозитория — их четыре, и все находят модель образцом examples/**/*.fts, без единой правки списка:

СверкаЧто требует от новой модели
flang/test/compat.test.mjsмост FTS → flang даёт ту же программу
flang/test/core-evaluate.test.mjsвычислитель на flang совпадает с ядром на TypeScript
flang/test/emit-c.test.mjsнапечатанный C собирается и считает так же
flang/test/emit-js.test.mjsто же для JavaScript

Ещё две сверки — core-json.test.mjs и core-parser.test.mjs — берут вообще все .fts репозитория через flang/test/corpus.mjs. Модули .flang так не подхватываются: их прогон подключают руками, тем самым переходником.

Что переносится в чужой проект. Дифференциальная сверка умеет брать модели вне репозитория: каталоги перечисляются в переменной FTS_MODEL_PATH (flang/test/corpus.mjs), и охват печатается в выводе теста. Проект, который держит свои .fts у себя, может прогонять их тем же набором, не копируя файлы.


Короткий список

  1. Может быть пример — уезжает в модель или в модуль; не может — остаётся у хозяина.
  2. .fts — то, что правит не только программист; .flang — разбор, строки, коллекции; .mjs — внешний мир.
  3. Имена файлов латиницей через дефис; расширение выбирает разбор.
  4. Имена внутри — по-русски, из нескольких слов — в ёлочках, форма постоянна.
  5. Каталог — слой, а не сущность; по файлу видно, какой командой он проверяется.
  6. Модуль — файл; граница — «что меняется вместе»; граф импортов — дерево.
  7. Один входной модуль на библиотеку, своей логики в нём нет.
  8. только перечисляет имена, а не зависимости; пути относительные.
  9. В stdlib/ проекта — то, что не знает про предметную область.
  10. Пример — часть объявления; у теоремы обязателен встречный снимок.
  11. Модель проходит check без предупреждений, а не «валидна».
  12. Тест проекта лежит в проекте; в набор его вносит переходник.