From Python, Go, and shell: working clients, not declarations
A flang model is not tied to Node.js. The boundary between it and the rest of
the code is not a library but a process: any language that can start a command
and read its output can compute a model function. Below are three working
clients: Python, Go and shell. One contract, one result. All three live in
examples/spec/clients/ and actually run.
Minimal example
The model is the discount calculation from
static/spec/models/order-discount.flang, the same one the HTTP service uses
in the Node.js chapter. For a command-line call it has a function with two
scalar arguments:
тотальная функция «Скидка по сумме»
принимает сумма: неотрицательное, постоянный: признак
возвращает число
пример «тот же расчёт без записи на входе»
дано сумма равно 20000
дано постоянный равно да
ожидается 3000
«Рассчитать скидку» от (запись «Покупка» с «сумма» равным сумма и «постоянный клиент» равным постоянный)
It builds a «Покупка» record and calls «Рассчитать скидку». The call:
$ flang run static/spec/models/order-discount.flang --function "Скидка по сумме" --args '{"сумма": 20000, "постоянный": true}' --trust
на веру: доказанность не считалась — запуск по ключу --trust
3000
What the compiler does
flang run computes one function itself: neither Node nor a C compiler is
needed. Before computing it checks the arguments against the declared types and
gives a verdict on what is proved.
The verdict matters here. Three of the model’s four promises rest only on
examples: the flang check --proof report calls them «сетка N» — checked on N
values from the examples, which is not a proof. Without explicit consent
flang run does not compute an unproved program and exits with code 3:
$ flang run static/spec/models/order-discount.flang --function "Скидка по сумме" --args '{"сумма": 20000, "постоянный": true}'
не доказано: утверждений 4: доказано 1, сетка 3, на веру 0 — запуск только по явному согласию: --на-веру
The --trust key gives consent for one run. The unproven key in .flangrc
gives it for all runs. The clients below pass --trust explicitly, so the
consent is visible in the code.
The boundary through JSON
The flang run contract is simple:
- arguments are
--args, a flat JSON of scalars: a number, a string, yes/no. Records and lists cannot be passed here, which is why the model has «Скидка по сумме»; - the value goes to stdout, on one line;
- the verdict on proofs and errors go to stderr, an error with a
FLANG_…code; - the exit code:
0— computed,1— an error in the input or in the program,2— the call itself could not be parsed,3— the program is not proved and the run without consent is refused.
The shape of the model is also available to a machine without a flang parser:
flang ast prints the parsed program as JSON — objects with fields and types,
functions with parameters and examples. The HTTP service builds its contract
from it, and the form builds its schema.
Install flang:
curl -fsSL https://raw.githubusercontent.com/digitable-lol/flang/main/install.sh | sh
The clients take the path to the binary from the FLANG variable, by default
flang from PATH.
The Python client
examples/spec/clients/python/calculate_discount.py uses only subprocess and
json from the standard library:
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())
The error is not swallowed. describe_failure looks in stderr for the first
line with a FLANG_… code — the reason for the refusal in the compiler’s words —
and shows it together with the exit code.
Run and output:
$ 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 вне неотрицательное
The Go client
examples/spec/clients/go/main.go uses only os/exec and 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 and stderr go into separate buffers: the reason for a refusal is not lost even if the process ended with an error. Output:
$ go run main.go
сумма=20000 постоянный_клиент=false -> скидка=2000
$ go run main.go -sum 20000 -loyal
сумма=20000 постоянный_клиент=true -> скидка=3000
The shell client
examples/spec/clients/shell/discount.sh is bash plus jq. jq builds the
argument JSON: gluing it from strings in bash breaks easily on quotes and on
the Unicode in сумма.
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 is lifted for the call on purpose: otherwise the script would exit
before reading stderr and showing the reason. Output:
$ ./discount.sh
сумма=20000 постоянный_клиент=false -> скидка=2000
$ ./discount.sh 20000 true
сумма=20000 постоянный_клиент=true -> скидка=3000
$ ./discount.sh -5 true
ошибка (код 1): FLANG_TYPE: вызов функции «Скидка по сумме»: аргумент «сумма»: -5 вне неотрицательное
Over HTTP instead of the CLI
If starting a process per call is expensive, there is a long-lived HTTP
service. examples/spec/discount-api works like that: the model’s examples run
once at start, the model is printed to JavaScript, and after that Node.js only
routes requests.
npm run spec:api
curl -s localhost:8788/discount -d '{"сумма":20000,"постоянный клиент":true}'
# {"discount": 3000}
A bad input comes back as 400 with the reason:
{
"error": "вход не подходит к «Покупке»: «сумма»: ждали неотрицательное целое",
"code": "INPUT"
}
Any client above can be rewritten to use requests, net/http or curl
instead of starting a process. The choice between the CLI and HTTP is a choice
between simplicity (one process per call, nothing kept alive) and throughput
(the model is prepared once, then only the network).
A third way: print the model into your language
Both ways above keep something next to your code: the flang binary or a Node
service. Sometimes that is not allowed. Then the model is printed into the
target language ahead of time with flang emit. There are ten targets:
$ flang emit static/spec/models/order-discount.flang --target zzz
flang emit: цели «zzz» у этой сборки flang нет — целей здесь ДЕСЯТЬ — «c», «cpp», «go», «rust», «java», «js», «ts», «elixir», «python», «csharp».
Printing to 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
The prodazhi.py module runs without dependencies. The model’s functions in it
are ordinary Python functions, with a role prefix in the name:
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>
The role prefix (fn_ for a function, rec_ for a record constructor) keeps an
object and a function with the same name from clashing after transliteration.
Next to it lies the runner flang_cli.py. A request is one JSON line, and so is
the answer:
$ echo '{"fn":"Рассчитать скидку","args":[{"r":[["сумма",{"n":"20000"}],["постоянный клиент",true]]}]}' | python3 flang_cli.py prodazhi
{"ok":true,"value":{"n":"3000"}}
Values in the protocol are tagged: a number travels as a string ({"n":"20000"})
so that -0, NaN and infinity are not lost; a string is {"s":"…"}; a record
is {"r":[["поле", …]]}; a flag is true/false. The runner checks the input
against the model’s types before the call:
$ echo '{"fn":"Рассчитать скидку","args":[{"r":[["сумма",{"n":"-1"}],["постоянный клиент",true]]}]}' | python3 flang_cli.py prodazhi
{"ok":false,"code":"FLANG_TYPE","message":"вызов функции «Рассчитать скидку»: аргумент «покупка».сумма: -1 вне неотрицательное"}
The protocol is the same for every target. The same model printed to 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"}}
Three thousand is exactly what the example «большая покупка постоянного клиента» in the model itself says. The runner is needed for a call through a pipe. In your own Java program the functions are called directly:
Ctx ctx = Prodazhi.newContext();
Value покупка = Prodazhi.rec_pokupka(Value.number(20000), Value.flag(true));
System.out.println(Prodazhi.fn_rasschitat_skidku(ctx, покупка)); // <flang число: 3000>
The printed code checks the model’s unproved promises on every call and throws
FLANG_PROPERTY if a promise is broken. Proved promises do not go into the
code.
What to choose
There are three ways, and the choice comes down to one question — what you are ready to keep next to your code.
| Way | What lives next to it | When to use it |
|---|---|---|
flang run in a process |
the flang binary and the model | rare calls, scripts, CI |
| HTTP service | a service with the model, one for everyone | high frequency, a short SLA |
| printing into the target language | nothing — only your code | flang cannot be installed, or a process per call is not acceptable |
The third way has a price. Printed code is a snapshot of the model at the moment
of printing: change the model, and you must print again. This is handled with a
--check step in CI that compares the printed code with the model (chapter
«Generation and CI»). One more thing:
flang emit does not run examples, so flang test is needed before printing.
Practice in the sandbox
Find the three promises in the proof report that rest only on examples. They
are why flang run asks for --trust. Change the amount in the example «тот же
расчёт без записи на входе» and see which example stops matching.
Common errors
Below are real answers of flang run on the model from this chapter.
FLANG_TYPE, a value of the wrong type (code 1):
FLANG_TYPE: вызов функции «Скидка по сумме»: аргумент «сумма» не соответствует типу неотрицательное
FLANG_TYPE, a missing argument (code 1, the example omits
постоянный):
FLANG_TYPE: вызов функции «Скидка по сумме» функции «Скидка по сумме» не задаёт аргумент «постоянный»
FLANG_UNKNOWN_NAME, an extra argument (code 1). flang does not let an
extra key through silently: most often it means the client and the model have
drifted apart.
FLANG_UNKNOWN_NAME: вызов функции «Скидка по сумме» функции «Скидка по сумме» задаёт неизвестный аргумент «вес»
FLANG_UNKNOWN_NAME, a typo in the function name (code 1):
FLANG_UNKNOWN_NAME: не найдена функция «Расчитать скидку»
A record in --args (code 2): --args accepts only a flat object of
scalars.
flang run: «--args» разобрать не удалось — ждался плоский объект скаляров, вроде '{"н":10}'
Code 3 and no value printed — the program is not proved, and --trust was
not passed.
The general rule for a client in any language: branch on the FLANG_… code and
the exit code, not on the message text. The text is written for a human.
Checklist
- The client reads stdout as the value only when the exit code is
0; otherwise it takes the reason from stderr. - Arguments are a flat JSON of scalars in
--args; for a record the model has a function with scalar arguments. - Consent to unproved code (
--trust) is passed explicitly, and the code shows that it is there. - Branching on an error uses the
FLANG_…code, not the text. - For rare calls — a process, for high frequency — an HTTP service, without
flang on the machine — a printed module and a
--checkstep in CI.