FTS Executable Utilities: Rules, Order, and Properties
A utility is a pure deterministic computation over one object: tariff, discount, classifier, limit. It does not access the network and does not write to the database. In this chapter — how to write several rules so that the order is predictable, not guessed based on code reading experience, and how to make the compiler stop if the result goes beyond acceptable boundaries.
Minimal example
категория «Продажи»
объект Покупка
сумма является деньгами
утилита «Рассчитать скидку»
принимает Покупка
возвращает деньги
начинает с 0
правило «Большая покупка»
если сумма не меньше 10000
то добавить 10 процентов от поля сумма
пример «Ниже порога»
дано сумма равна 5000
ожидается результат равен 0
пример «Выше порога»
дано сумма равна 20000
ожидается результат равен 2000
The object describes the input form. The utility names the input (принимает), the result
type (возвращает) and the required initial value (начинает с).
Without it, the result does not emerge from the implicit null or 0 by default runtime
— zero in the example above is written by the model author, not inserted by the compiler.
What the compiler does
- Natural syntax is parsed into canonical
FtsDocument— the same format is read byvalidate, the interpreter, and the TypeScript generator. validatechecks the structure: each rule has a condition and an action, each property has one result comparison, and references to fields exist in theпринимаетobject.- Upon execution (
executeUtilityor the generated function), the result receivesначинает с. - Rules are processed from top to bottom; for each, the condition is checked.
- For rules with a true condition, the action is executed:
то результат равенreplaces the result,то добавитьadds a number to it. - After all rules are processed in order, all properties are checked.
- If a property is violated, execution stops with diagnostic
FTS_UTILITY_PROPERTY— the value does not escape. - If all properties are satisfied, the calling side receives the final result.
This sequence is the same for the interpreter and for the generated
fts.utilities.ts: the generator has no right to optimize the rule order or
skip a property check.
Rules are not if/else
All rules whose conditions are true are executed in the order of declaration. This
is a fundamental difference from the if / else if chain, where a triggered branch
excludes the others:
категория «Продажи»
объект Покупка
сумма является деньгами
«постоянный клиент» является признаком
промокод является строкой
утилита «Рассчитать скидку»
принимает Покупка
возвращает деньги
начинает с 0
правило «Большая покупка»
если сумма не меньше 10000
и сумма не больше 100000
то добавить 10 процентов от поля сумма
правило «Постоянный клиент»
если «постоянный клиент» равен да
и сумма больше 0
и сумма не больше 100000
то добавить 5 процентов от поля сумма
правило «Промокод на крупный заказ»
если сумма не меньше 15000
и сумма не больше 100000
и промокод равен «ЛЕТО2026»
то добавить 7 процентов от поля сумма
правило «Очень крупная покупка»
если сумма больше 100000
то добавить 22000
свойство «Скидка ограничена»
результат не больше 22000
пример «Обычная покупка»
дано сумма равна 5000
дано «постоянный клиент» равен нет
дано промокод равен «НЕТ»
ожидается результат равен 0
пример «Крупная покупка без промокода»
дано сумма равна 20000
дано «постоянный клиент» равен нет
дано промокод равен «ЗИМА»
ожидается результат равен 2000
пример «Крупная покупка постоянного клиента с промокодом»
дано сумма равна 20000
дано «постоянный клиент» равен да
дано промокод равен «ЛЕТО2026»
ожидается результат равен 4400
пример «Покупка ровно на потолок скидки»
дано сумма равна 100000
дано «постоянный клиент» равен да
дано промокод равен «ЛЕТО2026»
ожидается результат равен 22000
For the example with twenty thousand, three out of four rules apply: 10% + 5% + 7% of 20 000 give 4 400. No rule “cancels” another — they add up, because each of them adds to the overall result.
The discount ceiling is recorded as a number (результат не больше 22000), not as a percentage of the field, and the boundaries by amount are stated directly in the rule conditions. The property is checked across the entire input domain, not just where the rules triggered: a limit of the form 25 процентов от поля сумма would go negative on a negative sum and break the calculation where no rule triggered. A detailed walkthrough is in the chapter on antipatterns.
Several conditions of one rule are joined by the word и: each subsequent
condition — a separate line with the same indentation as если. The third rule
will trigger only if both lines are true simultaneously.
The order is not meaningless: if a rule uses то результат равен, it overwrites everything
accumulated by the previous rules. Such a rule should be declared last
consciously, and not rely on the compiler to assign priorities on its own — it doesn’t do that.
Comparisons and Percentages
Six comparisons are available: равен, не равен, больше, меньше, не больше,
не меньше. равен and не равен work for any scalar value —
number, flag, string. The remaining four require both sides of the comparison
to be numbers: they will not compare a string with a date, the compiler will not
allow such a utility through validate, and at runtime comparing incompatible types
stops execution.
N процентов от поля X is computed from the value of the X field in the input data, not from the current result. The field name is specified exactly as it is declared in the object, without any grammatical cases: 10 процентов от поля сумма, 2 процента от поля «страховая сумма». The compiler does not attempt to decline Russian nouns — this is an intentional limitation, not an oversight.
Property — postcondition, not autocorrection
свойство «Скидка ограничена»
результат не больше 15000
The property is checked after all rules have executed. If the comparison
is false, FTS does not trim the discount to an acceptable limit and does not choose
a “safe” number instead of the incorrect one — it halts execution with
diagnostic FTS_UTILITY_PROPERTY and the name of the violated property. This
is a deliberate boundary: silent correction of the result would hide the error in the
model or in the input data, while rolling back outward forces it to be seen and fixed in the
rule itself.
A property is written as one comparison of the result — результат <сравнение> <выражение>. An expression can be a number, a field value, or a percentage of a field,
as in the example above.
Determinism and Utility Bounds
The same input data always produce the same result: the utility does not read the clock, does not call outside, and does not store state between invocations. This allows testing it without mocks and reusing the same calculation in the interpreter, in generated TypeScript, and in the browser sandbox — they must match because they read the same model.
Good candidates for utility: discount, tariff, commission, classifier, limit, value normalizer, command decision (“allow”/“reject” based on clear numeric thresholds). Bad candidates: “send an email”, “get currency rate from the network”, “record order in transactions”. As soon as a computation depends on the outside world or produces an effect, it ceases to be reproducible — and should remain in regular code that calls the utility, and not the other way around.
Practice in the sandbox
- Open the “Execute” tab and calculate the discount for the amount 12 000 without the regular customer status. Then add the status — make sure the result increases exactly by 5 % of the same amount, not recalculated from zero.
- Open
delivery-price.fts(tabcheck) and find two rules that apply the base rate: they set the lower limit, not the lineначинает с— that one sets zero. Determine which keyword in the property is responsible for “not less than”, not for “not more than”. - In
order-discount.ftsreplace the ceiling15000with a stricter one — for example,результат не больше 10 процентов от поля сумма— and runrunfor a regular customer with the amount 20 000. A discount of 3000 against the limit of 2000: make sure the tab shows diagnostics, not the corrected number.
Common Fallacies
FTS_EXPECTED_KEYWORD— file does not start withкатегория «Имя». Natural parsing requires the category declaration as the first line; without it the compiler reverts to the outdated bracket syntax and complains about the absence of a literalcategory. Check the first non-empty line of the file.FTS_NATURAL_DECLARATION— at the top level of the category, a word was encountered that is notобъект,структура,морфизм,теорема, orутилита(for example, a typo likeутилиттаor a custom wordштука). The compiler lists the valid options directly in the message.FTS_UTILITY_PROPERTY— the rules executed, but the result failed the property check. This is not a syntax error, but a signal that the input data or the rules themselves produce a result beyond the declared boundary. The message names both the property and the utility:нарушено свойство «Имя» утилиты «Имя».
Checklist
- The utility has
принимает,возвращаетandначинает с— without an initial value the utility won’t compile. - Each rule starts with
еслиand ends with one action:то добавитьorто результат равен. - Multiple conditions of one rule — separate lines
и, not one line separated by commas. - Ordinal comparisons (
больше,меньше,не больше,не меньше) — only for numbers; for the rest there areравенandне равен. - A property — one result comparison, checked after all rules and not trying to fix anything itself.
- At least one example is required for
fts test; without examples the command will end withFTS_NO_UTILITY_EXAMPLES— more details in the next chapter.