Исполняемые спецификации на flang Из Python, Go и shell: работающие клиенты, а не декларации
0%

Из Python, Go и shell: работающие клиенты, а не декларации

Из Python, Go и shell: работающие клиенты, а не декларации

Модель на flang не привязана к Node.js. Граница между ней и остальным кодом — не библиотека, а процесс: любой язык, который умеет запустить команду и прочитать её вывод, может посчитать функцию модели. Дальше — три работающих клиента: Python, Go и shell. Контракт один, результат один. Все три лежат в examples/spec/clients/ и правда запускаются.

Минимальный пример

Модель — расчёт скидки из static/spec/models/order-discount.flang, та же, что у HTTP-сервиса из главы про Node.js. Для вызова из командной строки в ней есть функция с двумя скалярными аргументами:

тотальная функция «Скидка по сумме»
  принимает сумма: неотрицательное, постоянный: признак
  возвращает число
  пример «тот же расчёт без записи на входе»
    дано сумма равно 20000
    дано постоянный равно да
    ожидается 3000
  «Рассчитать скидку» от (запись «Покупка» с «сумма» равным сумма и «постоянный клиент» равным постоянный)

Она собирает запись «Покупка» и зовёт «Рассчитать скидку». Вызов:

$ flang run static/spec/models/order-discount.flang --function "Скидка по сумме" --args '{"сумма": 20000, "постоянный": true}' --trust
на веру: доказанность не считалась — запуск по ключу --trust
3000

Что делает компилятор

flang run считает одну функцию сам: ни Node, ни компилятор C для этого не нужны. Перед вычислением он сверяет аргументы с объявленными типами и выносит вердикт о доказанности.

Вердикт здесь важен. В модели три обещания из четырёх держатся только на примерах: в отчёте flang check --proof они названы «сетка N» — проверено на N значениях из примеров, это не доказательство. Недоказанную программу flang run без явного согласия не считает и выходит кодом 3:

$ flang run static/spec/models/order-discount.flang --function "Скидка по сумме" --args '{"сумма": 20000, "постоянный": true}'
не доказано: утверждений 4: доказано 1, сетка 3, на веру 0 — запуск только по явному согласию: --на-веру

Ключ --trust даёт согласие на один прогон. Ключ unproven в .flangrc — на все. Клиенты ниже передают --trust явно, чтобы согласие было видно в коде.

Граница через JSON

Контракт flang run простой:

  • аргументы — --args, плоский JSON скаляров: число, строка, да/нет. Записи и списки сюда не передать, поэтому в модели и есть «Скидка по сумме»;
  • значение — в stdout, одной строкой;
  • вердикт о доказанности и ошибки — в stderr, ошибка с кодом FLANG_…;
  • код возврата: 0 — посчитано, 1 — ошибка во входе или в программе, 2 — не разобран сам вызов, 3 — программа не доказана и запуск без согласия отклонён.

Устройство самой модели тоже доступно машине без парсера flang: flang ast печатает разобранную программу JSON-ом — объекты с полями и типами, функции с параметрами и примерами. Из него HTTP-сервис строит контракт, а форма — свою схему.

Поставить flang:

curl -fsSL https://raw.githubusercontent.com/digitable-lol/flang/main/install.sh | sh

Клиенты берут путь к двоичному из переменной FLANG, по умолчанию — flang из PATH.

Клиент на Python

examples/spec/clients/python/calculate_discount.py — только subprocess и json из стандартной библиотеки:

def calculate_discount(amount: int, loyal: bool, model: Path = DEFAULT_MODEL) -> float:
    """Вызывает функцию модели «Скидка по сумме» и возвращает число."""
    command = [
        os.environ.get("FLANG", "flang"), "run", str(model),
        "--function", FUNCTION,
        "--args", json.dumps({"сумма": amount, "постоянный": loyal}, ensure_ascii=False),
        "--trust",
    ]
    completed = subprocess.run(command, capture_output=True, text=True, env={**os.environ, "LC_ALL": "C.UTF-8"})
    if completed.returncode != 0:
        raise RuntimeError(describe_failure(completed))
    return float(completed.stdout.strip())

Ошибка не глотается. describe_failure ищет в stderr первую строку с кодом FLANG_… — это причина отказа словами компилятора — и показывает её вместе с кодом возврата.

Запуск и вывод:

$ python3 calculate_discount.py
сумма=20000 постоянный_клиент=False -> скидка=2000

$ python3 calculate_discount.py --сумма 20000 --постоянный-клиент
сумма=20000 постоянный_клиент=True -> скидка=3000

$ python3 calculate_discount.py --сумма -5
ошибка: flang завершился с кодом 1: FLANG_TYPE: вызов функции «Скидка по сумме»: аргумент «сумма»: -5 вне неотрицательное

Клиент на Go

examples/spec/clients/go/main.go использует только os/exec и encoding/json:

func calculateDiscount(sum int, loyal bool, model string) (float64, error) {
	arguments, err := json.Marshal(map[string]any{"сумма": sum, "постоянный": loyal})
	if err != nil {
		return 0, err
	}

	bin := os.Getenv("FLANG")
	if bin == "" {
		bin = "flang"
	}
	cmd := exec.Command(bin, "run", model, "--function", functionName, "--args", string(arguments), "--trust")
	cmd.Env = append(os.Environ(), "LC_ALL=C.UTF-8")
	var out, errOut bytes.Buffer
	cmd.Stdout = &out
	cmd.Stderr = &errOut

	if err := cmd.Run(); err != nil {
		return 0, fmt.Errorf("flang: %v: %s", err, reason(errOut.String()))
	}
	return strconv.ParseFloat(strings.TrimSpace(out.String()), 64)
}

stdout и stderr пишутся в разные буферы: причина отказа не потеряется, даже если процесс завершился ошибкой. Вывод:

$ go run main.go
сумма=20000 постоянный_клиент=false -> скидка=2000

$ go run main.go -sum 20000 -loyal
сумма=20000 постоянный_клиент=true -> скидка=3000

Клиент на shell

examples/spec/clients/shell/discount.sh — bash и jq. JSON аргументов собирает jq: склеивать его строками в bash легко сломать на кавычках и юникоде в сумма.

ARGS="$(jq -cn --argjson sum "$SUM" --argjson loyal "$LOYAL" '{"сумма": $sum, "постоянный": $loyal}')"

set +e
LC_ALL=C.UTF-8 "$FLANG" run "$MODEL" --function "Скидка по сумме" --args "$ARGS" --trust >"$STDOUT_FILE" 2>"$STDERR_FILE"
STATUS=$?
set -e

if [[ $STATUS -ne 0 ]]; then
  # Первая строка с кодом FLANG_… — причина отказа словами компилятора.
  REASON="$(grep -m1 'FLANG_' "$STDERR_FILE" || head -n1 "$STDERR_FILE")"
  echo "ошибка (код $STATUS): $REASON" >&2
  exit "$STATUS"
fi

set -e на время вызова снимается намеренно: иначе скрипт вышел бы раньше, чем прочитал stderr и показал причину. Вывод:

$ ./discount.sh
сумма=20000 постоянный_клиент=false -> скидка=2000

$ ./discount.sh 20000 true
сумма=20000 постоянный_клиент=true -> скидка=3000

$ ./discount.sh -5 true
ошибка (код 1): FLANG_TYPE: вызов функции «Скидка по сумме»: аргумент «сумма»: -5 вне неотрицательное

Через HTTP вместо CLI

Если запуск процесса на каждый вызов дорог, есть долгоживущий HTTP-сервис. examples/spec/discount-api так и устроен: примеры модели прогоняются один раз при старте, модель печатается в JavaScript, дальше Node.js только маршрутизирует запросы.

npm run spec:api
curl -s localhost:8788/discount -d '{"сумма":20000,"постоянный клиент":true}'
# {"discount": 3000}

Ошибка входа приходит кодом 400 с причиной:

{
  "error": "вход не подходит к «Покупке»: «сумма»: ждали неотрицательное целое",
  "code": "INPUT"
}

Любой клиент выше переписывается на requests, net/http или curl вместо запуска процесса. Выбор между CLI и HTTP — выбор между простотой (процесс на вызов, ничего не держать живым) и пропускной способностью (модель готовится один раз, дальше только сеть).

Третий способ: напечатать модель в свой язык

У обоих способов выше рядом с вашим кодом что-то живёт: двоичный flang или сервис на Node. Иногда этого нельзя. Тогда модель печатается в целевой язык заранее, командой flang emit. Целей десять:

$ flang emit static/spec/models/order-discount.flang --target zzz
flang emit: цели «zzz» у этой сборки flang нет — целей здесь ДЕСЯТЬ — «c», «cpp», «go», «rust», «java», «js», «ts», «elixir», «python», «csharp».

Печать в Python:

$ flang emit static/spec/models/order-discount.flang --target python --out printed
…
напечатано файлов 4, байт 109125, в printed
…
$ ls printed
Makefile  flang_cli.py  flang_runtime.py  prodazhi.py

Модуль prodazhi.py работает без зависимостей. Функции модели в нём — обычные функции Python, с приставкой роли в имени:

import prodazhi, flang_runtime as rt

ctx = prodazhi.new_context()
p = prodazhi.rec_pokupka(rt.number(20000), rt.flag(True))
print(prodazhi.fn_rasschitat_skidku(ctx, p))
# <flang число: 3000>

Приставка роли (fn_ у функции, rec_ у конструктора записи) нужна, чтобы объект и функция с одинаковым именем не столкнулись после транслитерации.

Рядом лежит прогонщик flang_cli.py. Запрос — одна строка JSON, ответ — тоже одна строка:

$ echo '{"fn":"Рассчитать скидку","args":[{"r":[["сумма",{"n":"20000"}],["постоянный клиент",true]]}]}' | python3 flang_cli.py prodazhi
{"ok":true,"value":{"n":"3000"}}

Значения в протоколе размечены: число едет строкой ({"n":"20000"}), чтобы не потерять -0, NaN и бесконечность; строка — {"s":"…"}; запись — {"r":[["поле", …]]}; признак — true/false. Прогонщик сверяет вход с типами модели до вызова:

$ echo '{"fn":"Рассчитать скидку","args":[{"r":[["сумма",{"n":"-1"}],["постоянный клиент",true]]}]}' | python3 flang_cli.py prodazhi
{"ok":false,"code":"FLANG_TYPE","message":"вызов функции «Рассчитать скидку»: аргумент «покупка».сумма: -1 вне неотрицательное"}

Протокол один у всех целей. Та же модель, напечатанная в Java:

$ flang emit static/spec/models/order-discount.flang --target java --out printed-java
…
напечатано файлов 8, байт 163319, в printed-java
…
$ cd printed-java && make build
javac -encoding UTF-8 -Xlint:all -Werror -d . *.java
$ printf '{"fn":"Рассчитать скидку","args":[{"r":[["сумма",{"n":"20000"}],["постоянный клиент",true]]}]}\n' | java -cp . FlangCli Prodazhi
{"ok":true,"value":{"n":"3000"}}

Три тысячи — ровно то, что записано в примере «большая покупка постоянного клиента» в самой модели. Прогонщик нужен для вызова через трубу. В своей программе на Java функции зовутся напрямую:

Ctx ctx = Prodazhi.newContext();
Value покупка = Prodazhi.rec_pokupka(Value.number(20000), Value.flag(true));
System.out.println(Prodazhi.fn_rasschitat_skidku(ctx, покупка));   // <flang число: 3000>

Напечатанный код проверяет недоказанные обещания модели при каждом вызове и бросает FLANG_PROPERTY, если обещание нарушено. Доказанные обещания в код не едут.

Что выбрать

Способов три, и выбирают по одному вопросу — что вы готовы держать рядом со своим кодом.

Способ Что живёт рядом Когда брать
flang run в процессе двоичный flang и модель редкие вызовы, скрипты, CI
HTTP-сервис сервис с моделью, один на всех высокая частота, короткий SLA
печать в целевой язык ничего — только ваш код flang поставить нельзя или процесс на вызов недопустим

У третьего способа есть цена. Напечатанный код — срез модели на момент печати: изменили модель — обязаны напечатать заново. Решается это шагом --check в CI, который сравнивает напечатанное с моделью (глава «Генерация и CI»). И ещё одно: flang emit примеры не прогоняет, flang test нужен до печати.

Практика в песочнице

Найдите в отчёте о доказательствах три обещания, которые держатся только на примерах. Именно из-за них flang run просит --trust. Поменяйте сумму в примере «тот же расчёт без записи на входе» и посмотрите, какой пример перестанет сходиться.

Типичные ошибки

Ниже — настоящие ответы flang run на модели из этой главы.

FLANG_TYPE, значение не того типа (код 1):

FLANG_TYPE: вызов функции «Скидка по сумме»: аргумент «сумма» не соответствует типу неотрицательное

FLANG_TYPE, аргумента не хватает (код 1, в примере пропущен постоянный):

FLANG_TYPE: вызов функции «Скидка по сумме» функции «Скидка по сумме» не задаёт аргумент «постоянный»

FLANG_UNKNOWN_NAME, лишний аргумент (код 1). Лишний ключ flang не пропускает молча: чаще всего это значит, что клиент и модель разошлись.

FLANG_UNKNOWN_NAME: вызов функции «Скидка по сумме» функции «Скидка по сумме» задаёт неизвестный аргумент «вес»

FLANG_UNKNOWN_NAME, опечатка в имени функции (код 1):

FLANG_UNKNOWN_NAME: не найдена функция «Расчитать скидку»

Запись в --args (код 2): --args принимает только плоский объект скаляров.

flang run: «--args» разобрать не удалось — ждался плоский объект скаляров, вроде '{"н":10}'

Код 3 без вывода значения — программа не доказана, а --trust не передан.

Общее правило для клиента на любом языке: ветвитесь по коду FLANG_… и коду возврата, а не по тексту сообщения. Текст пишется для человека.

Чек-лист

  • Клиент читает stdout как значение только при коде возврата 0; при ненулевом — берёт причину из stderr.
  • Аргументы — плоский JSON скаляров в --args; для записи в модели есть функция со скалярными аргументами.
  • Согласие на недоказанное (--trust) передаётся явно, и в коде видно, что оно есть.
  • Ветвление по ошибке — по коду FLANG_…, а не по тексту.
  • Для нечастых вызовов — процесс, для высокой частоты — HTTP-сервис, без flang на машине — напечатанный модуль и шаг --check в CI.

Кейсы каталога по этой теме

Дальше: генерация и CI

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

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

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

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