@digitable-lol/fts
v0.4.7
Published
flang: a checkable language with total and ordinary functions, emitted to C, Go, Rust, Python, Java, C#, Elixir and JavaScript, over the FTS executable-specification surface.
Maintainers
Readme
English · Русский
FTS и flang — спецификация, которая исполняется и печатается в язык проекта
Написанный документ расходится с кодом на следующий день после того, как его приняли, и расходится молча: ничего не ломается оттого, что спецификация и реализация больше не про одно и то же. Здесь выбран другой путь — спецификация и есть программа. Правила пишутся один раз, исполняются, проверяются собственными примерами, а потом печатаются в C, Go, Rust, Python, Java, C#, Elixir или JavaScript, где напечатанный код обязан выдавать те же значения и те же коды ошибок, что интерпретатор, вход за входом.
Авторская поверхность русская. Английская тоже есть и лексится в те же самые идентификаторы
(функция / function, свёртка / fold), но имена в спецификации принадлежат той предметной
области, которая их придумала, и потому не переводятся ни в одну сторону — ни в исходнике, ни в
напечатанном коде.
Как связаны FTS и flang
- FTS (
.fts) — отступный язык исполняемых спецификаций: объекты предметной области, детерминированные утилиты, примеры, проверяемые свойства, морфизмы и машинно-проверяемые свидетельства. Эталонная реализация — ядро на TypeScript вsrc/. flang(.flang) — полный язык, в который вырос FTS: суммы типов, списки, строки как данные, рекурсия, сопоставление с образцом, связывание модулей, категорная поверхность, поверхность конкурентности и восемь бэкендов печати. Реализация —flang/src/.
FTS — тотальное подмножество flang: любая существующая модель .fts является валидной программой
flang. Это не лозунг, а дифференциальный тест: оба движка прогоняют каждую утилиту каждой модели
по сетке входов, и совпасть обязаны и значения, и коды ошибок. Прогон печатает собственные числа:
сверка: файлов 22, документов 20, из них с утилитами 13; утилит 24, входов 23084,
из них с ошибкой 773 (коды: FTS_UTILITY_PROPERTY), расхождений 0Остальное несут два документа: docs/overview.ru.md описывает язык и
проводит границу между тем, что доказано, и тем, что проверено, а
flang/SPEC.md — спецификация. Эта страница за ту границу не заходит.
Установка
Чтобы поставить flang, Node не нужен. Компилятор языка написан на самом языке и печатается в C, поэтому релиз содержит уже напечатанный C99: достаточно компилятора C.
brew install digitable-lol/tap/flangИли прямо из релизного архива, где не нужно ничего, кроме cc и make:
tar -xzf flang-*-c.tar.gz # внутри — исходники на C99, Makefile и страница flang.1
make # cc -std=c99 -Wall -Wextra -Werror -pedantic -O2
./flang_cli --help # что умеет: check, repl, --version
./flang_cli check м.flang # разбор, типы, завершаемость — словами, не JSON
./flang_cli # без аргументов — прогонщик: JSON на входе и выходеФормула Homebrew — packaging/homebrew/flang.rb, и её раздаёт tap.
Плагин asdf (и mise) ставит тот же архив из тех же релизов, его исходник —
packaging/asdf/, но asdf клонирует плагин целым репозиторием, а этот
репозиторий ещё не опубликован: пока плагин — исходник, а не способ установки. Ни тому, ни другому
не нужно ничего, кроме компилятора C. Так распространяются самоприменяющиеся языки: Go долго возил
сгенерированный C, Nim возит до сих пор.
Стоит назвать прямо, что это за бинарник. Это пять слоёв flang/self/: лексер,
парсер, типы, завершаемость, печать в C. Вычислителя среди них нет — и flang repl здесь
вычисляет единственным способом, каким этот бинарник может: печатает сессию в C, собирает её
системным cc против рантайма, поставленного рядом, и запускает. Без cc оболочка не
выключается: она по-прежнему проверяет разбор, типы и завершаемость и говорит об этом один раз.
Проверить файл этот бинарник умеет сам: flang check файл.flang проходит разбор, связывание,
типы и завершаемость и печатает замечания словами — с кодом и местом, а не JSON. flang --help
перечисляет команды, man flang описывает их подробно. Запустить программу или прогнать её
примеры не в оболочке можно только полным инструментарием — он ниже.
Полному инструментарию Node.js 20 или новее нужен, и вот почему именно: интерпретатор, языковой сервер, сервер MCP и семь бэкендов из восьми существуют только на JavaScript. Самоприменяющийся компилятор — тот, что лежит в релизе, — печатает в C и никуда больше.
npm install -g @digitable-lol/ftsОтсюда берутся команды, которыми пользуется эта страница: flang — язык, fts — модели,
fts-mcp — сервер MCP, плюс ftsc, ftsvm и ftspec. В клоне те же команды пишутся как
node flang/bin/flang.mjs и node dist/src/cli.js, и клон нужен для всего, что новее последнего
опубликованного релиза. Проблема начальной загрузки остаётся у тех, кто развивает сам язык, —
см. Развитие языка.
Одна функция, восемь целей
Это flang/examples/leetcode/035-search-insert-position.flang — LeetCode 35: номер, под которым
значение стоит в отсортированном списке, а если его там нет — номер, куда его следовало бы
вставить. Одна свёртка, завершение доказано:
тотальная функция «Место вставки»
принимает элементы: список числа, цель: число
возвращает число
пример «Пример 1 из условия»
дано элементы равно [1, 3, 5, 6]
дано цель равно 5
ожидается 2
свёртка элементы начиная с 0 как акк и эл → если эл меньше цель то акк плюс 1 иначе аккВсё, что ниже, получено запуском
flang emit flang/examples/leetcode/035-search-insert-position.flang --target c --out ./out-c
# …и то же самое с go, rust, python, java, csharp, elixir, jsи вставлено дословно, а не переписано руками. Семь бэкендов печатают модуль, рантайм, прогонщик
«JSON на входе — JSON на выходе», файл сборки и — там, где у цели такое есть, — манифест пакета
(go.mod, Cargo.toml, flang.csproj); бэкенд JavaScript печатает один самодостаточный файл.
Здесь показана только сама функция.
/*
* Функция flang «Место вставки».
*
* Тотальная: завершение доказано анализом завершаемости (totality.mjs).
* @param elementy — «элементы»: список: число
* @param cel — «цель»: число
* @return значение: число
*/
fl_status mesto_vstavki_mesto_vstavki(fl_ctx *ctx, fl_value elementy, fl_value cel, fl_value *result, fl_error *error) {
fl_value fl_t1 = fl_nothing();
FL_TRY(fl_require_list(ctx, elementy, "свёртка", &fl_t1, error));
fl_value akk = fl_number(0.0); /* «акк» */
for (size_t fl_t2 = 0; fl_t2 < fl_t1.as.list.count; fl_t2 += 1) {
const fl_value el = fl_t1.as.list.items[fl_t2]; /* «эл» */
fl_value fl_t3 = fl_nothing();
FL_TRY(fl_lt(ctx, el, cel, &fl_t3, error));
bool fl_t4 = false;
FL_TRY(fl_cond(ctx, fl_t3, &fl_t4, error));
fl_value fl_t5 = fl_nothing();
if (fl_t4) {
fl_value fl_t6 = fl_nothing();
FL_TRY(fl_add(ctx, akk, fl_number(1.0), &fl_t6, error));
fl_t5 = fl_t6;
} else {
fl_t5 = akk;
}
akk = fl_t5;
}
*result = akk;
return FL_OK;
}// MestoVstavki — функция flang «Место вставки».
//
// Тотальная: завершение доказано анализом завершаемости (totality.mjs).
//
// Параметр elementy — «элементы»: список: число.
// Параметр cel — «цель»: число.
// Результат — значение: число.
func MestoVstavki(ctx *rt.Ctx, elementy rt.Value, cel rt.Value) (rt.Value, error) {
t1, e2 := rt.RequireList(ctx, elementy, "свёртка")
if e2 != nil {
return rt.Value{}, e2
}
// «акк»
akk := rt.Number(0.0)
for t3 := range t1 {
// «эл»
el := t1[t3]
t4, e5 := rt.Lt(ctx, el, cel)
if e5 != nil {
return rt.Value{}, e5
}
t6, e7 := rt.Cond(ctx, t4)
if e7 != nil {
return rt.Value{}, e7
}
var t8 rt.Value
if t6 {
t9, e10 := rt.Add(ctx, akk, rt.Number(1.0))
if e10 != nil {
return rt.Value{}, e10
}
t8 = t9
} else {
t8 = akk
}
akk = t8
}
return akk, nil
}/// Функция flang «Место вставки».
///
/// Тотальная: завершение доказано анализом завершаемости (totality.mjs).
///
/// Параметр `elementy` — «элементы»: список: число.
/// Параметр `cel` — «цель»: число.
/// Результат — значение: число.
pub fn funkciya_mesto_vstavki(ctx: &rt::Ctx, elementy: rt::Value, cel: rt::Value) -> Result<rt::Value, rt::Error> {
let t1 = rt::require_list(ctx, elementy.clone(), "свёртка")?;
// «акк»
let mut akk = rt::number(0.0);
// «эл»
for el in t1.iter().cloned() {
let t2 = rt::lt(ctx, el.clone(), cel.clone())?;
let t3 = rt::cond(ctx, t2)?;
let t4 = if t3 {
let t5 = rt::add(ctx, akk.clone(), rt::number(1.0))?;
t5
} else {
akk.clone()
};
akk = t4;
}
return Ok(akk);
}def fn_mesto_vstavki(ctx, elementy, cel):
"""Функция flang «Место вставки».
Тотальная: завершение доказано анализом завершаемости (totality.mjs).
Параметр elementy — «элементы»: список: число.
Параметр cel — «цель»: число.
Результат — значение: число.
"""
_t1 = rt.require_list(ctx, elementy, "свёртка")
# «акк»
akk = rt.number(0.0)
for el in _t1:
if rt.cond(ctx, rt.lt(ctx, el, cel)):
_t2 = rt.add(ctx, akk, rt.number(1.0))
else:
_t2 = akk
akk = _t2
return akk /**
* Функция flang «Место вставки».
*
* Тотальная: завершение доказано анализом завершаемости (totality.mjs).
*
* @param elementy «элементы»: список: число
* @param cel «цель»: число
* @return значение: число
*/
public static Value fn_mesto_vstavki(Ctx ctx, Value elementy, Value cel) {
Value[] t1 = Flang.requireList(ctx, elementy, "свёртка");
// «акк»
Value akk = Value.number(0.0d);
for (Value el : t1) {
Value t2;
if (Flang.cond(ctx, Flang.lt(ctx, el, cel))) {
t2 = Flang.add(ctx, akk, Value.number(1.0d));
} else {
t2 = akk;
}
akk = t2;
}
return akk;
} /// <summary>
/// Функция flang «Место вставки».
///
/// Тотальная: завершение доказано анализом завершаемости (totality.mjs).
///
/// Параметр elementy — «элементы»: список: число.
/// Параметр cel — «цель»: число.
/// Результат — значение: число.
/// </summary>
public static Value FnMestoVstavki(Ctx ctx, Value elementy, Value cel)
{
Value[] t1 = Flang.RequireList(ctx, elementy, "свёртка");
// «акк»
Value akk = Value.Number(0.0d);
foreach (Value el in t1)
{
Value t2;
if (Flang.Cond(ctx, Flang.Lt(ctx, el, cel)))
{
t2 = Flang.Add(ctx, akk, Value.Number(1.0d));
}
else
{
t2 = akk;
}
akk = t2;
}
return akk;
} @doc """
Функция flang «Место вставки».
Тотальная: завершение доказано анализом завершаемости (totality.mjs).
Параметр `elementy` — «элементы»: список: число.
Параметр `cel` — «цель»: число.
Результат — значение: число.
"""
def fn_mesto_vstavki(elementy, cel) do
t1 = Flang.Rt.require_list(elementy, "свёртка")
t2 = {:num, 0.0}
t3 =
Enum.reduce(t1, t2, fn el, akk ->
t4 =
if Flang.Rt.cond_flag(Flang.Rt.lt(el, cel)) do
t5 = {:num, 1.0}
Flang.Rt.add(akk, t5)
else
akk
end
t4
end)
t3
end/**
* Функция flang «Место вставки».
*
* Тотальная: завершение доказано анализом завершаемости (totality.mjs).
*
* @param {Array<number>} elementy — «элементы»
* @param {number} cel — «цель»
* @returns {number}
*/
export function mestoVstavki(elementy, cel) {
const $t1 = $requireList(elementy, "свёртка")
let akk = 0
for (const el of $t1) {
let $t2
if ($cond($lt(el, cel))) {
$t2 = $add(akk, 1)
} else {
$t2 = akk
}
akk = $t2
}
return akk
}Бэкенд JavaScript встраивает только те вспомогательные функции рантайма, которые этот модуль действительно использует, — поэтому на выходе один самодостаточный файл, который идёт и в Node, и в браузере.
Напечатанный код — не заготовка, которую доводят руками. Он несёт имена предметной области в комментариях, отдаёт диагностические коды и тексты интерпретатора дословно, а в шапке написано, чем он является: «Правьте исходник на flang и печатайте заново: любая правка здесь потеряется.»
Почему бэкендам можно верить
Каждый бэкенд проверяется дифференциально, а не эталонными файлами. Корпус — стандартная
библиотека и решения LeetCode: flang/stdlib/*.flang и flang/examples/leetcode/*.flang, то
есть 31 программа со 154 функциями и 259 примерами. Для каждой функции строится сетка входов из
её собственных примеров и намеренно неверных аргументов (ничто, строка там, где ждут список,
несуществующий вариант); программа печатается в пустой каталог, собирается настоящим тулчейном из
одного только того, что выдал бэкенд, и запускается настоящим процессом. Прогон сам отчитывается
об охвате, поэтому утверждение проверяемо, а не процитировано:
✔ stdlib и leetcode: собранный Rust совпадает с интерпретатором
ℹ программ: 31, функций: 154, сверенных входов: 2235, за 28 с
✔ примеры stdlib и leetcode сходятся у собранного Rust так же, как у интерпретатора
ℹ сверенных примеров: 259Бэкенд C дополнительно собирается и gcc, и clang с
-std=c99 -Wall -Wextra -Werror -pedantic -O2 и проверяется под valgrind на отсутствие
потерянных байт.
Тестам бэкендов нужен настоящий тулчейн, и без него они пропускаются явно. Пропущенный тест — не
пройденный тест, поэтому там, где тулчейн обязан присутствовать (CI, релизная машина), ставится
FTS_REQUIRE_TOOLCHAINS: 1 требует все бэкенды, rust,go — перечисленные, и недостающий
компилятор тогда роняет тест по имени, а не исчезает молча. FTS_TOOLCHAIN_PATH добавляет
каталоги поиска.
Зачем это нужно
Обычное устройство: спецификация в одном артефакте, реализация в другом и обещание, что кто-то держит их в согласии. Обещание нарушается тихо — расхождение документа и кода ничего не роняет и никем не замечается, пока не станет инцидентом.
Здесь правило написано один раз и в той форме, которую читает человек из предметной области, а не
только программист (фрагмент examples/utilities/discount.fts):
категория «Продажи»
объект Покупка
сумма является деньгами
«постоянный клиент» является признаком
утилита «Рассчитать скидку»
принимает Покупка
возвращает деньги
начинает с 0
правило «Большая покупка»
если сумма не меньше 10000
то добавить 10 процентов от поля сумма
правило «Постоянный клиент»
если «постоянный клиент» равен да
то добавить 5 процентов от поля сумма
свойство «Скидка ограничена»
результат не больше 20 процентов от поля сумма
пример «Большая покупка»
дано сумма равна 20000
дано «постоянный клиент» равен нет
ожидается результат равен 2000Ни фигурных скобок, ни стрелок, ни двоеточий: поверхность отступная и силлогистическая, а читаемые имена берутся в кавычки-ёлочки. Скобочный диалект из наследия тоже принимается — ради совместимости.
Из этого единственного исходника получаются реализация, тесты и проверки — сразу на восьми
языках. свойство выше не комментарий: оно превращается в постусловие напечатанного кода. Печать
examples/utilities/discount.fts в Python даёт, дословно:
# постусловие «Скидка ограничена»
if not rt.post(ctx, rt.lte(ctx, _t3, rt.percent(ctx, rt.number(20.0), rt.field_get(ctx, vhod, "сумма"))), "Скидка ограничена", "Рассчитать скидку"):
raise rt.fail("FTS_UTILITY_PROPERTY", "нарушено свойство «Скидка ограничена» утилиты «Рассчитать скидку»")FTS_UTILITY_PROPERTY — собственный диагностический код ядра FTS, и формулировка сообщения тоже
его. Служба на Python, служба на Go и бинарник на C, порождённые из этой модели, откажут одному и
тому же входу одними и теми же словами. Вот что «единый источник правды» обязан означать, чтобы
чего-то стоить.
Настоящая задача, а не «hello world»
LeetCode 121 — наибольшая прибыль от одной покупки и одной продажи, один проход, состояние в
записи из двух полей. Это flang/examples/leetcode/121-best-time-to-buy-and-sell-stock.flang
целиком, без заголовочного комментария:
объект «Сделка»
минимум является числом
прибыль является числом
тотальная функция «Лучшая прибыль»
принимает цены: список числа
возвращает число
пример «Пример 1 из условия»
дано цены равно [7, 1, 5, 3, 6, 4]
ожидается 5
пример «Пример 2 из условия»
дано цены равно [7, 6, 4, 3, 1]
ожидается 0
пример «Пустой список»
дано цены равно пустой список
ожидается 0
разбор цены
случай пусто
то 0
случай голова и хвост
пусть начальное равно запись «Сделка» с минимум равным голова и прибыль равным 0
пусть итог равно свёртка хвост начиная с начальное как акк и цена
пусть минимум равно если цена меньше акк.минимум то цена иначе акк.минимум
пусть сегодня равно цена минус акк.минимум
пусть прибыль равно если сегодня больше акк.прибыль то сегодня иначе акк.прибыль
запись «Сделка» с минимум равным минимум и прибыль равным прибыль
итог.прибыльЭто читается вслух как русская фраза — «разбор цены, случай пусто, то ноль», — а признак
тотальная в первой строке не украшение, а утверждение, которое компилятор обязан был доказать,
прежде чем принять файл. Примеры лежат внутри функции, а не в отдельном тестовом файле, и
запускаются оттуда же:
flang test flang/examples/leetcode/121-best-time-to-buy-and-sell-stock.flang --prettyНаборов примеров два, и оба сторожатся тестом, а не добрыми намерениями. В
flang/examples/leetcode/ лежат 26 решений, из них 20 тотальны
целиком; у каждого комментарий объясняет не только алгоритм, но и то, где язык упёрся: почему
двоичному поиску нужен список-«топливо», чтобы его приняли за завершающийся; почему Single Number
выходит на O(n²) — побитовых операций нет. В flang/examples/rosetta/
лежат семь канонических задач Rosetta Code, и тест закрепляет, сколько функций каждого файла
доказано тотальными: набор показывает границу языка, поэтому сдвинувшаяся граница обязана ронять
тест, а не тихо устаревать в комментарии. Стандартная библиотека
(flang/stdlib/: higher-order, lists, numbers, optional, result,
strings) написана так же — 112 функций, из них 105 доказаны тотальными. higher-order — та, что
на функциях-значениях: свёртка, отображение, фильтр, поиск, сортировка и композиция принимают
функцию аргументом.
Что даёт признак тотальная
Полнота по Тьюрингу и гарантированная завершаемость несовместимы, поэтому flang не выбирает между ними: он делит программы на два класса, а к какому классу относится ваша — решает компилятор.
| | тотальная | обычная |
|----------------------------|-----------------------------|-------------------------------|
| рекурсия | убывающая: по части значения или по числовой мере со сторожем | любая |
| завершаемость | доказана компилятором | не гарантируется |
| её примеры | гарантированно завершаются | могут упереться в лимит шагов |
| допускается в факт-чекинг | да | нет |
тотальная требует, чтобы каждый рекурсивный вызов получал убывающий аргумент, и убывания
принимаются двух видов: структурное — хвост списка, поле варианта, поле записи — и числовое, по
мере. Мера — это н минус <число> при условии, что параметр в точке вызова ограничен снизу
проверкой-неравенством (если н не больше 0). Оба условия обязательны: без постоянного шага
цепочка может не убывать вовсе, без границы она уходит в минус бесконечность. Если анализ не
доказал убывание, вы получаете FLANG_NOT_TOTAL и файл не компилируется: это ошибка, а не
предупреждение. Любая существующая модель .fts попадает в тотальный класс по построению.
Счёт ВВЕРХ мерой не является и остаётся вне тотального класса: «Числа от и до» от 1 и н
наращивает начало, а конец — параметр, не число, и границей быть не может. Строковые задачи
границу перешли раньше и по-другому: встроенная форма разложить … на символы раскладывает
строку в список односимвольных строк по кодовым точкам, и посимвольный проход становится
рекурсией по хвосту. Благодаря ей flang/examples/rosetta/reverse-string.flang тотален целиком —
вместе с кириллицей и эмодзи.
Это не педантизм, и причина вполне конкретная. Встроенный режим факт-чекинга
(flang/src/factcheck.mjs) отвечает на вопрос «верно ли это
утверждение об этих данных» — а система, обязанная ответить «да» или «нет», не имеет права
зависнуть. Поэтому она отказывается запускать функцию, завершаемость которой не доказана, ещё до
того, как что-либо вычислит:
echo '{"н": 30}' | flang facts flang/examples/leetcode/509-fibonacci-number.flang \
--facts - --claims '["«Фибоначчи» от н равно 832040"]' --pretty{
"claim": "«Фибоначчи» от н равно 832040",
"holds": false,
"why": "функция «Фибоначчи» не помечена как «тотальная»; факт-чекинг допускает только тотальные функции — иначе ответ может не наступить"
}То же утверждение о тотальной функции проверяется, и вердикт несёт собственный вывод — в steps
записано, что разобрано, что тотальность проверена, какие факты прочитаны, что вычислено и как
сравнено:
echo '{"цены": [7, 1, 5, 3, 6, 4]}' | flang facts \
flang/examples/leetcode/121-best-time-to-buy-and-sell-stock.flang \
--facts - --claims '["«Лучшая прибыль» от цены равно 5"]' --pretty{ "ok": true, "results": [ { "holds": true,
"why": "«Лучшая прибыль» от факта «цены» = 5; требование «равно 5» выполнено", "status": "verified" } ] }У режима нет доступа ни к файлам, ни к сети, ни к часам, и есть жёсткий бюджет шагов: ответ зависит только от четвёрки «программа, факты, утверждения, лимиты» — и потому воспроизводится.
Категорная поверхность
Семантика была категорной и раньше — молчала об этом поверхность. Морфизм — стрелка между
объектами, композиция записывается словом после, длинный конвейер — в порядке чтения, а
единица — тождественный морфизм объекта. Только слова: ни →, ни ∘.
морфизм «отгрузить» из «Заказ» в «Отгрузка»
морфизм «выставить» из «Отгрузка» в «Счёт»
морфизм «оформить» это «выставить» после «отгрузить»
единица «Заказ»
цепочка «провести заказ»
сначала «отгрузить»
затем «выставить»Стыковка проверяется компилятором и относится к доказанному: «б» после «а» собирается тогда и
только тогда, когда кодомен «а» равен домену «б», а цепочка собирается, только если не рвётся
в середине. Несостыковка называет оба конца:
FLANG_COMPOSE_MISMATCH: композиция «оформить» не стыкуется:
«выставить» приводит в «Счёт», а «отгрузить» ожидает «Заказ»Функтор отображает объекты и стрелки, и слово не даётся без гарантии: отдельного разрешения проверять законы нет, потому что отображение, не сохраняющее композицию, функтором не является.
функтор «Электронное в бумажное» из «Продажи» в «Продажи»
объект «Заказ» отображается в «Заказ»
объект «Отгрузка» отображается в «Накладная»
объект «Счёт» отображается в «Счёт»
морфизм «отгрузить» отображается в морфизм «печатать»
морфизм «выставить» отображается в морфизм «подписать»
морфизм «оформить» отображается в морфизм «оформить бумажно»Три закона здесь доказываются, а не проверяются на примерах: это утверждения обо всех входах, взятые из одних объявлений, без сетки и без решателя.
- согласование стрелки с объектами: если стрелка ведёт из «A» в «B», её образ обязан вести из
образа «A» в образ «B» (
FLANG_FUNCTOR_ARROW_MISMATCH); - сохранение композиции: образ композиции обязан быть композицией образов в том же порядке
(
FLANG_FUNCTOR_COMPOSITION); - сохранение единиц: образ тождественного морфизма обязан быть тождественным морфизмом образа
(
FLANG_FUNCTOR_IDENTITY).
Это возможно ровно потому, что морфизм здесь объявление, а не значение: имя, домен и кодомен
известны до запуска. Объявлением он и остался, хотя функция значением стала (flang/cat/HOF.md):
будь композиция вычислением над функциями-значениями, проверять было бы нечего — равенство двух
вычислений неразрешимо.
Где поверхность кончается, сказано так же прямо: имена категорий (из «Продажи» в «Биллинг»)
остаются пометкой для читателя, потому что категория отдельной сущностью не объявляется и
проверять принадлежность не на чем. Естественные преобразования описаны в
flang/cat/SPEC.md и не реализованы; моноиды, группы, изоморфизмы,
бифункторы и монады — реализованы, и у монады заодно есть форма связывания в монаде
(flang/cat/MONAD.md).
Конкурентность: шесть шагов из семи
Значения неизменяемы по устройству, поэтому два вычисления, глядящие на одно значение, не могут
помешать друг другу: гонок по данным нет не потому, что их ловит анализ, а потому, что их нечем
устроить. Не хватало языку слова, называющего одновременность. Замыканий в языке нет (значением
стала функция, а не замыкание), поэтому породить процесс от fun () -> …, как в BEAM, нельзя —
и процесс здесь объявление: имя, тип состояния, начальная функция и обработчик.
процесс «Счётчик»
состояние «Счёт»
начинает с «пустой счёт»
принимает «Команда счёта»
обрабатывает «шаг счёта»
надзор «Учёт»
процесс «Счётчик» стратегия «перезапустить»
процесс «Журнал» стратегия «остановить»
порог отказов 3 за 5000 миллисекунд иначе «передать выше»
прогон «два прибавления и доклад»
семя 4172
дано «Счётчик» принимает (вариант «прибавить» с «сколько» равным 2)
дано «Счётчик» принимает (вариант «прибавить» с «сколько» равным 3)
дано «Счётчик» принимает (вариант «доложить» с «повод» равным "итог: ")
ожидается «Счётчик» равен (запись «Счёт» с «всего» равным 5)
ожидается «Журнал» равен (запись «Записи» с «строки» равным ["итог: 5"])Обработчик — обычная чистая функция, возвращающая «новое состояние плюс список действий»; отправка
сообщения описывается, а не выполняется — выполняет её планировщик. прогон — это пример с
семенем, и он идёт в тот же вывод flang test, что и обычные примеры: конкурентность не должна
становиться местом, где проверки заканчиваются. Одно семя даёт один журнал доставок, побайтово, и
это проверяется повторным прогоном после повторного разбора исходника. Взаимная блокировка —
определённый исход (исход: "покой"), а не зависание.
Одно семя проверяет одно чередование, а семантика модели — любое. Поэтому у прогона есть сетка семян, и на ней «равен» становится инвариантом, а «любое из» называет множество достижимых итогов:
прогон «порядок в сборщике — одно из двух, и третьего не бывает»
семя от 1 до 1000
дано «Левый» принимает (вариант «тик» с «метка» равным "Л")
дано «Правый» принимает (вариант «тик» с «метка» равным "П")
ожидается «Левый» равен (запись «Счёт» с «всего» равным 1)
ожидается «Сборщик» любое из [
(запись «Метки» с «строки» равным ["Л", "П"]),
(запись «Метки» с «строки» равным ["П", "Л"])]Множество проверяется в обе стороны: ни один из тысячи прогонов не вышел за него И каждое названное значение где-то встретилось. Односторонняя проверка тем слабее, чем шире множество, — дописав туда лишнее, автор ослаблял бы её молча. Расхождение называет семя, и по нему чередование предъявляется поодиночке.
flang test flang/conc/examples/counter.flang --prettyКонтракт — flang/conc/SPEC.md, и в нём порядок работ из семи шагов, из
которых сделаны шесть: поверхность, проверки, планировщик эталона, надзор, печать в Elixir,
перебор семян и измерение. Стратегии применяются по-настоящему: перезапуск возвращает состояние к
тому же самому начальному значению, порог отказов считается по виртуальному времени, «передать
выше» доходит до надзора ступенью выше, а если выше никого — останавливает всю программу с исходом
"отказ дошёл доверху". Примеры лежат в flang/conc/examples/.
Своей виртуальной машины под это не пишется: у языка уже есть цель Elixir, и процесс flang печатается в процесс BEAM, а надзор — в дерево супервизоров OTP. Вытеснение по счёту редукций, куча со своим сборщиком у каждого процесса, планировщик на каждое ядро, распределённость и горячая загрузка достаются при этом в настоящем виде, а не в приблизительном.
flang emit flang/conc/examples/counter.flang --target elixir --out /tmp/счётчик
cd /tmp/счётчик && make build
echo '{"run":"два прибавления и доклад"}' | elixir -pa _build -e 'Flang.Cli.main(["SchyotchikIZhurnal"])'Сверять напечатанное с эталоном по значению нельзя: семантика определена любым чередованием
атомарных пробегов, а семени у планировщика BEAM нет. Поэтому сверяется набор: эталон на тысяче
семян даёт множество исходов и множество журналов доставки, и каждый исход настоящего запуска на
BEAM обязан в это множество попасть. Проверено и то, что множество насыщено (половина сетки семян
даёт тот же набор), и то, что оно узко (достаточно изменить в исходе одно число, и он из набора
выпадает). Остальные семь целей процессов пока не печатают: там программа с процесс даёт
обработчики как обычные функции и больше ничего.
Модель измерена — node --expose-gc flang/conc/bench.mjs, — и числа выбраны так, чтобы не зависеть
от загрузки машины, потому что мерить пришлось на занятой. Процесс flang на BEAM занимает 2768
байт против 2720 у голого GenServer, то есть в память узла их помещается столько же, сколько
процессов BEAM; доставка сообщения стоит 106 редукций против 19 у голого GenServer — впятеро с
половиной дороже, и это цена модели поверх машины. Пробег обработчика — 17 витков
интерпретатора, отправка добавляет 9: описание действия и есть построенное значение, оно не
бесплатно. У планировщика эталона нашлось и неприятное: очередь готовых пересобирается полным
перебором процессов, поэтому переключение стоит O(числа процессов) — около 12 нс на каждый
объявленный процесс.
Оболочка и языковой сервер
flang repl flang/stdlib/lists.flangОболочка копит объявления в сессии и вычисляет выражения поверх них. Каждое объявление идёт той же
дорогой, что и check — разбор, типы, завершаемость, — поэтому в сессию не попадает то, что
компилятор отверг бы. Объявление заканчивается пустой строкой, .помощь перечисляет команды
(.объявления, .исходник, .сохранить, .загрузить, .сбросить, .выход; по-английски
работают .help .list .source .save .load .reset .quit). Это единственная команда с человеческим
выводом вместо JSON.
» тотальная функция «Удвоить»
… принимает х: число
… возвращает число
… х умножить на 2
…
объявлено: тотальная функция «Удвоить» — завершение доказано
» «Удвоить» от «Длина» от [1, 2, 3]
6Тотальность здесь не пометка, а вердикт: об обычной функции оболочка так и говорит —
завершение не доказано: вычисление ограничено лимитом шагов, — и на превышении лимита приходит
FLANG_RECURSION_LIMIT.
Языковой сервер — flang/src/lsp.mjs, поднимается по stdio командой
node flang/bin/flang-lsp.mjs --stdio (в пакете он объявлен командой flang-lsp). Буферу он даёт
ровно то же, что flang check даёт файлу, — те же диагностики, те же коды, те же слова, — плюс
дополнение, сигнатуру при наведении и переход к объявлению, в том числе в импортированный модуль.
Переименования, быстрых правок и форматирования нет намеренно: это была бы вторая реализация языка
рядом с первой. Настройка редакторов и полный список того, что сервер умеет и чего не умеет, — в
editors/flang-lsp/README.md.
Две реализации и неподвижная точка
Реализации две, и обе поддерживаются намеренно. Эталонная написана на TypeScript и JavaScript и служит определением поведения языка. Самоприменяющаяся написана на самом flang.
Ядро FTS, написанное на flang
flang/core/ — это ядро FTS (лексер, парсер, вычислитель, печать JSON),
переписанное на flang: 300 функций, каждая помечена тотальная и доказана. fts check тоже не
имеет права зависнуть.
Критерий правильности здесь не «свои тесты проходят». Он дифференциальный и записан в
flang/core/SPEC.md: прогнать всю цепочку — текст → лексер на flang →
парсер на flang → печать JSON на flang — и потребовать, чтобы выданная строка побайтово
совпала с JSON.stringify(compile(текст)) ядра на TypeScript. Прогон идёт по каждой модели .fts
репозитория — на чистом клоне их 50, на обеих поверхностях (47 отступных и 3 скобочных) — с нулём
расхождений. Если на машине есть внешний каталог моделей, его модели попадают в тот же прогон, и
число у вас окажется больше; обещание касается корпуса, а не числа. Диагностики сверяются
отдельно — на 34 намеренно сломанных отступных моделях и 13 скобочных, причём и код, и текст
сообщения.
Побайтовое равенство — сильное утверждение, а не формальность, потому что строка JSON выставляет
наружу всё, что более мягкое сравнение спрятало бы: порядок вставки ключей; 5 против "5"
против да; как отрисовано дробное число; сохранило ли имя свои кавычки-ёлочки; какая диагностика
сработала первой и какими словами. Реализация, «в целом такая же», ломается на этом тесте на
первом же документе. (В ранней редакции контракта скаляры хранились строками — печать JSON
опровергла это сразу же: 5, "5" и да печатаются по-разному и как текст неразличимы.)
Компилятор, написанный на flang, и неподвижная точка
flang/self/ — это компилятор flang, написанный на flang. Пять слоёв, и у каждого
свой эталон на JavaScript, с которым он сверяется точно — не «в целом так же», а до последней
составляющей результата:
| Слой | Функций | Эталон | Что обязано совпасть |
|-----------------------|--------:|--------------------|-------------------------------------------------------------|
| self/lexer.flang | 88 | src/lexer.mjs | поток токенов: вид, значение, закавыченность, строка и столбец |
| self/parser.flang | 372 | src/parser.mjs | AST — побайтово после сериализации |
| self/types.flang | 276 | src/types.mjs | диагностики (код, текст, строка и столбец) и таблица сигнатур |
| self/totality.flang | 124 | src/totality.mjs | вердикт: список доказанных функций в том же порядке, диагностики, признак ok |
| self/emit-c.flang | 328 | src/emit/c.mjs | напечатанный C — побайтово, и он собирается без предупреждений |
Сверка живёт в flang/test/self-*.test.mjs и идёт не только по исправным файлам: на исправном
входе верный ответ — «диагностик нет», и его выдаёт любая функция, которая не делает ничего.
Поэтому в корпус входят намеренно испорченные программы и фаззинг случайными исходниками, где
эталон и реализация на flang обязаны ошибаться одинаково — теми же кодами, теми же словами и в том
же месте.
Готовность определяется не тем, что «собралось», а классической неподвижной точкой:
1. компилятор на JS печатает self/*.flang → C → сборка → flang₁
2. flang₁ печатает те же self/*.flang → C → сборка → flang₂
3. C, напечатанный flang₁, и C, напечатанный flang₂, совпадают побайтовоНеподвижная точка сошлась. Эталон, flang₁ и flang₂ печатают компилятор одинаково — все
семь напечатанных файлов C, — то есть компилятор понимает язык так же, как эталон, и никакой набор тестов этого
не заменяет. Проверка — flang/test/self-bootstrap.test.mjs, и она печатает результат:
✔ шаги 2 и 3: flang₁ печатает сам себя, flang₂ печатает то же самое
ℹ неподвижная точка сошлась: 6 файлов совпали побайтово у эталона, flang₁ и flang₂Именно отсюда берётся релиз без Node: C в релизном архиве напечатан из этих исходников. Эталонную реализацию при этом не удаляют и не удалят — относительно неё и проверяется схождение, а её удаление сделало бы проверку невозможной.
Одно решение стоит назвать отдельно. Компилятору тотальность не требуется — в отличие от
факт-чекинга он имеет право упереться в лимит шагов и честно об этом сказать. Поэтому в
flang/self/ обычные функции разрешены там, где доказательство убывания обошлось бы дороже пользы
(обход графа вызовов по Тарьяну, рекурсивный спуск по потоку токенов), и каждое такое место обязано
быть названо в разделе «Долги» файла flang/self/SPEC.md. У ядра FTS в
flang/core/ этого послабления нет.
Отсюда следует, что ядро собирается в нативный бинарник
flang emit flang/core/parser.flang --target c --out ./core-c
make -C ./core-cИзмерено на этой машине (gcc 13.3, x86-64, -std=c99 -Wall -Wextra -Werror -pedantic -O2):
- сборка идёт без единого предупреждения — флаги записаны в напечатанный
Makefileи являются частью контракта бэкенда, а не советом; - 671 616 байт (633 048 после
strip), линкуется только сlibcиlibm— ни Node, ни какого-либо рантайма ставить не нужно; - самая большая модель репозитория (
tools/gacascade/models/assignment.fts, 19.5 КБ исходника) разбирается примерно за 14 мс вместе со стартом процесса; - его вывод сверен с
JSON.stringify(compile(…))ядра на TypeScript и совпадает побайтово.
Бинарник говорит на том же протоколе «JSON на входе — JSON на выходе», по одному запросу на строку, так что вызвать его может любой язык, умеющий в конвейеры, — без FFI:
node -e 'const fs=require("fs");
process.stdout.write(JSON.stringify({fn:"Скомпилировать",
args:[{s:fs.readFileSync("examples/utilities/discount.fts","utf8")}]})+"\n")' \
| ./core-c/flang_cliМодули, стандартная библиотека и целый проект
flang/examples/import-check.flang:
модуль «Проба импорта»
использует «Списки» из "../stdlib/lists.flang"
тотальная функция «Сумма пробы»
принимает элементы: список числа
возвращает число
«Сумма» от элементыВыборочная форма берёт только то, что названо, — использует «Списки» из "…" только «Сумма»,
«Длина», — ею же разрешается конфликт имён между двумя модулями.
Как это раскладывается в полноразмерном проекте, показывает
examples/library-api — REST-сервис библиотеки: предметная
область описана двумя моделями FTS, разбор и обработка данных — пятью модулями flang, а HTTP и
хранилище остаются хозяину на Node. Граница проведена по одному правилу — если у куска логики
может быть пример, он уезжает в модель или в модуль языка, где пример выполняется набором, — а
правила именования файлов, раскладки каталогов, нарезки на модули и прогона в CI, выведенные из
этого проекта, собраны в Раскладке проекта.
Развитие языка
Эталонная реализация на JavaScript остаётся навсегда: относительно неё проверяется неподвижная точка, и её удаление сделало бы проверку невозможной. Работа над ней идёт в клоне:
npm install
npm run build
node scripts/build-release-c.mjs # печатает релизный C и собирает его без NodeКоманды, на которые язык отвечает:
# разобрать, проверить типы, доказать тотальность
flang check flang/examples/leetcode/035-search-insert-position.flang --pretty
# прогнать примеры, объявленные внутри функций
flang test flang/examples/leetcode/035-search-insert-position.flang --pretty
# вызвать функцию
flang run flang/examples/leetcode/035-search-insert-position.flang \
--function "Место вставки" --args '{"элементы":[1,3,5,6],"цель":2}'
# {"function":"Место вставки","args":{...},"result":1}
# напечатать — цели: c | csharp | elixir | go | java | js | python | rust
flang emit flang/examples/leetcode/035-search-insert-position.flang \
--target python --out ./out-pythonМодель .fts — тоже программа flang, и те же команды принимают её напрямую. Но этот путь идёт
через мост совместимости, которому нужно собранное ядро на TypeScript, поэтому сначала —
npm install && npm run build внутри клона:
flang check examples/utilities/discount.fts --pretty
flang emit examples/utilities/discount.fts --target go --out ./out-goСобственный CLI FTS — для моделей:
fts pipeline examples/real-world/order-shipment.fts --pretty
fts test examples/utilities/discount.fts --pretty
fts run examples/utilities/discount.fts \
--utility "Рассчитать скидку" --input examples/utilities/discount.input.json --pretty
fts certify examples/real-world/order-shipment.fts \
--context examples/real-world/order-shipment.context.json --pretty
fts generate examples/utilities/discount.fts --out generatedТесты:
npm run test:flang # язык: парсер, типы, тотальность, бэкенды, ядро и компилятор на flang
npm test # всё: ядро, инструменты, flangДифференциальные сверки ядра (flang/test/core-json.test.mjs, flang/test/core-parser.test.mjs)
идут по каждой модели .fts репозитория. FTS_MODEL_PATH (список каталогов через тот же
разделитель, что и в PATH) добавляет корпуса моделей извне; несуществующий каталог, файл вместо
каталога или каталог без единой модели — это ошибка, а не молчаливый пропуск. В обоих случаях
тесты печатают фактический охват — числа и источники, — чтобы прогон на чистом клоне отличался от
прогона с внешним корпусом по одному взгляду на вывод:
node --test flang/test/core-json.test.mjs
# ✔ корпус моделей найден — 50 моделей: только репозиторий, внешний корпус не подключён …Каждая команда пишет JSON в stdout, диагностику в stderr и возвращает ненулевой код при отказе —
один и тот же контракт везде, и именно он делает всё это пригодным для CI, редакторов и агентов.
Единственное исключение — flang repl, который разговаривает с человеком.
Остальной репозиторий
- Библиотека —
compile,validate,executeUtility,testUtilities,generateTypeScript,certify,verifyCertificate,pipeline. Ни рантайм-зависимостей, ни ввода-вывода из библиотечного API. Формат обмена описан вschema/document.schema.json; точка входа./browserдаёт разбор, проверку и визуализацию без криптографии Node.js, чтобы строгие решения по сертификатам оставались на сервере. tools/ftsc— компилятор проекта: деревья.fts-модулей, проверенные функторы между категориями, кодогенерация на восемь языков (C, Rust, C#, Java, Elixir, Go, Python, TypeScript).tools/ftsvm— исполняет утилиты по IRftscинтерпретацией или JIT-компиляцией в JavaScript.tools/ftspec— находит конфликты между спецификациями, инвариантами конституции и принятыми решениями до того, как начата реализация.- Ещё шесть инструментов в
tools/: языковой сервер FTSftsls(один LSP на VS Code, Neovim, JetBrains, Zed, Emacs и Helix), карта покрытия правилftsmap, синтез моделей из исторических решенийftsynth, эволюционный поискgasearch, каскад планированияgacascadeи общая привязка диагностики к строке и колонкеlocate. - MCP-сервер —
fts-mcp(илиfts mcp) поверх stdio, десять read-only инструментов:fts_compile,fts_check,fts_test,fts_generate,fts_execute,fts_prove,fts_visualize,fts_certify,fts_verify,fts_pipeline. См. Agent integration. - Редакторы — подсветка
.fts(Vim, VS Code, tree-sitter, Chroma, Linguist) вeditors/и языковой сервер.flangвeditors/flang-lsp. - Бенчмарки —
npm run benchmark(benchmark:quick— короткий прогон); стенд и зафиксированная база на Apple M1 Max лежат вbenchmarks/.
Дальше по теме: Описание языка · Справочник языка · Как работает FTS · Исполняемые утилиты · Прикладные примеры · Раскладка проекта · Зачем нужен FTS и как его интегрировать · Architecture (en) · Adoption (en) · спецификация flang · контракт ядра на flang · контракт самоприменения · контракт теорката · контракт конкурентности.
Правило именования документации: файл .md без языкового суффикса — английский, X.ru.md — его
русская версия. Исключение — README.md и SPEC.md рядом с кодом: они остаются под этими именами
на том языке, на котором написаны, потому что GitHub показывает их титульной страницей каталога.
Известные ограничения
Названы прямо, потому что на проект с необозначенными границами нельзя опереться. Та же граница
проведена в docs/overview.ru.md; полные списки — в
flang/SPEC.md §10 и в разделах «Долги» контрактов.
Доказано против проверено. Разница существенна, а слова звучат одинаково, поэтому:
- доказано — утверждения обо всех входах, установленные компилятором: завершение
(
тотальная), типы и исчерпывающностьразбора, стыковка композиции и цепочки и три закона функтора выше; - проверено — утверждения о конечном множестве: свойства утилит, объявленные примеры, прогоны конкурентности и согласие интерпретатора с восемью бэкендами. «Проверено на N входах» — не то же самое, что «доказано», и эта страница одно слово вместо другого не употребляет.
Расширение доказуемого возможно — условия, укладывающиеся в линейную арифметику, разрешимы, — но подключение решателя к условиям верификации остаётся открытой задачей, а не готовой возможностью.
Язык.
- Функции первого класса есть в языке и печатаются во все восемь целей. Ограничение снято
дефункционализацией (Reynolds, 1972): значение-функция — это тег,
функция «Удвоить», а применениеф от 5— диспетчер по конечному списку тегов, поэтому цели без замыканий и доказуемость завершения не страдают (flang/cat/HOF.md). Понижение делает ОДИН проход перед печатью (flang/src/defunc.mjs): бэкенду достаётся первопорядковая программа, и высшего порядка не видит ни один из восьми. Напечатанное собрано настоящими тулчейнами и сверено с интерпретатором на сетке входов. Чего пока нет — самоприменения:self/новой формы не знает, поэтому программы репозитория (stdlib,examples) ею не пользуются. - Эффекты описываются, а не выполняются, и это работает:
вариант «Прочитать файл» с путь равным …строит значение, а исполняет его хозяин (flang io,flang/src/host/node.mjs). Поручений пять — чтение и запись файла, запрос по сети, время, случайное число, — набор закрыт. Монады ввода-вывода при этом НЕТ, и причина уже не в полиморфизме: параметрические типы есть и в языке, и в самоприменении, и в стандартной библиотеке («Возможно» от «А»вflang/stdlib/optional.flang). Не хватает теоркат-слоя:checkFunctorsзнает имя типа, а не его применение, — фаза 3 вflang/cat/POLY.md. Пока её нет, последовательность действий выражается машиной продолжений, где продолжение объявлено значением, а не спрятано в замыкании; чем это отличается от монады —flang/cat/SPEC.md, раздел «Эффекты и HTTP». Слой исполнения сделан для одной цели из восьми (Node); печать программы с планом — во всех восьми. - Нет словарей, нет массивов с произвольным доступом, нет побитовых операций. Табличное динамическое программирование (Coin Change, Edit Distance) не переносится; словарь — это список пар.
- Анализ тотальности знает структурное убывание и числовую меру с ПОСТОЯННЫМ шагом. Всё, где шаг
не постоянен, остаётся вне класса: двоичный поиск делит промежуток пополам, Евклид берёт
остаток, счёт вверх растёт — им по-прежнему нужен список-«топливо». Мера при этом подпёрта
сторожем: числа flang — IEEE-754 double, и при большом |x|
x минус 1равен x, поэтому компилятор ставит на доказанном мерой вызове проверку убывания. Не убыло — отказFLANG_MEASURE, одинаковый у вычислителя и у всех восьми целей, а не зацикливание. - Вариант, названный ключевым словом (
Да,Плюс,Больше), в образцах не разбирается, и диагностика винит образец вместо настоящей причины. Обходится переименованием либо явной формойслучай вариант «Имя», которой пользуется стандартная библиотека.
Категорная поверхность. Морфизмы, композиция, цепочки, единицы, функторы, бифункторы,
изоморфизмы, моноиды, группы и монады реализованы; у монады есть форма связывания в монаде.
Не реализованы естественные преобразования — они описаны в
flang/cat/SPEC.md. Имена категорий в объявлении функтора —
пометка для читателя, а не проверяемое утверждение. Монадой сегодня не объявить список и всё
рекурсивное, включая ввод-вывод: отображение эндофунктора печатается на месте, поэтому параметр
обязан стоять в поле целиком (flang/cat/MONAD.md).
Конкурентность. Сделаны шесть шагов из семи. Не сделан шестой — планировщик в рантайме C:
процессы печатаются только в Elixir, а остальные семь целей дают из программы с процесс
обработчики как обычные функции и больше ничего. Действия породить нет, поэтому набор процессов
задан объявлением и динамического дерева, как в OTP, не бывает; адресат сообщения обязан быть
литералом; распределённости нет. Сетка семян проверяет конечный набор чередований — это
проверенное утверждение, а не доказанное, и свободы от взаимной блокировки она не даёт. Измерение
сделано на занятой машине (средняя нагрузка 18–76 при восьми доступных ядрах), поэтому числа времени в нём —
верхние оценки, и рядом с каждым названа нагрузка; числа, от неё не зависящие (витки, редукции,
байты), названы отдельно и повторяются от прогона к прогону.
Ядро, написанное на flang. Оно совпадает с ядром на TypeScript побайтово на всём корпусе, а места, где не совпало бы, известны и записаны:
- лексер: нет нормализации NFC, нет блочных комментариев, нет экспоненциальной записи (
1e3читается как имя), нет разворачивания экранирующих последовательностей внутри строковых литералов, апостроф строку не открывает, «внутри кавычек» определяется чётностью, а не автоматом, и незакрытая простая кавычка не диагностируется; - печать JSON: одиночный суррогат не экранируется (формы «код символа» в языке нет), а отрисовка
чисел передоверена встроенному
к строке, который каждый бэкенд обязан реализовать какNumber::toStringиз ECMAScript — иначе побайтовое равенство ломается на первой же дроби; - парсер: диагностики не несут ни номера строки, ни колонки (поток токенов пока не носит позиций); при глубоко незакрытых скобках код диагностики совпадает, а текст сообщения не всегда; документ, записанный сырым JSON, не разбирается;
- вычислитель: проверки уровня документа (
FTS_UNKNOWN_UTILITY,FTS_NO_UTILITIES,FTS_UTILITY_INPUT_TYPE) относятся к слою, который видит документ целиком, и здесь не реализованы.
Ни на одно из этих мест не попадает ни одна модель корпуса — именно поэтому они перечислены здесь,
а не обнаружены вами. Ядро на TypeScript остаётся рабочей реализацией FTS и эталоном, по которому
сверяется переписывание: flang/core/ воспроизводит его, но не заменяет.
Состояние
0.x — фаза проектирования языка. Канонический вид JSON и коды диагностик считаются поверхностями
совместимости; синтаксис растёт через задокументированные предложения.
Лицензия
BSD 2-Clause. Раньше проект нёс Apache-2.0, унаследованную от репозитория, из которого он вырос, а не выбранную; BSD 2-Clause — выбор осознанный. Дословный текст лицензии лежит в LICENSE; LICENSE-RU.md объясняет намерение по-русски и юридической силы не имеет. Все, кто получил код под Apache-2.0, сохраняют эти права — изменение относится к последующим версиям.
