English · Русский
A written specification drifts from the code the day after it is merged. This repository takes the other route: the specification is the program. You write the rules once, run them, test them against their own examples, and then print them into C, Go, Rust, Python, Java, C#, Elixir or JavaScript — where the printed code is required to produce the same values and the same error codes as the interpreter, checked input by input.
The authoring surface is Russian; an English surface exists and lexes to the same identifiers
(функция / function, свёртка / fold). The prose below is English, the code is not
translated — names in a specification belong to the domain that wrote them.
- FTS (
.fts) — an indentation-based executable specification language for domain objects, deterministic utilities, examples, checked properties, morphisms and machine-checkable evidence. Its reference implementation is the TypeScript core insrc/. flang(.flang) — the full language FTS grew into: sum types, lists, strings as data, recursion, pattern matching, module linking, a category surface, a concurrency surface and eight code generators. Its implementation isflang/src/.
FTS is the total subset of flang: every existing .fts model is a valid flang program. That is
not a slogan but a differential test — both engines run every utility of every model over a grid
of inputs, and both the values and the error codes must agree. The run prints its own numbers:
сверка: файлов 22, документов 20, из них с утилитами 13; утилит 24, входов 23084,
из них с ошибкой 773 (коды: FTS_UTILITY_PROPERTY), расхождений 0
Two documents carry the rest: docs/overview.ru.md describes the language
and draws the line between what is proven and what is checked, and
flang/SPEC.md is the specification. This page does not go past that line.
The layout follows from the section above, and it surprises on first sight: 15 directories at
the root, several of the names repeated. There is src/ and there is flang/src/; there is test/
and flang/test/; there is examples/ and flang/examples/. Two languages mean two
implementations, two test runs and two example corpora. Merging them would erase the seam the
checking runs along — each side is the reference the other is compared against, and with one
directory there would be nothing left to compare.
src/ the FTS core in TypeScript — the reference everything else is true against
test/ its test run; built into dist/ and executed from there
bootstrap/ the bootstrap point: the compiler printed to C99 — «make -C bootstrap», no Node
flang/src/ the flang implementation in JavaScript — the reference for the language
flang/self/ the same compiler, written in flang itself
flang/core/ the same FTS core, written in flang: lexer, parser, evaluator, JSON printing
flang/stdlib/ the standard library; its index is printed from the modules themselves
flang/examples/ flang programs: leetcode, rosetta, cat, monad, io, web, errors
flang/test/ the language test run — from the lexer to all eight backends
flang/bin/ flang and flang-lsp: adapters over flang/src, never a home for meaning
flang/cat/ the category-surface contract
flang/conc/ the concurrency contract and its examples
examples/ .fts models, and library-api — a whole REST service on FTS and flang
schema/ the interchange format: JSON Schema for the document and the certificate
tools/ 9 tools built on top of the compiled core
editors/ .fts syntax highlighting and the .flang language server
web/ the same compiler as a page element — no server, no build step
packaging/ Homebrew, asdf and the flang.1 man page
scripts/ printing the library index, the changelog and the release C
benchmarks/ the harness and a checked-in measurement baseline
.claude/ developer assistant skills: knowledge-base rules
docs/ documentation; README and SPEC files stay next to the code they describe
.github/ CI and the fts-check action
How to tell what checks a file without opening it. By its directory and extension: src/ and
test/ go through npm run test:core, everything under flang/ through npm run test:flang, each
tool carries its own tools/*/test/, and npm test runs all three suites. A file you cannot
immediately assign to one of those commands is filed in the wrong place.
Laying out your own project on FTS and flang is a separate document: Раскладка проекта.
Installing flang needs a C99 compiler and nothing else. The compiler is written in flang itself and prints to C, so the release ships that C already printed.
brew install digitable-lol/tap/flangOr straight from the release archive, with nothing but cc and make:
tar -xzf flang-*-c.tar.gz # inside: C99 sources, a Makefile and the flang.1 man page
make # cc -std=c99 -Wall -Wextra -Werror -pedantic -O2
./flang_cli --help # what it does: check, repl, --version
./flang_cli check m.flang # parse, types, totality — in words, not JSON
./flang_cli # with no arguments: JSON in, JSON out, one request per lineThe Homebrew formula is packaging/homebrew/flang.rb and the
tap serves it. The asdf (and mise) plugin installs the same archive from the same releases, and
its source is packaging/asdf/ — but asdf clones a plugin as a whole
repository, and that repository is not published yet, so for now the plugin is source rather than
an install path. Neither needs anything but a C compiler. This is how self-hosting languages
ship — Go carried generated C for years, Nim still does.
Be clear about what that binary is. It is the five layers of flang/self/:
lexer, parser, types, totality, printing to C. There is no evaluator among them — which is why
flang repl there evaluates the only honest way this binary can: it prints the session to C,
builds it with the system cc against the runtime installed beside it, and runs that. Without a
cc the shell does not switch off — it keeps checking parse, types and totality, and says so
once. Checking a file needs nothing else: flang check file.flang runs parse, linking, types and
totality and prints its findings in words — with a code and a place, not JSON. flang --help
lists the commands and man flang describes them. Running a program or its examples
non-interactively still needs the full toolchain below.
The full toolchain does need Node.js 20 or newer, and here is exactly why: the interpreter, the language server, the MCP server and seven of the eight backends exist only in JavaScript. The self-hosted compiler — the one in the release — prints to C and nothing else.
npm install -g @digitable-lol/ftsThat gives the commands used on this page: flang for the language, fts for models, fts-mcp
for the MCP server, plus ftsc, ftsvm and ftspec. Inside a clone the same commands are
node flang/bin/flang.mjs and node dist/src/cli.js — and a clone is what you need for anything
newer than the last published release.
In a clone, too, the compiler builds without Node. The tree carries a bootstrap point — the same compiler printed to C99, 7 files and 5,823,370 bytes:
git clone https://github.com/digitable-lol/flang && cd flang
make -C bootstrap # only cc and make; about 4 minutes of CPU
bootstrap/flang_cli --versionWhat it is, what guards it and how it is updated: bootstrap/README.md.
Node is still needed for the reference implementation and seven of the eight backends, but not to
build the compiler; see Developing the language.
This is flang/examples/leetcode/035-search-insert-position.flang — LeetCode 35, the position
where a value belongs in a sorted list. One fold, proven terminating:
тотальная функция «Место вставки»
принимает элементы: список числа, цель: число
возвращает число
пример «Пример 1 из условия»
дано элементы равно [1, 3, 5, 6]
дано цель равно 5
ожидается 2
свёртка элементы начиная с 0 как акк и эл → если эл меньше цель то акк плюс 1 иначе акк
Everything below was produced by running
flang emit flang/examples/leetcode/035-search-insert-position.flang --target c --out ./out-c
# …and again with go, rust, python, java, csharp, elixir, jsand is pasted verbatim, not written by hand. Seven backends emit the module, a runtime, a
JSON-in/JSON-out driver, a build file and — where the target has one — a package manifest
(go.mod, Cargo.toml, flang.csproj); the JavaScript backend emits a single self-contained
module plus the same driver next to it (flang_cli.js, dropped by --no-cli) — the driver is
what makes the declared call-depth limit real for an ordinary run. Two of the eight are shown
here, and only the function itself; the other six read the same way — run the command and look.
C — out-c/mesto_vstavki.c
/*
* Функция 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;
}JavaScript — out-js/mesto_vstavki.js, a single dependency-free file
/**
* Функция 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
}The JS backend inlines only the runtime helpers this module actually uses, so the module itself stays one self-contained file that runs in Node and in the browser. The driver is emitted beside it as a separate file and is not part of the module: the browser does not need it, and under Node it is what makes the declared call-depth limit real.
The generated code is not a sketch you finish by hand. It carries the domain names in comments, it reports the interpreter's diagnostic codes and messages verbatim, and the header says what it is: «Правьте исходник на flang и печатайте заново: любая правка здесь потеряется.»
Each backend is checked differentially, not by golden files. The corpus is the standard library
and the LeetCode solutions — flang/stdlib/*.flang and flang/examples/leetcode/*.flang,
95 programs with 511 functions and 1291 examples between them. For every function a grid of inputs
is built from its own examples plus deliberately wrong arguments (null, a string where a list is
wanted, a variant that does not exist), the program is printed into an empty directory, compiled
with the real toolchain from nothing but what the backend emitted, and run as a real process.
The run reports what it covered, so the claim is checkable rather than quoted:
✔ stdlib и leetcode: собранный C# совпадает с интерпретатором
ℹ программ: 95, функций: 511, сверенных входов: 8151, из них по лимиту шагов только по коду: 3, за 754 с
✔ примеры stdlib и leetcode сходятся у C# так же, как у интерпретатора
ℹ сверенных примеров: 1291
The C backend additionally compiles under gcc and clang with
-std=c99 -Wall -Wextra -Werror -pedantic -O2 and is checked under valgrind for zero
unreachable bytes.
A rule is written once, in the form a domain expert reads, not only a programmer. From that
single source come the implementation, the tests and the checks — in eight languages at once,
and a declared свойство becomes a postcondition of the emitted code: a Python service, a Go
service and a C binary refuse the same input with the same words.
The worked example, from source to emitted postcondition — Why this exists.
LeetCode 121 — best profit from one buy and one sell, one pass, state in a two-field record.
This is flang/examples/leetcode/121-best-time-to-buy-and-sell-stock.flang in full:
объект «Сделка»
минимум является числом
прибыль является числом
тотальная функция «Лучшая прибыль»
принимает цены: список числа
возвращает число
пример «Пример 1 из условия»
дано цены равно [7, 1, 5, 3, 6, 4]
ожидается 5
пример «Пример 2 из условия»
дано цены равно [7, 6, 4, 3, 1]
ожидается 0
пример «Пустой список»
дано цены равно пустой список
ожидается 0
разбор цены
случай пусто
то 0
случай голова и хвост
пусть начальное равно запись «Сделка» с минимум равным голова и прибыль равным 0
пусть итог равно свёртка хвост начиная с начальное как акк и цена
пусть минимум равно если цена меньше акк.минимум то цена иначе акк.минимум
пусть сегодня равно цена минус акк.минимум
пусть прибыль равно если сегодня больше акк.прибыль то сегодня иначе акк.прибыль
запись «Сделка» с минимум равным минимум и прибыль равным прибыль
итог.прибыль
It reads as Russian prose — "разбор цены / случай пусто / то 0" — and the тотальная keyword on
the first line is a claim the compiler had to prove before accepting the file. The examples are
part of the function, not a separate test file:
flang test flang/examples/leetcode/121-best-time-to-buy-and-sell-stock.flang --prettyTwo example sets are kept, and both are guarded by tests rather than by good intentions.
flang/examples/leetcode/ holds 82 solutions; 81 of them are total,
as are 301 functions out of 303 — the single exception is deliberate and explained in the file
(202-happy-number.flang: the "until the number repeats" loop does terminate, but the language
has nothing to prove it with). Each carries a comment explaining not only the algorithm but where
the language pushed back — why "is this character already in the window" is linear (there is no set
in the language), why a dynamic-programming table costs a square (appending copies the list), why
Single Number is O(n²) because there are no bitwise operations. Of the twelve tasks previously
listed as inexpressible, eight are solved by this batch, and their entries in index.json have
been rewritten.
flang/examples/rosetta/ holds 14 canonical Rosetta Code tasks, each
written twice — 28 files: once on the Russian surface and once on the English one, with a test
comparing each pair as trees, up to a renaming of names. That test also pins the number of
functions each file proves total: the set exists to show the
border of the language, so a border that moves has to break a test rather than quietly outdate a
comment. The standard library (flang/stdlib/: dictionary, hashmap, higher-order,
lists, logic, numbers, numtree, optional, result, sets, strings, strlists, tree) is written the same
way —
13 modules, 208 functions, of which 204 are proven total. higher-order is the one built on
first-class functions: fold, map, filter, search, sort and composition take a function as an
argument.
Turing completeness and guaranteed termination are incompatible, so flang does not choose
between them: it splits programs into two classes, and the compiler decides which class yours
is in. A тотальная function has its termination proven, and only such a function is admitted
into fact-checking, which is not allowed to hang.
Which kinds of descent are accepted, what a declared measure is, and why this is not pedantry —
What тотальная buys you.
There are two implementations, and both are maintained on purpose. The reference one is
written in TypeScript and JavaScript and defines the behaviour of the language. The
self-hosted one is written in flang itself: flang/core/ is the FTS core,
flang/self/ is the compiler — five layers, each checked byte for byte against
its own reference.
Readiness is not "it built" but the classical fixed point, and it has converged. How it works, what checks it and where the release comes from — Two implementations, and the fixed point.
flang/examples/import-check.flang:
модуль «Проба импорта»
использует «Списки» из "../stdlib/lists.flang"
тотальная функция «Сумма пробы»
принимает элементы: список числа
возвращает число
«Сумма» от элементы
A selective form takes only what you name — использует «Списки» из "…" только «Сумма», «Длина» —
which is also how a name conflict between two modules is resolved.
How that scales to a full-size project is shown by
examples/library-api, a REST service for a library: the
domain is two FTS models, parsing and data handling are five flang modules, and HTTP and storage
stay with the host on Node. The rule the split follows is one sentence — if a piece of logic can
have an example, it moves into a model or a module, where the example is executable — and the
naming, layout, module-splitting and CI conventions derived from that project are collected in
Раскладка проекта.
The JavaScript reference implementation stays forever: the fixed point is checked against it,
and removing it would make that check impossible. Work happens in a clone —
npm install && npm run build.
What to run when you change the compiler, how the bootstrap point is guarded, and the full list of commands the language answers to — Developing the language.
- Library —
compile,validate,executeUtility,testUtilities,generateTypeScript,certify,verifyCertificate,pipeline. No runtime dependencies, no I/O from the library API. The interchange format isschema/document.schema.json; the./browserentrypoint gives parsing, validation and visualization without Node.js cryptography, so strict certificate decisions stay on the server. tools/ftsc— the project compiler: trees of.ftsmodules, checked functors between categories, code generation for eight languages (C, Rust, C#, Java, Elixir, Go, Python, TypeScript).tools/ftsvm— executes utilities from theftscIR by interpretation or by JIT to JavaScript.tools/ftspec— finds conflicts between specifications, constitution invariants and recorded decisions, before implementation starts.- Six more tools in
tools/, each with its own README, and the read-only MCP serverfts-mcpover stdio — see Agent integration. - Editors — syntax highlighting for
.fts(Vim, VS Code, tree-sitter, Chroma, Linguist) ineditors/, and the.flanglanguage server ineditors/flang-lsp. - Benchmarks —
npm run benchmark(benchmark:quickfor a short run); the harness and a checked-in Apple M1 Max baseline are inbenchmarks/.
All documentation, with an index — docs/README.md: the guide, measurement
reports, the knowledge base and the conference submission.
Further reading — in English: Architecture · Adoption · Agents. In Russian (the language surface is Russian, and so is most of the prose): Описание языка · Справочник языка · Как это работает · Исполняемые утилиты · Прикладные примеры · Раскладка проекта · Зачем нужен FTS и как его интегрировать · flang SPEC · core-in-flang contract · self-hosting contract · category contract · concurrency contract.
The documentation naming rule: an .md file with no language suffix is English, X.ru.md is its
Russian version. The exception is README.md and SPEC.md next to code — they keep those names
in whichever language they are written, because GitHub shows them as a directory's front page.
Stated plainly, because a project with unmarked boundaries cannot be relied on. The full list
is Known limits: what proven means against checked, what the
language does not have, where the categorical surface stops, and what is done in concurrency.
The same boundary is drawn in docs/overview.ru.md; the complete lists
are in flang/SPEC.md §10 and in the "Долги" sections of the contracts.
0.x is the language-design phase. The canonical JSON shape and the diagnostic codes are treated
as compatibility surfaces; syntax may grow through documented proposals.
BSD 2-Clause. The project previously carried Apache-2.0, inherited from the repository it grew out of rather than chosen; BSD 2-Clause is the deliberate choice. See LICENSE.