Generation and CI: code that does not drift from the rule
This chapter shows what flang emit prints, why --check mode catches a rule
edited behind the model’s back, and how to order the CI steps so that a cheap
check fails before an expensive one.
Minimal example
модуль «Продажи»
объект «Покупка»
«сумма»: неотрицательное
«постоянный клиент»: признак
тотальная функция «Очень крупная покупка»
принимает покупка: «Покупка»
возвращает число
обеспечивает «за очень крупную покупку не больше потолка» результат не больше 15000
пример «сто тысяч — ещё не очень крупная»
дано покупка равно запись «Покупка» с «сумма» равным 100000 и «постоянный клиент» равным нет
ожидается 0
если покупка.«сумма» больше 100000
то 15000
иначе 0
тотальная функция «Рассчитать скидку»
принимает покупка: «Покупка»
возвращает число
обеспечивает «скидка ограничена» результат не больше 15000
пример «большая покупка постоянного клиента»
дано покупка равно запись «Покупка» с «сумма» равным 20000 и «постоянный клиент» равным да
ожидается 3000
пример «очень крупная покупка»
дано покупка равно запись «Покупка» с «сумма» равным 200000 и «постоянный клиент» равным нет
ожидается 15000
если покупка.«сумма» больше 100000
то «Очень крупная покупка» от покупка
иначе если покупка.«постоянный клиент»
то (покупка.«сумма» умножить на 15) делить на 100
иначе (покупка.«сумма» умножить на 10) делить на 100
Two functions, two promises. The proof report (flang check --proof) says
different things about them:
постусловие «за очень крупную покупку не больше потолка» функции «Очень крупная покупка» — доказано сведением цели с телом функции: правило «ограниченность точным потолком по построению», объявленные типы аргументов не понадобились — утверждение обо ВСЕХ входах, а не о написанных; теоремы при нём нет и не нужно
постусловие «скидка ограничена» функции «Рассчитать скидку» — сетка 2 значения (примеры функции): нарушений НЕ ИСКАЛИ — прогона примеров не было, посчитано только их число. Это не доказательство — теоремы при утверждении нет
The first promise is proved for all inputs. The compiler checked the second one only on the two values from the examples — that is what the report calls a “grid” (сетка): the proof kernel does not bound a percentage of the amount. Below you will see what this changes in the printed code.
What the compiler does
flang emit prints a checked program into one of ten languages: c, cpp, go,
rust, java, js, ts, elixir, python, csharp. Printing is preceded by the same
check as flang check: parsing, types, termination and the proof kernel. A
program with a type error is not printed at all.
flang emit order-discount.flang --target ts --out generated
проверок при работе снято 1: постусловие доказано ядром обо всех входах и
проверено примерами функции — в напечатанный код оно не едет.
напечатано файлов 5, байт 54350, в generated
проверено перед печатью — разбор, типы, завершаемость и ядро доказательств.
ПРИМЕРЫ НЕ ПРОГНАНЫ: их считает вычислитель на самом языке, и на самых больших
программах он в предел шагов этого бинарника не укладывается — свяжи с ними
печать, и компилятор перестал бы печатать сам себя. Прогоните их отдельно:
flang test <файл>
Two lines of this output matter more than the rest.
A proved promise does not travel into the code. If the kernel proved a promise for all inputs and the function has an example, there is no point in checking it on every call. For the model above the printed JavaScript looks like this:
export function ochenKrupnayaPokupka(pokupka) {
if ($cond($gt($field(pokupka, "сумма"), 100000))) {
return 15000;
} else {
return 0;
}
}
export function rasschitatSkidku(pokupka) {
let $t1;
if ($cond($gt($field(pokupka, "сумма"), 100000))) {
$t1 = ochenKrupnayaPokupka(pokupka);
} else {
let $t2;
if ($cond($field(pokupka, "постоянный клиент"))) {
$t2 = $div($mul($field(pokupka, "сумма"), 15), 100);
} else {
$t2 = $div($mul($field(pokupka, "сумма"), 10), 100);
}
$t1 = $t2;
}
// постусловие «скидка ограничена»
if (!$post($lte($t1, 15000), "скидка ограничена", "Рассчитать скидку")) {
$fail(
"FLANG_PROPERTY",
"нарушено свойство «скидка ограничена» функции «Рассчитать скидку»",
{ line: 21, column: 3 },
);
}
return $t1;
}
The first function has no check: its promise is proved. The second keeps a
runtime check: its promise rests on examples only, and the printed code will
stop with an error coded FLANG_PROPERTY if the rule ever returns more than the
cap. There will be no silently wrong number.
Printing does not run the examples. On a model with a failing example
flang emit writes the files and exits with code 0. That is why flang test
runs as a separate step before printing — both in the generation script and in
CI.
Function names in the printed code are transliterated (rasschitatSkidku), and
the mapping to the model’s names lives in the module itself:
$PROGRAM.functions is a table from the flang name to the function. The course
tests call functions through it: functions.get('Рассчитать скидку').
--check mode and how drift is caught
examples/spec/typescript-codegen/generate.mjs wraps printing in
writeGenerated. The script first runs flang test, then
flang emit --target ts. In --check mode it writes nothing and compares the
printed output with what is in generated/:
node examples/spec/typescript-codegen/generate.mjs # записать generated/
node examples/spec/typescript-codegen/generate.mjs --check # сверить с моделью
If someone edits return 15000 directly in the .ts file instead of the model,
--check sees the mismatch and exits with code 1:
Напечатанный код отстал от модели: program.ts
Запустите: node examples/spec/typescript-codegen/generate.mjs
generate.test.mjs checks this in a dedicated test: it prints the module into a
temporary directory, overwrites program.ts with the line
// правка мимо модели and expects stale: ['program.ts']. A second test
guards against the opposite mistake — printing a model that fails its own
examples. Replace the expectation 3000 with 4200, and the script stops
before printing:
модель не проходит свои примеры:
/model.flang: примеров 10, прошло 8, не прошло 2
пример «большая покупка постоянного клиента» функции «Рассчитать скидку» не прошёл: значение не совпало с ожидаемым: ожидалось 4200, получено 3000
A third test imports the committed program.ts — Node 23.6 and later reads
TypeScript by itself — and compares its answers with the model’s examples.
Printed code that computes something other than the model fails this test.
Byte-for-byte comparison works because printing is deterministic: the native
flang binary and the WebAssembly build print the same module, and --check
gives the same answer with either of them.
Order of CI steps and what fails first
A ready workflow lives in examples/spec/ci/flang-check.yml (it goes into
.github/workflows/flang-check.yml):
- run: npm ci
- name: Поставить flang
run: |
curl -fsSL https://raw.githubusercontent.com/digitable-lol/flang/main/install.sh \
| PREFIX="$RUNNER_TEMP/flang" sh
echo "$RUNNER_TEMP/flang/bin" >> "$GITHUB_PATH"
echo "FLANG_BIN=$RUNNER_TEMP/flang/bin/flang" >> "$GITHUB_ENV"
- name: Проверить модели
run: |
for model in static/spec/models/*.flang; do
flang check "$model" --proof || [ $? -eq 3 ]
done
- name: Выполнить предметные примеры
run: flang test static/spec/models/
- name: Сверить напечатанный код
run: node examples/spec/typescript-codegen/generate.mjs --check
- name: Тесты интеграций
run: npm run spec:examples
The compiler version is pinned by the variable FLANG_VERSION: 0.7.29 at the
workflow level: install.sh reads it and verifies the archive against the
release checksums. Upgrading the compiler is a separate change, not a surprise
inside someone else’s pull request.
The order is not accidental — each step is cheaper and more precise than the next one:
flang check --proof— parsing, types, termination and the proof report. A typo in a field name fails here in a fraction of a second. Code 1 is an error and the step turns red. Code 3 means “checked, but some promises rest on examples only”; the step lets it pass, and the report stays in the log.flang test— domain examples: the rule compiled but returns the wrong number. A whole directory is checked with one command:flang test static/spec/models/.generate.mjs --check— drift between the model and the committed TypeScript. The model is already correct at this step; a failure means someone forgot to reprint after editing the model.npm run spec:examples— integrations: the HTTP service, shipment admission, the form, equivalence tests. The most expensive layer, furthest from the model, fails last.
Code 3 is the team’s decision. flang check without --proof says nothing
about proofs and exits with zero. With --proof, code 3 separates “proved”
from “checked on examples”. The --strict key separates outcomes more strictly:
0 — everything proved, 3 — something rests on examples, 1 — refuted. For a model
with a percentage discount --strict always gives 3, so the workflow does not
use it. If your rules are proved completely, add --strict and remove
|| [ $? -eq 3 ].
Agent work
flang ships a service for an AI assistant — flang --mcp-mode, JSON-RPC over
the standard streams. It offers two tools: flang_check (check a program and
return the proof report) and flang_prove (what proves one named promise).
Printing is not among them.
That is a convenient boundary. The agent edits the model and immediately sees
three separate verdicts: proved, “grid N” and “stated, not proved” — the answer
has no green “ok” tick. Printing code, writing files and committing remain a
separate step carried out by a person or a pipeline with flang emit and
generate.mjs.
Practice in the sandbox
The sandbox checks the model with the same compiler and shows which promises
are proved. Change the cap of the very-large-purchase function from 15000 to
16000 and watch the proved promise turn into an error. Then restore 15000 and
change an expectation in an example — this is what a broken example looks like,
the kind flang emit alone would let through.
Common mistakes
FLANG_EXAMPLE— an example failed.flang checkandflang testexit with code 1, whileflang emitprints the model as if nothing happened. Never print withoutflang testbefore it.FLANG_PROPERTY— the printed function broke a promise the kernel did not prove. In CI this is a failed integration test, not a silently wrong number.FLANG_TYPE— an input value of the wrong type. The runnerflang_cli.jsnext to the printed module checks the input against the declared types before the call; a direct call of a module function from your own code does not do that check — guard the input boundary yourself (likereadPurchaseinexamples/spec/discount-api/server.mjs).
Checklist
- The model passes
flang check --proofandflang testbefore printing. generate.mjsor your equivalent callsflang testbeforeflang emit.- CI verifies the committed code with
--checkinstead of trusting the developer to remember to reprint. - The flang version in CI is pinned.
- CI steps go from the cheap check to the expensive integrations.
- The decision about code 3 is written into the workflow explicitly: let it
pass or require
--strict. - A pull request that edits the model shows a small readable diff of the model, not only a large diff of the printed file.