flang — язык с доказуемым завершением Как читать репозиторий и как участвовать
0%

Как читать репозиторий и как участвовать

Как читать репозиторий и как участвовать

Язык суточной давности — плохой выбор для продакшена и хороший для чтения. Всё помещается в один каталог, слои не переплетены, и почти каждое решение объяснено в комментарии рядом с кодом. Эта статья — про то, как этим воспользоваться.

Что где лежит

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 называет существующей. Код новее документа, и разрыв растёт.

Маршрут чтения

Если читать подряд, естественный порядок такой.

  1. flang/SPEC.md целиком — 18 КБ, полчаса. Разделы 1 (два класса), 3 (типы) и 5 (AST) — обязательны, остальное по интересу. Раздел 10, «известные ограничения», прочитайте обязательно: он задаёт тон.
  2. flang/src/totality.mjs, первые шестьдесят строк. Не код, а объяснение метода — с контрпримером, доказывающим, почему нужна единая убывающая позиция. Лучший текст в репозитории.
  3. flang/stdlib/optional.flang — 169 строк, читается как статья. Тип, объяснение зачем, и разбор двух ограничений, которые видны прямо в коде модуля. Читать его стоит именно сейчас: оба ограничения за сутки перестали быть свойствами языка, и шапку переписали — не задним числом, а с прямым «здесь стояло … это больше не правда». Образец того, как правят такие комментарии.
  4. flang/src/interpret.mjs, шапка — почему явный стек кадров вместо рекурсии по стеку JS.
  5. flang/src/emit/go.mjs, шапка — шесть пунктов «где расходятся сами языки». Полезно даже тем, кому flang безразличен. Следом — шапки emit/rust.mjs и emit/python.mjs: тот же жанр, но про другие языки, и вместе три файла читаются как сравнительный разбор.
  6. flang/core/SPEC.md — если интересно, как на этом языке пишут настоящий парсер. Раздел «Долги» занимает больше половины и стоит того.
  7. flang/self/SPEC.md — почему компилятор языка переписан на самом языке, чем это отличается от переписывания ядра FTS и как выглядит критерий готовности, если проверять нечем, кроме самой проверяемой программы. Читается за двадцать минут и меняет взгляд на предыдущие шесть пунктов.
  8. 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.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов
Дальше