Раскладка проекта на 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.md — README.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 у себя, может прогонять их тем же набором, не копируя файлы.
Короткий список
- Может быть пример — уезжает в модель или в модуль; не может — остаётся у хозяина.
.fts— то, что правит не только программист;.flang— разбор, строки, коллекции;.mjs— внешний мир.- Имена файлов латиницей через дефис; расширение выбирает разбор.
- Имена внутри — по-русски, из нескольких слов — в ёлочках, форма постоянна.
- Каталог — слой, а не сущность; по файлу видно, какой командой он проверяется.
- Модуль — файл; граница — «что меняется вместе»; граф импортов — дерево.
- Один входной модуль на библиотеку, своей логики в нём нет.
толькоперечисляет имена, а не зависимости; пути относительные.- В
stdlib/проекта — то, что не знает про предметную область. - Пример — часть объявления; у теоремы обязателен встречный снимок.
- Модель проходит
checkбез предупреждений, а не «валидна». - Тест проекта лежит в проекте; в набор его вносит переходник.