Как читать репозиторий и как участвовать
Язык суточной давности — плохой выбор для продакшена и хороший для чтения. Всё помещается в один каталог, слои не переплетены, и почти каждое решение объяснено в комментарии рядом с кодом. Эта статья — про то, как этим воспользоваться.
Что где лежит
flang/
SPEC.md спецификация языка
bin/flang.mjs CLI: check | run | test | facts | ast | emit | io | repl
bin/flang-lsp.mjs языковой сервер
src/
lexer.mjs текст → токены (INDENT/DEDENT)
parser.mjs токены → AST
types.mjs проверка и вывод типов, исчерпывающность разбора
totality.mjs анализ структурного убывания
interpret.mjs вычисление AST
builtins.mjs строки, списки, числа, представление значений
link.mjs связывание модулей
factcheck.mjs встраиваемый факт-чекинг
compat.mjs мост: FtsDocument → AST flang
defunc.mjs дефункционализация: один проход перед всеми восемью бэкендами
tags.mjs список тегов — одно определение на анализ и вычислитель
io.mjs словарь поручений и исполнитель плана; host/node.mjs — хозяин
conc.mjs процессы, надзор, планировщик прогонов
monoid.mjs законы моноида и группы на конечной сетке
repl.mjs оболочка «flang repl»
lsp.mjs языковой сервер; вход — bin/flang-lsp.mjs
emit/{c,csharp,elixir,go,java,js,python,rust}.mjs печать в целевые языки
stdlib/ пять модулей на самом flang
core/ ядро FTS, переписанное на flang
self/ компилятор flang, переписанный на flang
cat/ контракты: теоркат (SPEC.md), высший порядок (HOF.md), полиморфизм (POLY.md)
conc/ контракт конкурентности и шесть программ с процессами
examples/ решения LeetCode, Rosetta Code, ввод-вывод, проба импорта
test/ 50 файлов тестов и glob.mjs — своя выборка файлов
Каталог src/ заметно вырос с прошлой редакции — семь новых модулей за сутки, —
и по этому списку видно, что именно приехало в язык: ввод-вывод, процессы,
функции первого класса, законы моноида, оболочка и языковой сервер.
Рядом с каждым emit/<язык>.mjs, кроме js.mjs, лежит каталог того же имени с
рантаймом и прогонщиком на целевом языке: emit/c/, emit/go/, emit/python/,
emit/rust/, emit/java/, emit/csharp/, emit/elixir/.
Два каталога легко перепутать, а путать их нельзя. core/ — это ядро FTS,
написанное на flang: сделано, целиком тотально, сверено побайтово. self/ — это
сам компилятор flang, написанный на flang: сознательно не тотален, сверен
побайтово по слоям и собран в одну программу, у которой сошлась неподвижная
точка. У каждого свой контракт —
core/SPEC.md и self/SPEC.md, — и оба стоит читать (глава «Ядро FTS на
flang»).
Третий и четвёртый каталоги контрактов появились за сутки и стоят чтения не
меньше: в cat/ лежат SPEC.md (теоркат), HOF.md (функции первого класса) и
POLY.md (полиморфизм), в conc/ — контракт модели конкурентности. Жанр у них
один и редкий: не «как пользоваться», а «что решено, что отвергнуто и по какому
измерению».
Сверяясь со спецификацией, держите в голове: раздел 6 SPEC.md перечисляет
слои без link.mjs и без self/, из бэкендов знает один emit/js.mjs, а
команду fmt называет существующей. Код новее документа, и разрыв растёт.
Маршрут чтения
Если читать подряд, естественный порядок такой.
flang/SPEC.mdцеликом — 18 КБ, полчаса. Разделы 1 (два класса), 3 (типы) и 5 (AST) — обязательны, остальное по интересу. Раздел 10, «известные ограничения», прочитайте обязательно: он задаёт тон.flang/src/totality.mjs, первые шестьдесят строк. Не код, а объяснение метода — с контрпримером, доказывающим, почему нужна единая убывающая позиция. Лучший текст в репозитории.flang/stdlib/optional.flang— 169 строк, читается как статья. Тип, объяснение зачем, и разбор двух ограничений, которые видны прямо в коде модуля. Читать его стоит именно сейчас: оба ограничения за сутки перестали быть свойствами языка, и шапку переписали — не задним числом, а с прямым «здесь стояло … это больше не правда». Образец того, как правят такие комментарии.flang/src/interpret.mjs, шапка — почему явный стек кадров вместо рекурсии по стеку JS.flang/src/emit/go.mjs, шапка — шесть пунктов «где расходятся сами языки». Полезно даже тем, кому flang безразличен. Следом — шапкиemit/rust.mjsиemit/python.mjs: тот же жанр, но про другие языки, и вместе три файла читаются как сравнительный разбор.flang/core/SPEC.md— если интересно, как на этом языке пишут настоящий парсер. Раздел «Долги» занимает больше половины и стоит того.flang/self/SPEC.md— почему компилятор языка переписан на самом языке, чем это отличается от переписывания ядра FTS и как выглядит критерий готовности, если проверять нечем, кроме самой проверяемой программы. Читается за двадцать минут и меняет взгляд на предыдущие шесть пунктов.flang/src/emit/c/flang_runtime.c, шапкаfl_b_dobavit— один абзац про то, во что обходится «дописать элемент в список» там, где память никто не отдаёт. Полезен и вне flang: это про любой рантайм без сборщика мусора.
Файлы, которые не стоит читать целиком: flang/src/parser.mjs, 2903 строки;
flang/core/parser.flang, 2675 строк на самом flang; flang/self/parser.flang,
3929 строк там же. Туда лучше ходить с конкретным вопросом — и первым делом в
раздел «Долги» соответствующего SPEC.md, где почти на любой вопрос «почему так
странно» уже есть ответ.
Запуск
Клон, и всё:
git clone https://github.com/digitable-lol/flang.git
cd flang
node flang/bin/flang.mjs check flang/stdlib/lists.flang --pretty
node flang/bin/flang.mjs test flang/stdlib/optional.flang --pretty
Ни npm install, ни сборки. В README.md заявлен Node.js 20 или новее; CI
проверяет на 20, 22 и 24.
Тесты
node --test flang/test/*.test.mjs
Общее число тестов мы здесь больше не приводим, и на это есть причина. В прошлой редакции стояло «1277 тестов языка»; за сутки тестовых файлов стало 50 вместо 27, и любое число, записанное в статью, устареет быстрее, чем вы её дочитаете. Вместо итога — прогоны отдельных файлов, которые мы сделали сами и которые проверяют самое новое в языке:
$ node --test flang/test/self-bootstrap.test.mjs
ℹ неподвижная точка сошлась: 7 файлов совпали побайтово у эталона, flang₁ и flang₂
ℹ tests 12 · pass 12 · fail 0 · duration_ms 131581
$ node --test flang/test/emit-elixir-conc.test.mjs
ℹ программ: 6, прогонов: 11, запусков на BEAM: 275, семян у эталона на прогон: 1000
ℹ tests 7 · pass 7 · fail 0
$ node --test flang/test/hof-emit.test.mjs
ℹ tests 18 · pass 18 · fail 0
$ node --test flang/test/cat-*.test.mjs flang/test/hof.test.mjs \
flang/test/poly.test.mjs flang/test/io.test.mjs flang/test/missing.test.mjs
ℹ tests 144 · pass 144 · fail 0
Сколько увидите вы — зависит и от вашей машины, и это устройство, а
не случайность. Тест бэкенда, который умеет собрать и запустить напечатанный
код, делает именно это; если тулчейна нет, случай пропускается с причиной, а
не объявляется зелёным. У нас пропускались тесты бэкенда Go — в выводе у каждого
стоит «тулчейн Go не найден (ни в PATH, ни в FTS_TOOLCHAIN_PATH) — пропуск», — а
Rust, Python, Java и Elixir нашлись, и напечатанный код действительно собирался и
запускался настоящим процессом. Сверка с BEAM без установленного elixirc
пропустилась бы точно так же.
Приём этот стоит перенять. Пропуск с причиной виден в выводе и потому осознаётся; тест, который «проверяет» напечатанный код, не собирая его, выглядит так же, как настоящий, и молчит ровно там, где должен был бы кричать.
Заложите время, и заложите щедро. Шесть файлов из пятидесяти — все self-* —
гоняют компилятор, написанный на flang, поверх интерпретатора, написанного на
JavaScript. По отдельности они идут минутами: у нас один только
self-bootstrap.test.mjs занял 131 секунду, и это на машине, где параллельно шли
чужие сборки. Прибавьте emit-elixir-conc, который тысячу раз прогоняет эталон
на сетке семян и 275 раз запускает BEAM (21 секунда), — и станет понятно, почему
прогон целиком лучше не ставить в цикл «поправил — проверил».
Лимиты в этих файлах подняты до сорока–четырёхсот миллионов шагов не для запаса:
лимит там проверяет, что разбор остался линейным по числу токенов, а не свалился
в квадрат (глава «Ядро FTS на flang»).
Оговорка про сборку существенна. Девять тестовых файлов из пятидесяти
сверяются с ядром FTS напрямую — bin-through-symlink, compat,
core-evaluate, core-json, core-parser, emit-c, emit-js, factcheck,
interpret, — и импортируют dist/src/index.js. На свежем клоне без сборки они
дают ERR_MODULE_NOT_FOUND, а не «ошибку теста»:
npm install && npm run build
npm run test:flang
Скрипт test:flang — это ровно node --test flang/test/*.test.mjs; npm test
прогоняет ядро, инструменты и flang.
Остальные сорок один файл работают без сборки, потому что сверяются они не с
ядром FTS, а с эталонными flang/src/*.mjs. Мы на этом попались: без сборки
падает ровно один случай, и не в тех девяти файлах, а в self-emit-c — там
единственная проверка «модели FTS через compat» тоже тянет dist динамическим
импортом. Остальные тесты того же файла, включая печать собственного исходника,
идут без сборки. Например:
$ node --test flang/test/totality.test.mjs
ℹ tests 25
ℹ pass 25
ℹ fail 0
И одна практическая мелочь, на которую мы напоролись. Шаблон
flang/test/*.test.mjs берёт всё, что лежит в каталоге, включая файлы,
которые ещё не закоммичены. У нас в прогон попали два таких, и один из них,
недописанный, дал два падения с ReferenceError. К языку это отношения не
имело; но если у вас «упало два теста», сперва посмотрите git status.
И вторая мелочь, того же жанра. Если считаете что-нибудь по каталогу поиском —
grep -c, grep -l, — проверьте, чем именно вы ищете. Файл
flang/test/self-emit-c.test.mjs содержит нулевой байт: это образец, на котором
проверяется экранирование строк в C. Часть поисковых утилит от этого считает
файл двоичным и молча его пропускает, отвечая ровно тем же, чем ответила бы на
честное отсутствие совпадений, — пустым выводом и кодом возврата 1. Ключ -a
снимает вопрос. Мы на этом ошиблись в соседней главе и разобрали случай там
(глава «Ядро FTS на flang»).
Обратный случай: тесты, которых не было видно
Пропуск с причиной — это то, как надо. Как не надо, тот же репозиторий показал через несколько часов после нашего прогона, и случай стоит разобрать, потому что зелёным при этом выглядело задание, от которого зависела публикация.
globSync появился в node:fs только в Node 22. Семь тестовых файлов
импортировали его оттуда напрямую — а пакет обещает Node ≥ 20 в engines, CI
гоняет матрицу 20, 22, 24, и publish-npm.yml поднимает именно 20. На Node 20
эти семь файлов не загружались вовсе: падал не отдельный случай, а весь файл, до
первой проверки. В задании test (20) шло 920 тестов вместо 1277 — 357
случаев не исполнялись там ни разу, — и семь «падений», которые были не
падениями, а отказами загрузки.
Ломался при этом не язык. Сам flang на Node 20 работает целиком, остальные 913
случаев там проходили, и обещание >= 20 было верным. Поэтому починили выборку
файлов, а не engines: в flang/test/glob.mjs появился свой globSync на
readdirSync — те же образцы, тот же вид результата, — и в семи файлах
поменялась одна строка импорта.
Разница между этим случаем и пропуском бэкенда Go — вся разница между честной и нечестной оснасткой. Пропуск говорит, чего он не проверил, и стоит в выводе отдельной строкой. Файл, не дошедший до первой проверки, не говорит ничего: он просто уменьшает итоговое число, а итоговое число никто не помнит наизусть.
Что говорят AGENTS.md и CONTRIBUTING.md
Почти ничего — и это единственное место в треке, где нам пришлось откатить собственное утверждение назад.
В прошлой редакции здесь было написано, что оба файла переписали и они наконец
знают про два языка: AGENTS.md открывался словами «этот репозиторий содержит
два языка», у CONTRIBUTING.md появился раздел «Changes to flang must include»
из четырёх требований. На момент этой сверки ни того, ни другого в репозитории
нет. AGENTS.md снова начинается строкой «This repository implements the Formal
Type Surface language», слова flang в нём нет ни разу, а CONTRIBUTING.md
по-прежнему требует «compatibility notes in MIGRATION.md» — файла, которого в
репозитории не существует.
Мы не знаем, откат это или потеря при переписывании истории, и гадать не будем;
git log по обоим файлам показывает, что последний раз их трогали задолго до
появления flang. Практический вывод один: правила разработки flang в
документах не записаны, их по-прежнему приходится вычитывать из кода.
Заодно это хорошая иллюстрация к тому, о чём весь трек. Документ может не только отстать — он может отъехать назад, и заметить это можно единственным способом: открыв его, а не сославшись на память о том, что там было вчера.
Правила, которых репозиторий держится последовательно, из кода тем не менее вычитываются, и им разумно следовать.
AST — контракт, менять нельзя мимоходом. Раздел 5 спецификации начинается словами: парсер выдаёт ровно это, тайпчекер, интерпретатор и кодогенераторы читают только это, «ни один слой не разбирает текст повторно».
Коды и тексты диагностик — наблюдаемое поведение. В бэкендах записано, что сообщения копируются буквально, вплоть до кавычек-ёлочек, потому что «код и текст — часть наблюдаемого поведения, а не украшение». Изменение текста ошибки — изменение поведения.
Изменение в языке — изменение в девяти местах. Интерпретатор и восемь
бэкендов обязаны совпадать. Добавили встроенную форму — реализуйте её в
builtins.mjs и во всех восьми emit/*.mjs, причём везде, кроме JS, ещё и в
рантайме на целевом языке. Цена одной встроенной формы за трое суток выросла
почти вдвое, и это, пожалуй, главное практическое следствие восьми бэкендов:
каждая новая цель — не только новая ниша, но и постоянный налог на любое
изменение в языке. И налог этот считается не по девяти местам, а по всем: у семи
бэкендов из восьми есть ещё рантайм на целевом языке, а слои самоприменения из
flang/self/, которые ту же форму знают, сверяются с эталоном побайтово — то
есть разойтись им нельзя.
Оговорка, без которой счёт получается слишком мрачным. Две крупнейшие правки последних суток — функции первого класса и параметрический полиморфизм — стоили бэкендам по одной строке каждая и ноль строк соответственно. Полиморфизм не стоил ничего, потому что все восемь целей типы и так стирают; функции первого класса — потому что дефункционализация легла одним проходом ПЕРЕД печатью, и в каждом бэкенде стоит ровно строка его вызова. Налог платится за расширение СЕМАНТИКИ значений, а не за расширение языка вообще.
Совместимость с FTS проверяется, а не декларируется. Любая существующая FTS-модель обязана оставаться валидной программой flang с тем же результатом.
Отвергнутую альтернативу записывают рядом. Это не формальное требование, но стиль выдержан по всему репозиторию: почти каждое нетривиальное решение сопровождается объяснением, что рассматривалось и почему не подошло. Правка в этом стиле будет читаться как своя.
Долг оформляется явно. Раздел «Долги» есть и в flang/core/SPEC.md, и в
flang/self/SPEC.md, и в обоих он занимает больше половины файла. В тестах ему
отвечают случаи с именами вида «известный долг: одинокий суррогат не
экранируется», закреплённые затем, «чтобы закрытие долга не прошло молча». Приём
хороший и переносится в любой проект — а как он окупается, видно на «символах
строки»: пока каждая нетотальная функция названа с причиной, видно, что за
сотнями отдельных «не доказалось» стоит одна недостающая встроенная форма.
С чего начать, если хочется поучаствовать
Самое дешёвое и полезное — обновить то, что уже устарело. Мы нашли пять
пунктов в flang/examples/leetcode/index.json, описывающих ограничения,
которых больше нет (глава «Чего в языке пока нет»), и комментарии в
flang/stdlib/optional.flang и result.flang, объясняющие, почему у функций
нет примеров, — при том что примеры с вариантами теперь работают. Проверка
каждого пункта занимает минуту, правка — строку.
Там же — раздел 6 flang/SPEC.md: он всё ещё перечисляет из бэкендов один
emit/js.mjs, ничего не знает про link.mjs и self/ и всё ещё называет
существующей команду fmt, которой нет.
И один пункт, который мы нашли на себе. В flang/core/SPEC.md дважды стоит
«56 моделей» — число, которое воспроизводится только на машине, где рядом лежит
ещё один репозиторий с моделями; на чистом клоне их 47. Корневой README.md это
уже исправил и сформулировал образцово: «the promise is the corpus, not the
number». Перенести ту же формулировку в core/SPEC.md — правка на две строки, а
цена ошибки высокая: мы сами на неё попались
(глава «Ядро FTS на flang»).
Дальше по возрастанию сложности: диагностика для варианта, названного ключевым
словом (спецификация сама называет текущее сообщение долгом, а span там вдобавок
null); недостающие встроенные формы, названные в core/SPEC.md и
self/SPEC.md как условие закрытия долгов — и здесь стоит выбирать не по
сложности, а по цене.
Самая дорогая из недостающих — «символы строки», разложение строки в список.
Она одна держит нетотальность лексера, парсера и печати в C, написанных на самом
flang: сотни функций в трёх файлах, каждая с одной и той же записанной причиной
(глава «Чего в языке пока нет»). Рядом —
«нормализовать» и «код символа», которая держит сразу два долга ядра (одинокий
суррогат в печати JSON и печать имени-неидентификатора в ts_compat), и обратная
ей «символ по коду», без которой не развернуть \uXXXX в литерале. И отдельно —
логические операции, отсутствие которых портит читаемость любого условия сложнее
одного сравнения.
Помните только про цену: встроенная форма реализуется в девяти местах сразу — интерпретатор и восемь бэкендов, — и это не фигура речи, а то, что покажет прогон тестов, если сделать её в восьми.
Лицензия
BSD 2-Clause, LICENSE в корне; намерение по-русски — в LICENSE-RU.md. В
README.md отмечено, что ранние версии выходили под Apache-2.0, унаследованной
от исходного репозитория, а не выбранной; кто получил код под Apache-2.0,
сохраняет те права.
И напоследок
Всё, что описано в этом треке, сверено на 7 августа 2026 года, коммит b1ecbff.
Это шестая сверка, и каждая учила чему-то своему.
Первая показала, что список ограничений, составленный самим проектом несколькими часами раньше, уже устарел на пять пунктов.
Вторая — что устаревает и наш собственный текст: за час между редакциями у языка прибавилось два бэкенда кодогенерации, а парсер ядра FTS научился разбирать скобочный диалект. Править пришлось одиннадцать глав из тринадцати.
Третья оказалась поучительнее двух первых, потому что устарел не список
возможностей, а способ о языке думать: в репозитории началось самоприменение —
компилятор flang пишется на flang, — и главу про ядро FTS пришлось не
дописывать, а перестраивать. Заодно выяснились три вещи, которые мы сами
написали неверно. Число моделей в корпусе оказалось машинозависимым, и мы
привели его, не проверив (глава «Ядро FTS на flang»).
Утверждение «лимита шагов в напечатанном коде нет» оказалось верным для двух
целей из пяти (глава «Кодогенерация»). А похвала
переписанным AGENTS.md и CONTRIBUTING.md — просто неверной: они вернулись к
прежнему виду, и хвалить теперь нечего.
Вывод из третьей сверки стоит унести отдельно от всего остального в треке. Отставание документации — не чья-то небрежность, а свойство режима, в котором живёт репозиторий; но документ умеет не только отставать, но и откатываться назад, и единственный способ это заметить — открыть его снова, а не помнить, что там было вчера. Ровно то же относится и к тексту, который вы дочитали.
Четвёртая и пятая были про обвязку: конвейер публикации, тег, реестр, Homebrew, установка без Node.
Шестая оказалась самой дорогой, и урок у неё другой — не про отставание, а про направление отставания. Отстают ведь не только описания того, что есть. За одни сутки в языке появились ввод-вывод, процессы на BEAM, функции первого класса, полиморфизм, моноид, изоморфизм и бифунктор — и от этого устарел не список возможностей, а список ограничений: половина наших «в языке этого нет» стала неправдой.
Асимметрия здесь принципиальная, и в самом репозитории она названа лучше, чем
сумели мы: утверждение о недостаче — единственное, которое не ломается, когда
становится ложным. Тест на «умеет» краснеет, когда умение пропадает; тест на
«не умеет» никто не пишет, и фраза тихо переживает свою правду. Отсюда приём,
который стоит забрать себе независимо от flang: у каждой фразы «мы этого не
умеем» должен быть исполняемый двойник. В репозитории он есть —
flang/test/missing.test.mjs, программа-улика на каждую недостачу; разбор — в
главе «Чего в языке пока нет».
Поэтому главный совет остаётся тем же, что в главе «flang: язык, которому сутки»: проверяйте запуском. Инструмент отвечает за секунды и не врёт.
Что дальше
Всё до сих пор было про устройство: слои, проверки, бэкенды, долги. Осталась одна глава, и она про применение. Прикладная поверхность у языка ровно одна — встраиваемый факт-чекинг, тот самый режим, ради которого программы и поделены на два класса. В треке он появлялся трижды и всякий раз частями.
Дальше — «Факт-чекинг»: три входа режима, узкая грамматика утверждений и причина, по которой её не расширяют, три исхода вместо двух — и таблица, в которой flang стоит рядом с языковой моделью, регуляркой и запросом SQL.