Executable specifications in flang Structures and types: the shape of subject data
0%

Structures and types: the shape of subject data

Structures and types: the shape of subject data

This chapter is about what a function’s input is made of: built-in types, optional fields, states and nested records. And about an honest boundary: what the compiler checks by itself and what is left to the application.

Minimal example

модуль «Счета»

тип «Валюта»
  вариант «Рубль»
  вариант «Доллар»
  вариант «Евро»

объект «Строка счёта»
  «номер»: строка
  «сумма»: неотрицательное
  «валюта»: «Валюта»
  «срок оплаты»: строка
  «просрочен»: признак
  комментарий иногда является строкой

объект is a record: a set of fields under one name. It has no behaviour — no methods, no constructor, no default values. A field is written as «имя»: тип or in the form имя является типом; иногда является gives a field the right to be absent.

A тип with variants is a closed list of values. The currency here is not the string "RUB" but one of three variants, and a typo in a variant name will not pass the check.

What the compiler does

flang check parses the declarations and checks the types. A field’s type must be known: built in, declared in the module, or brought in with использует. If the type is declared nowhere, the file does not pass:

модуль «Счета»

объект «Строка счёта»
  «номер»: строка
  «валюта»: «Валюта»
FLANG_UNKNOWN_NAME … неизвестный тип «Валюта»

flang ast prints the parsed model — JSON with the types and functions. Form, table and service contract adapters read it. For the minimal example (the span and fts fields are left out):

[
  { "kind": "sum", "name": "Валюта",
    "variants": [ { "name": "Рубль", "fields": [] }, { "name": "Доллар", "fields": [] }, { "name": "Евро", "fields": [] } ] },
  { "kind": "record", "name": "Строка счёта",
    "fields": [
      { "name": "номер", "type": { "kind": "string" } },
      { "name": "сумма", "type": { "kind": "named", "name": "неотрицательное" } },
      { "name": "валюта", "type": { "kind": "named", "name": "Валюта" } },
      { "name": "срок оплаты", "type": { "kind": "string" } },
      { "name": "просрочен", "type": { "kind": "flag" } },
      { "name": "комментарий", "type": { "kind": "string" }, "optional": true }
    ] }
]

Optionality is a separate flag, "optional": true, not a part of the type string.

Built-in types

flang What it is In flang ast In TypeScript (flang emit --target ts)
строка text string string
число an IEEE 754 number, NaN included number number
неотрицательное an integer from 0 to 2⁵³−1 named number
целое an integer with no lower bound named number
сотых money as a whole number of cents named any
признак да or нет flag any
список строки a list list Array<string>
иногда является … the field may be absent "optional": true the field’s type, with no null in it

A type is not decoration. It decides what the compiler can prove. A неотрицательное has two bounds, and a promise like “the total is not less than a summand” follows for the kernel from the argument types alone. A число has no bounds, and the same promise stays unproved.

The compiler also accepts the words дата and деньги, but they are not separate types: дата is the same string ("kind": "string") and деньги the same number ("kind": "number"). That is why "вчера" passes as a date just as easily as "2026-07-15":

модуль «Сроки»

объект «Счёт»
  «срок»: дата

тотальная функция «Срок оплаты»
  принимает счёт: «Счёт»
  возвращает строка
  пример «любая строка проходит как дата»
    дано счёт равно запись «Счёт» с «срок» равным "вчера"
    ожидается "вчера"
  счёт.«срок»

flang 0.7.29 has no calendar type. The application parses and checks the date at the boundary, and the model receives an already checked string of the form ГГГГ-ММ-ДД or a number of days. There is no built-in currency type either — a currency is declared as its own type with variants, as in the minimal example.

An optional field

иногда является lets you leave a field out when building a record. The other side: in 0.7.29 such a field is of little use inside functions. The value read from it has the type “string or nothing”, and it cannot be compared with ничто:

FLANG_TYPE … сравнение «eq» между строка или ничто и ничто

If a rule must behave differently depending on whether a value is present, declare a type with variants:

модуль «Заявки»

тип «Комментарий»
  вариант «Есть» содержит текст: строка
  вариант «Нет»

объект «Заявка»
  «имя»: строка
  «комментарий»: «Комментарий»

тотальная функция «Нужен ответ менеджера»
  принимает заявка: «Заявка»
  возвращает признак
  пример «без комментария ответ не нужен»
    дано заявка равно запись «Заявка» с «имя» равным "Анна" и «комментарий» равным (вариант «Нет»)
    ожидается нет
  разбор заявка.«комментарий»
    случай вариант «Есть» с текст как текст
      то да
    случай «Нет»
      то нет

иногда является stays useful where the field only travels further: into a form, into a table, into the printed code.

A state is a type with variants

An order’s state is not a string and not a set of flags but a type with variants. A variant may carry fields of its own: a shipped order has a waybill, a created one does not.

модуль «Заказы»

тип «Состояние заказа»
  вариант «Создан»
  вариант «Оплачен»
  вариант «Отгружен» содержит «накладная»: строка

тотальная функция «Можно отгрузить»
  принимает заказ: «Состояние заказа»
  возвращает признак
  пример «оплаченный заказ можно отгрузить»
    дано заказ равно вариант «Оплачен»
    ожидается да
  разбор заказ
    случай «Создан»
      то нет
    случай «Оплачен»
      то да
    случай вариант «Отгружен» с «накладная» как накладная
      то нет

разбор must name every variant. Remove the last case and the file will not pass the check:

FLANG_MATCH_NOT_EXHAUSTIVE … разбор «Состояние заказа» не покрывает «Отгружен»

That is the point. A new state that someone forgot about in a rule shows up at check time, not in production. A chain of если … иначе если over a state string gives no such guarantee: a forgotten case silently falls into the last иначе.

A nested record

If an object has compound data — not one value but several fields together — the field gets the type of another record:

модуль «Доставка»

объект «Адрес»
  «город»: строка
  «индекс»: строка

объект «Заказ»
  «номер»: строка
  «адрес доставки»: «Адрес»

тотальная функция «Город доставки»
  принимает заказ: «Заказ»
  возвращает строка
  пример «город берётся из вложенной записи»
    дано заказ равно запись «Заказ» с «номер» равным "ЗК-1" и «адрес доставки» равным (запись «Адрес» с «город» равным "Казань" и «индекс» равным "420000")
    ожидается "Казань"
  заказ.«адрес доставки».«город»

In flang ast such a field is { "kind": "named", "name": "Адрес" }. The fields of the nested record are not expanded there: a form adapter looks up the address record in the types list itself if it needs its fields.

What an object turns into

A form does not have to be described a second time — its fields are already in the model. examples/spec/form-schema/schema.mjs turns a record from flang ast into a form description:

export function flangFormSchema(ast, objectName) {
  const records = ast.types.filter((item) => item.kind === 'record');
  const record = records.find((item) => item.name === objectName.replace(/^«|»$/gu, ''));
  if (!record) {
    throw new Error(`в модели нет объекта «${objectName}» (есть: ${records.map((item) => item.name).join(', ')})`);
  }
  return {
    id: `${ast.module}.${record.name}`,
    title: record.name,
    fields: record.fields.map((field) => {
      const control = controlFor(field.type, ast);
      const entry = { name: field.name, label: field.name, type: typeName(field.type), control, required: !field.optional };
      if (control === 'select') {
        entry.options = ast.types.find((item) => item.name === entry.type).variants.map((variant) => variant.name);
      }
      if (entry.type === 'неотрицательное') entry.min = 0;
      return entry;
    }),
  };
}

A flag becomes a checkbox, неотрицательное a numeric field with min: 0, and a field with a variant type a choice among the variants. Whether a field is required comes from optional. Layout, hints and accessibility stay with the UI component: the model only removes the duplication of names, types and requiredness.

How types reach TypeScript

flang emit --target ts prints a module where every record is a TypeScript type. For the invoice line from invoices-table.flang:

export type StrokaSchyota = {
  номер: string; контрагент: string; сумма: number; валюта: Valyuta; "срок оплаты": string; просрочен: any
};

Two things are worth knowing in advance. признак and сотых are printed as any, so TypeScript will not catch a string in place of a flag. The input boundary does that: the flang_cli.js runner or a check in the service. An optional field is printed with its own type and without null, and the record factory puts null into an absent field.

Type and function names in the printed code are transliterations of the model’s names (StrokaSchyota). Under their original names the functions are available through $PROGRAM.functions.

Practice in the sandbox

Find the currency type and the «валюта» field of the invoice line record. In an example, change (вариант «Рубль») to (вариант «Юань») and run the check: the compiler answers that it expected a variant of the currency type instead of quietly accepting the value.

The decision type has two variants, and the refusal variant has a причина field. In the function that returns the refusal reason, remove the «Разрешено» case and read what the check answers.

The fields «вес» and «расстояние» are different units of measure, but to the compiler both are just неотрицательное. Think about which separate record would protect against mixed-up values.

Common mistakes

FLANG_PARSE … поле записывается как '«имя» является типом' — the field line matches neither «имя»: тип nor имя является типом. A common cause is a missing word: сумма деньгами instead of «сумма»: деньги.

FLANG_UNKNOWN_NAME … неизвестный тип — the field’s type is declared nowhere. Declare it in the module (a тип with variants or an объект) or bring in the module where it is declared.

FLANG_MATCH_NOT_EXHAUSTIVE — a разбор does not name some variant. Add the case instead of hiding it behind a general branch: this error is the whole reason a state is made a type.

A name taken by the language. Words like с or состояние are reserved, and a parameter with such a name gives FLANG_PARSE … это слово занято языком. Put the name in guillemets or rename it.

Checklist

  • I know the built-in types and that дата and деньги are a string and a number, not separate checks.
  • My state is a type with variants, and the match names every variant.
  • A field a rule depends on is required or written as a present/absent variant type rather than with иногда является.
  • I can read a record in flang ast and predict its type in the printed TypeScript.

Catalog cases on this topic

Next: rules, order and promises

Spotted a mistake? Select a fragment of the text — a bug icon will appear next to it.

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

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

Доска запросов
Next